summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2023-09-12 14:20:36 -0700
committer Andrey Epin <ayepin@google.com> 2023-09-18 09:59:57 -0700
commit1c0f4edb1114be0b7b994d6d7a7e3c6f0c5215d0 (patch)
tree5c3e2ea23f153b49c227a0a1bac138e67fe3d16b
parent41e6f25694939bb4c88485d0513e413da772931b (diff)
Add ResolverListAdapter unit tests
Replace AsyncTask usage with an background executor and posting on a main thread hanler to facilitate testing. Tests are mostly around targets resolution logic in the adapter. Test: atest IntentResolverUnitTests:ResolverListAdapterTest Change-Id: I7af047226aa718ca3052aa4284d1e9d2a4c43ded
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java46
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java14
-rw-r--r--java/src/com/android/intentresolver/ResolverInfoHelpers.kt34
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java103
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java2
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt727
-rw-r--r--java/tests/src/com/android/intentresolver/util/TestExecutor.kt40
-rw-r--r--java/tests/src/com/android/intentresolver/util/TestImmediateHandler.kt42
8 files changed, 938 insertions, 70 deletions
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index d1101f1e..a9ed983d 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -43,6 +43,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
@@ -567,7 +570,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in callerTargets.
for (TargetInfo existingInfo : mCallerTargets) {
- if (mResolverListCommunicator.resolveInfoMatch(
+ if (ResolveInfoHelpers.resolveInfoMatch(
dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -658,30 +661,23 @@ public class ChooserListAdapter extends ResolverListAdapter {
* in the head of input list and fill the tail with other elements in undetermined order.
*/
@Override
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- Trace.beginSection("ChooserListAdapter#SortingTask");
- mResolverListController.topK(params[0], mMaxRankedTargets);
- Trace.endSection();
- return params[0];
- }
-
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- notifyDataSetChanged();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ Trace.beginSection("ChooserListAdapter#SortingTask");
+ mResolverListController.topK(components, mMaxRankedTargets);
+ Trace.endSection();
}
+ @Override
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ //TODO: this method is different from super's only in that `notifyDataSetChanged` is
+ // called conditionally here; is it really important?
+ notifyDataSetChanged();
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 0d3becc2..47a8bf2a 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -2262,20 +2262,6 @@ public class ResolverActivity extends FragmentActivity implements
mRetainInOnStop = retainInOnStop;
}
- /**
- * Check a simple match for the component of two ResolveInfos.
- */
- @Override // ResolverListCommunicator
- public final boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
- return lhs == null ? rhs == null
- : lhs.activityInfo == null ? rhs.activityInfo == null
- : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
- && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName)
- // Comparing against resolveInfo.userHandle in case cloned apps are present,
- // as they will have the same activityInfo.
- && Objects.equals(lhs.userHandle, rhs.userHandle);
- }
-
private boolean inactiveListAdapterHasItems() {
if (!shouldShowTabs()) {
return false;
diff --git a/java/src/com/android/intentresolver/ResolverInfoHelpers.kt b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
new file mode 100644
index 00000000..8d1d8658
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ResolveInfoHelpers")
+
+package com.android.intentresolver
+
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+
+fun resolveInfoMatch(lhs: ResolveInfo?, rhs: ResolveInfo?): Boolean =
+ (lhs === rhs) ||
+ ((lhs != null && rhs != null) &&
+ activityInfoMatch(lhs.activityInfo, rhs.activityInfo) &&
+ // Comparing against resolveInfo.userHandle in case cloned apps are present,
+ // as they will have the same activityInfo.
+ lhs.userHandle == rhs.userHandle)
+
+private fun activityInfoMatch(lhs: ActivityInfo?, rhs: ActivityInfo?): Boolean =
+ (lhs === rhs) ||
+ (lhs != null && rhs != null && lhs.name == rhs.name && lhs.packageName == rhs.packageName)
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 282a672f..14ce0e0e 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -28,6 +28,7 @@ import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
@@ -42,6 +43,9 @@ import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.icons.TargetDataLoader;
@@ -53,6 +57,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -75,6 +80,8 @@ public class ResolverListAdapter extends BaseAdapter {
private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>();
private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>();
+ private final Executor mBgExecutor;
+ private final Handler mMainHandler;
private ResolveInfo mLastChosen;
private DisplayResolveInfo mOtherProfile;
@@ -103,6 +110,37 @@ public class ResolverListAdapter extends BaseAdapter {
ResolverListCommunicator resolverListCommunicator,
UserHandle initialIntentsUserSpace,
TargetDataLoader targetDataLoader) {
+ this(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ AsyncTask.SERIAL_EXECUTOR,
+ context.getMainThreadHandler());
+ }
+
+ @VisibleForTesting
+ public ResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ ResolverListCommunicator resolverListCommunicator,
+ UserHandle initialIntentsUserSpace,
+ TargetDataLoader targetDataLoader,
+ Executor bgExecutor,
+ Handler mainHandler) {
mContext = context;
mIntents = payloadIntents;
mInitialIntents = initialIntents;
@@ -117,6 +155,8 @@ public class ResolverListAdapter extends BaseAdapter {
mTargetIntent = targetIntent;
mResolverListCommunicator = resolverListCommunicator;
mInitialIntentsUserSpace = initialIntentsUserSpace;
+ mBgExecutor = bgExecutor;
+ mMainHandler = mainHandler;
}
public final DisplayResolveInfo getFirstDisplayResolveInfo() {
@@ -402,35 +442,42 @@ public class ResolverListAdapter extends BaseAdapter {
// Send an "incomplete" list-ready while the async task is running.
postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
- createSortingTask(doPostProcessing).execute(filteredResolveList);
+ mBgExecutor.execute(() -> {
+ List<ResolvedComponentInfo> sortedComponents = null;
+ //TODO: the try-catch logic here is to formally match the AsyncTask's behavior.
+ // Empirically, we don't need it as in the case on an exception, the app will crash and
+ // `onComponentsSorted` won't be invoked.
+ try {
+ sortComponents(filteredResolveList);
+ sortedComponents = filteredResolveList;
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to sort components", t);
+ throw t;
+ } finally {
+ final List<ResolvedComponentInfo> result = sortedComponents;
+ mMainHandler.post(() -> onComponentsSorted(result, doPostProcessing));
+ }
+ });
return false;
}
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- mResolverListController.sort(params[0]);
- return params[0];
- }
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- notifyDataSetChanged();
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ mResolverListController.sort(components);
}
- protected void processSortedList(List<ResolvedComponentInfo> sortedComponents,
- boolean doPostProcessing) {
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ notifyDataSetChanged();
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ }
+ }
+
+ protected void processSortedList(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
final int n = sortedComponents != null ? sortedComponents.size() : 0;
Trace.beginSection("ResolverListAdapter#processSortedList:" + n);
if (n != 0) {
@@ -509,7 +556,7 @@ public class ResolverListAdapter extends BaseAdapter {
mPostListReadyRunnable = null;
}
};
- mContext.getMainThreadHandler().post(mPostListReadyRunnable);
+ mMainHandler.post(mPostListReadyRunnable);
}
}
@@ -572,7 +619,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in display.
for (DisplayResolveInfo existingInfo : mDisplayList) {
- if (mResolverListCommunicator
+ if (ResolveInfoHelpers
.resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -728,7 +775,7 @@ public class ResolverListAdapter extends BaseAdapter {
public void onDestroy() {
if (mPostListReadyRunnable != null) {
- mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
+ mMainHandler.removeCallbacks(mPostListReadyRunnable);
mPostListReadyRunnable = null;
}
if (mResolverListController != null) {
@@ -856,8 +903,6 @@ public class ResolverListAdapter extends BaseAdapter {
*/
interface ResolverListCommunicator {
- boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
-
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index d5a5fedf..cb56ab30 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -254,7 +254,6 @@ public class ResolverListController {
isComputed = true;
}
- @VisibleForTesting
@WorkerThread
public void sort(List<ResolvedComponentInfo> inputList) {
try {
@@ -273,7 +272,6 @@ public class ResolverListController {
}
}
- @VisibleForTesting
@WorkerThread
public void topK(List<ResolvedComponentInfo> inputList, int k) {
if (inputList == null || inputList.isEmpty() || k <= 0) {
diff --git a/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt
new file mode 100644
index 00000000..a5fe6c47
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverListAdapterTest.kt
@@ -0,0 +1,727 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import android.view.LayoutInflater
+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.atomic.AtomicInteger
+import org.junit.Test
+import org.mockito.Mockito.anyBoolean
+
+private const val PKG_NAME = "org.pkg.app"
+private const val PKG_NAME_TWO = "org.pkgtwo.app"
+private const val CLASS_NAME = "org.pkg.app.TheClass"
+
+class ResolverListAdapterTest {
+ private val testHandler = TestImmediateHandler()
+ private val layoutInflater = mock<LayoutInflater>()
+ private val context =
+ mock<Context> {
+ whenever(getSystemService(Context.LAYOUT_INFLATER_SERVICE)).thenReturn(layoutInflater)
+ whenever(mainThreadHandler).thenReturn(testHandler)
+ }
+ private val targetIntent = Intent(Intent.ACTION_SEND)
+ private val payloadIntents = listOf(targetIntent)
+ private val resolverListController =
+ mock<ResolverListController> {
+ whenever(filterIneligibleActivities(any(), anyBoolean())).thenReturn(null)
+ whenever(filterLowPriority(any(), anyBoolean())).thenReturn(null)
+ }
+ private val resolverListCommunicator = FakeResolverListCommunicator()
+ private val userHandle = UserHandle.of(0)
+ private val targetDataLoader = mock<TargetDataLoader>()
+ private val executor = TestExecutor()
+
+ @Test
+ fun test_oneTargetNoLastChosen_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ }
+
+ @Test
+ fun test_oneTargetThatWasLastChosen_NoTargetsInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(0)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNotNull()
+ assertThat(testSubject.filteredPosition).isEqualTo(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_oneTargetLastChosenNotInTheList_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(createResolveInfo(PKG_NAME_TWO, CLASS_NAME))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_oneTargetThatWasLastChosenFilteringDisabled_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ false,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ // we don't reset placeholder count
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ }
+
+ @Test
+ fun test_twoTargetsNoLastChosenUseLayoutWithDefaults_twoTargetsInAdapter() {
+ testTwoTargets(hasLastChosen = false, useLayoutWithDefaults = true)
+ }
+
+ @Test
+ fun test_twoTargetsNoLastChosenDontUseLayoutWithDefaults_twoTargetsInAdapter() {
+ testTwoTargets(hasLastChosen = false, useLayoutWithDefaults = false)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenUseLayoutWithDefaults_oneTargetInAdapter() {
+ testTwoTargets(hasLastChosen = true, useLayoutWithDefaults = true)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenDontUseLayoutWithDefaults_oneTargetInAdapter() {
+ testTwoTargets(hasLastChosen = true, useLayoutWithDefaults = false)
+ }
+
+ private fun testTwoTargets(hasLastChosen: Boolean, useLayoutWithDefaults: Boolean) {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ if (hasLastChosen) {
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ }
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val resolverListCommunicator = FakeResolverListCommunicator(useLayoutWithDefaults)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - (if (useLayoutWithDefaults) 1 else 0)
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isEqualTo(hasLastChosen)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(executor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(0)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isEqualTo(hasLastChosen)
+ if (hasLastChosen) {
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size - 1)
+ assertThat(testSubject.filteredItem).isNotNull()
+ assertThat(testSubject.filteredPosition).isEqualTo(0)
+ } else {
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ }
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenNotInTheList_twoTargetsInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(resolverListController.lastChosen)
+ .thenReturn(createResolveInfo(PKG_NAME, CLASS_NAME + "2"))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = false
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - 1
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(executor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_twoTargetsWithOtherProfileAndLastChosen_oneTargetInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ resolvedTargets[1].getResolveInfoAt(0).targetUserId = 10
+ whenever(resolvedTargets[1].getResolveInfoAt(0).loadLabel(any())).thenReturn("Label")
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.otherProfile).isNotNull()
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(executor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_resultsSorted_appearInSortedOrderInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.sort(any())).thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ components[0] = components[1].also { components[1] = components[0] }
+ null
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(resolvedTargets[0].getResolveInfoAt(0).activityInfo.packageName)
+ .isEqualTo(PKG_NAME_TWO)
+ assertThat(resolvedTargets[1].getResolveInfoAt(0).activityInfo.packageName)
+ .isEqualTo(PKG_NAME)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_ineligibleActivityFilteredOut_filteredComponentNotPresentInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.filterIneligibleActivities(any(), anyBoolean()))
+ .thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.getItem(0)?.resolveInfo)
+ .isEqualTo(resolvedTargets[0].getResolveInfoAt(0))
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_baseResolveList_excludedFromIneligibleActivityFiltering() {
+ val rList = listOf(createResolveInfo(PKG_NAME, CLASS_NAME))
+ whenever(resolverListController.addResolveListDedupe(any(), eq(targetIntent), eq(rList)))
+ .thenAnswer { invocation ->
+ val result = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ result.addAll(
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ )
+ null
+ }
+ whenever(resolverListController.filterIneligibleActivities(any(), anyBoolean()))
+ .thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.clear()
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ rList,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(2)
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_lowPriorityComponentFilteredOut_filteredComponentNotPresentInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.filterLowPriority(any(), anyBoolean())).thenAnswer {
+ invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ executor,
+ testHandler,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ executor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.getItem(0)?.resolveInfo)
+ .isEqualTo(resolvedTargets[0].getResolveInfoAt(0))
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ private fun createResolvedComponents(
+ vararg components: ComponentName
+ ): List<ResolvedComponentInfo> {
+ val result = ArrayList<ResolvedComponentInfo>(components.size)
+ for (component in components) {
+ val resolvedComponentInfo =
+ ResolvedComponentInfo(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ targetIntent,
+ createResolveInfo(component.packageName, component.className)
+ )
+ result.add(resolvedComponentInfo)
+ }
+ return result
+ }
+
+ private fun createResolveInfo(packageName: String, className: String): ResolveInfo =
+ mock<ResolveInfo> {
+ activityInfo =
+ ActivityInfo().apply {
+ name = className
+ this.packageName = packageName
+ applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
+ }
+ targetUserId = UserHandle.USER_CURRENT
+ }
+}
+
+private class FakeResolverListCommunicator(private val layoutWithDefaults: Boolean = true) :
+ ResolverListAdapter.ResolverListCommunicator {
+ private val sendVoiceCounter = AtomicInteger()
+ private val updateProfileViewButtonCounter = AtomicInteger()
+
+ val sendVoiceCommandCount
+ get() = sendVoiceCounter.get()
+ val updateProfileViewButtonCount
+ get() = updateProfileViewButtonCounter.get()
+
+ override fun getReplacementIntent(activityInfo: ActivityInfo?, defIntent: Intent): Intent {
+ return defIntent
+ }
+
+ override fun onPostListReady(
+ listAdapter: ResolverListAdapter?,
+ updateUi: Boolean,
+ rebuildCompleted: Boolean,
+ ) = Unit
+
+ override fun sendVoiceChoicesIfNeeded() {
+ sendVoiceCounter.incrementAndGet()
+ }
+
+ override fun updateProfileViewButton() {
+ updateProfileViewButtonCounter.incrementAndGet()
+ }
+
+ override fun useLayoutWithDefault(): Boolean = layoutWithDefaults
+
+ override fun shouldGetActivityMetadata(): Boolean = true
+
+ override fun onHandlePackagesChanged(listAdapter: ResolverListAdapter?) {}
+}
diff --git a/java/tests/src/com/android/intentresolver/util/TestExecutor.kt b/java/tests/src/com/android/intentresolver/util/TestExecutor.kt
new file mode 100644
index 00000000..214b9707
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/util/TestExecutor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import java.util.concurrent.Executor
+
+class TestExecutor(private val immediate: Boolean = false) : Executor {
+ private var pendingCommands = ArrayDeque<Runnable>()
+
+ val pendingCommandCount: Int
+ get() = pendingCommands.size
+
+ override fun execute(command: Runnable) {
+ if (immediate) {
+ command.run()
+ } else {
+ pendingCommands.add(command)
+ }
+ }
+
+ fun runUntilIdle() {
+ while (pendingCommands.isNotEmpty()) {
+ pendingCommands.removeFirst().run()
+ }
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/util/TestImmediateHandler.kt b/java/tests/src/com/android/intentresolver/util/TestImmediateHandler.kt
new file mode 100644
index 00000000..9e6fc989
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/util/TestImmediateHandler.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+
+/**
+ * A test Handler that executes posted [Runnable] immediately regardless of the target time (delay).
+ * Does not support messages.
+ */
+class TestImmediateHandler : Handler(createTestLooper()) {
+ override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean {
+ msg.callback.run()
+ return true
+ }
+
+ companion object {
+ private val looperConstructor by lazy {
+ Looper::class.java.getDeclaredConstructor(java.lang.Boolean.TYPE).apply {
+ isAccessible = true
+ }
+ }
+
+ private fun createTestLooper(): Looper = looperConstructor.newInstance(true)
+ }
+}