diff options
Diffstat (limited to 'java')
| -rw-r--r-- | java/src/com/android/intentresolver/ResolverListAdapter.java | 29 | ||||
| -rw-r--r-- | java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt | 256 |
2 files changed, 270 insertions, 15 deletions
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index a243ac2a..4ad8d926 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -58,6 +58,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; public class ResolverListAdapter extends BaseAdapter { private static final String TAG = "ResolverListAdapter"; @@ -82,6 +83,7 @@ public class ResolverListAdapter extends BaseAdapter { private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>(); private final Executor mBgExecutor; private final Handler mMainHandler; + private final AtomicBoolean mDestroyed = new AtomicBoolean(); private ResolveInfo mLastChosen; private DisplayResolveInfo mOtherProfile; @@ -93,7 +95,6 @@ public class ResolverListAdapter extends BaseAdapter { private int mLastChosenPosition = -1; private final boolean mFilterLastUsed; - private Runnable mPostListReadyRunnable; private boolean mIsTabLoaded; // Represents the UserSpace in which the Initial Intents should be resolved. private final UserHandle mInitialIntentsUserSpace; @@ -546,17 +547,17 @@ public class ResolverListAdapter extends BaseAdapter { * @param rebuildCompleted Whether the list has been completely rebuilt */ void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) { - if (mPostListReadyRunnable == null) { - mPostListReadyRunnable = new Runnable() { - @Override - public void run() { - mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, - doPostProcessing, rebuildCompleted); - mPostListReadyRunnable = null; + Runnable listReadyRunnable = new Runnable() { + @Override + public void run() { + if (mDestroyed.get()) { + return; } - }; - mMainHandler.post(mPostListReadyRunnable); - } + mResolverListCommunicator.onPostListReady(ResolverListAdapter.this, + doPostProcessing, rebuildCompleted); + } + }; + mMainHandler.post(listReadyRunnable); } private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { @@ -772,10 +773,8 @@ public class ResolverListAdapter extends BaseAdapter { } public void onDestroy() { - if (mPostListReadyRunnable != null) { - mMainHandler.removeCallbacks(mPostListReadyRunnable); - mPostListReadyRunnable = null; - } + mDestroyed.set(true); + if (mResolverListController != null) { mResolverListController.destroy(); } diff --git a/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt index a5fe6c47..0f381aac 100644 --- a/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt +++ b/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt @@ -22,15 +22,22 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.ResolveInfo +import android.os.Handler +import android.os.Looper import android.os.UserHandle import android.view.LayoutInflater +import com.android.intentresolver.ResolverListAdapter.ResolverListCommunicator import com.android.intentresolver.icons.TargetDataLoader import com.android.intentresolver.util.TestExecutor import com.android.intentresolver.util.TestImmediateHandler import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger import org.junit.Test import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.verify private const val PKG_NAME = "org.pkg.app" private const val PKG_NAME_TWO = "org.pkgtwo.app" @@ -663,6 +670,255 @@ class ResolverListAdapterTest { assertThat(testSubject.unfilteredResolveList).hasSize(2) } + @Test + fun testPostListReadyAtEndOfRebuild_synchronous() { + val communicator = mock<ResolverListCommunicator> {} + val testSubject = + ResolverListAdapter( + context, + payloadIntents, + /*initialIntents=*/ null, + /*rList=*/ null, + /*filterLastUsed=*/ true, + resolverListController, + userHandle, + targetIntent, + communicator, + /*initialIntentsUserSpace=*/ userHandle, + targetDataLoader, + executor, + testHandler, + ) + val doPostProcessing = false + + executor.runUntilIdle() + + testSubject.rebuildList(doPostProcessing) + verify(communicator).onPostListReady(testSubject, doPostProcessing, true) + } + + @Test + fun testPostListReadyAtEndOfRebuild_stages() { + // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks. + val resolvedTargets = + createResolvedComponents( + ComponentName(PKG_NAME, CLASS_NAME), + ComponentName(PKG_NAME_TWO, CLASS_NAME), + ) + // TODO: there's a lot of boilerplate required for this test even to trigger the expected + // conditions; if the configuration is incorrect, the test may accidentally pass for the + // wrong reasons. Separating responsibilities to other components will help minimize the + // *amount* of boilerplate, but we should also consider setting up test defaults that work + // according to our usual expectations so that we don't overlook false-negative results. + whenever( + resolverListController.getResolversForIntentAsUser( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(resolvedTargets) + val communicator = + mock<ResolverListCommunicator> { + whenever(getReplacementIntent(any(), any())).thenAnswer { invocation -> + invocation.arguments[1] + } + } + val testSubject = + ResolverListAdapter( + context, + payloadIntents, + /*initialIntents=*/ null, + /*rList=*/ null, + /*filterLastUsed=*/ true, + resolverListController, + userHandle, + targetIntent, + communicator, + /*initialIntentsUserSpace=*/ userHandle, + targetDataLoader, + executor, + testHandler, + ) + val doPostProcessing = false + + testSubject.rebuildList(doPostProcessing) + + executor.runUntilIdle() + + val inOrder = inOrder(communicator) + inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, false) + inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, true) + } + + @Test + fun testPostListReadyAtEndOfRebuild_queued() { + // Set up a runnable that blocks the callback handler until we're ready to start + // processing queued messages. This is to test against a legacy bug where subsequent + // list-ready notifications could be dropped if the older one wasn't yet dequeued, even if + // the newer ones had different parameters. + // TODO: after removing the logic responsible for this "dropping" we could migrate off the + // `Handler` API (to `Executor` or similar) and use a simpler mechanism to test. + val callbackHandler = Handler(Looper.getMainLooper()) + val unblockCallbacksSignal = CountDownLatch(1) + val countdownBlockingRunnable = Runnable { unblockCallbacksSignal.await() } + callbackHandler.post(countdownBlockingRunnable) + + // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks. + val resolvedTargets = + createResolvedComponents( + ComponentName(PKG_NAME, CLASS_NAME), + ComponentName(PKG_NAME_TWO, CLASS_NAME), + ) + // TODO: there's a lot of boilerplate required for this test even to trigger the expected + // conditions; if the configuration is incorrect, the test may accidentally pass for the + // wrong reasons. Separating responsibilities to other components will help minimize the + // *amount* of boilerplate, but we should also consider setting up test defaults that work + // according to our usual expectations so that we don't overlook false-negative results. + whenever( + resolverListController.getResolversForIntentAsUser( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(resolvedTargets) + val communicator = + mock<ResolverListCommunicator> { + whenever(getReplacementIntent(any(), any())).thenAnswer { invocation -> + invocation.arguments[1] + } + } + val testSubject = + ResolverListAdapter( + context, + payloadIntents, + /*initialIntents=*/ null, + /*rList=*/ null, + /*filterLastUsed=*/ true, + resolverListController, + userHandle, + targetIntent, + communicator, + /*initialIntentsUserSpace=*/ userHandle, + targetDataLoader, + executor, + callbackHandler + ) + val doPostProcessing = false + testSubject.rebuildList(doPostProcessing) + + // Finish all the background work (enqueueing both the "partial" and "complete" progress + // callbacks) before dequeueing either callback. + executor.runUntilIdle() + + // Allow the handler to flush out and process both callbacks. + unblockCallbacksSignal.countDown() + + // Finally, force a synchronization in the other direction to ensure that we've finished + // processing any callbacks before we start making assertions about them. + // TODO: there are less "ad-hoc" ways to write this (e.g. with a special `Handler` subclass + // for tests), but we should switch off using `Handler` altogether (in favor of `Executor` + // or otherwise), and then it'll be much simpler to clean up this boilerplate. + val unblockAssertionsSignal = CountDownLatch(1) + val countdownAssertionsRunnable = Runnable { unblockAssertionsSignal.countDown() } + callbackHandler.post(countdownAssertionsRunnable) + unblockAssertionsSignal.await() + + // TODO: we may not necessarily care to assert that there's a "partial progress" callback in + // this case, since there won't be a chance to reflect the "partial" state in the UI before + // the "completion" is queued (and if we depend on seeing an intermediate state, that could + // be a bad sign for our handling in the "synchronous" case?). But we should probably at + // least assert that the "partial" callback never arrives *after* the completion? + val inOrder = inOrder(communicator) + inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, false) + inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, true) + } + + @Test + fun testPostListReadyAtEndOfRebuild_skippedIfStillQueuedOnDestroy() { + // Set up a runnable that blocks the callback handler until we're ready to start + // processing queued messages. + // TODO: after removing the logic responsible for this "dropping" we could migrate off the + // `Handler` API (to `Executor` or similar) and use a simpler mechanism to test. + val callbackHandler = Handler(Looper.getMainLooper()) + val unblockCallbacksSignal = CountDownLatch(1) + val countdownBlockingRunnable = Runnable { unblockCallbacksSignal.await() } + callbackHandler.post(countdownBlockingRunnable) + + // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks. + val resolvedTargets = + createResolvedComponents( + ComponentName(PKG_NAME, CLASS_NAME), + ComponentName(PKG_NAME_TWO, CLASS_NAME), + ) + // TODO: there's a lot of boilerplate required for this test even to trigger the expected + // conditions; if the configuration is incorrect, the test may accidentally pass for the + // wrong reasons. Separating responsibilities to other components will help minimize the + // *amount* of boilerplate, but we should also consider setting up test defaults that work + // according to our usual expectations so that we don't overlook false-negative results. + whenever( + resolverListController.getResolversForIntentAsUser( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(resolvedTargets) + val communicator = + mock<ResolverListCommunicator> { + whenever(getReplacementIntent(any(), any())).thenAnswer { invocation -> + invocation.arguments[1] + } + } + val testSubject = + ResolverListAdapter( + context, + payloadIntents, + /*initialIntents=*/ null, + /*rList=*/ null, + /*filterLastUsed=*/ true, + resolverListController, + userHandle, + targetIntent, + communicator, + /*initialIntentsUserSpace=*/ userHandle, + targetDataLoader, + executor, + callbackHandler + ) + val doPostProcessing = false + testSubject.rebuildList(doPostProcessing) + + // Finish all the background work (enqueueing both the "partial" and "complete" progress + // callbacks) before dequeueing either callback. + executor.runUntilIdle() + + // Notify that our activity is being destroyed while the callbacks are still queued. + testSubject.onDestroy() + + // Allow the handler to flush out, but now the callbacks are gone. + unblockCallbacksSignal.countDown() + + // Finally, force a synchronization in the other direction to ensure that we've finished + // processing any callbacks before we start making assertions about them. + // TODO: there are less "ad-hoc" ways to write this (e.g. with a special `Handler` subclass + // for tests), but we should switch off using `Handler` altogether (in favor of `Executor` + // or otherwise), and then it'll be much simpler to clean up this boilerplate. + val unblockAssertionsSignal = CountDownLatch(1) + val countdownAssertionsRunnable = Runnable { unblockAssertionsSignal.countDown() } + callbackHandler.post(countdownAssertionsRunnable) + unblockAssertionsSignal.await() + + verify(communicator, never()).onPostListReady(eq(testSubject), eq(doPostProcessing), any()) + } + private fun createResolvedComponents( vararg components: ComponentName ): List<ResolvedComponentInfo> { |