summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Govinda Wasserman <gwasserman@google.com> 2023-11-22 09:21:08 -0500
committer Govinda Wasserman <gwasserman@google.com> 2023-11-22 09:24:53 -0500
commitb34ae5d8650a1a4f074db68cb64c3b9648cb9275 (patch)
tree368b28971e081060582ff39ee234421acf417b02
parent55da290431232e15b5a8c175561ea57796b572d5 (diff)
Splits list controller into interfaces
Each interface has a single concern, allowing for list controllers to be built by composition. New list controllers are not currently in use, but will be in a future change once resolver comparators and list adapters get updated. Test: atest com.android.intentresolver.v2.listcontroller BUG: 302113519 Change-Id: Ie1d24571c07d1408aa80f8a86311d0fee5e78255
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java8
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java6
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt70
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt77
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ListController.kt21
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt69
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt121
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt108
-rw-r--r--java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java6
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt61
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt83
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt77
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt499
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt111
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt74
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt125
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt309
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt197
-rw-r--r--java/tests/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt63
22 files changed, 2187 insertions, 10 deletions
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index 131aa8d9..724fa849 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -232,7 +232,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link
* #compare(ResolvedComponentInfo, ResolvedComponentInfo)}
*/
- abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
+ public abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
/**
* Computes features for each target. This will be called before calls to {@link
@@ -248,7 +248,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
/** Implementation of compute called after {@link #beforeCompute()}. */
- abstract void doCompute(List<ResolvedComponentInfo> targets);
+ public abstract void doCompute(List<ResolvedComponentInfo> targets);
/**
* Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
@@ -257,12 +257,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
public abstract float getScore(TargetInfo targetInfo);
/** Handles result message sent to mHandler. */
- abstract void handleResultMessage(Message message);
+ public abstract void handleResultMessage(Message message);
/**
* Reports to UsageStats what was chosen.
*/
- public final void updateChooserCounts(String packageName, UserHandle user, String action) {
+ public void updateChooserCounts(String packageName, UserHandle user, String action) {
if (mUsmMap.containsKey(user)) {
mUsmMap.get(user).reportChooserSelection(
packageName,
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 6f7b54dd..0651d26c 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -87,12 +87,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
}
@Override
- int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
- void doCompute(List<ResolvedComponentInfo> targets) {
+ public void doCompute(List<ResolvedComponentInfo> targets) {
if (targets.isEmpty()) {
mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
return;
@@ -144,7 +144,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
}
@Override
- void handleResultMessage(Message msg) {
+ public void handleResultMessage(Message msg) {
// Null value is okay if we have defaulted to the ResolverRankerService.
if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj;
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
new file mode 100644
index 00000000..5855e2fc
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+
+/** A class that is able to identify components that should be hidden from the user. */
+interface FilterableComponents {
+ /** Whether this component should hidden from the user. */
+ fun isComponentFiltered(name: ComponentName): Boolean
+}
+
+/** A class that never filters components. */
+class NoComponentFiltering : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean = false
+}
+
+/** A class that filters components by chooser request filter. */
+class ChooserRequestFilteredComponents(
+ private val chooserRequestParameters: ChooserRequestParameters,
+) : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean =
+ chooserRequestParameters.filteredComponentNames.contains(name)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
new file mode 100644
index 00000000..bb9394b4
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
@@ -0,0 +1,70 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for translating [Intent]s to [ResolvedComponentInfo]s. */
+interface IntentResolver {
+ /**
+ * Get data about all the ways the user with the specified handle can resolve any of the
+ * provided `intents`.
+ */
+ fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo>
+}
+
+/** Resolves [Intent]s using the [packageManager], deduping using the given [ResolveListDeduper]. */
+class IntentResolverImpl(
+ private val packageManager: PackageManager,
+ resolveListDeduper: ResolveListDeduper,
+) : IntentResolver, ResolveListDeduper by resolveListDeduper {
+ override fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo> {
+ val baseFlags =
+ ((if (shouldGetOnlyDefaultActivities) PackageManager.MATCH_DEFAULT_ONLY else 0) or
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+ (if (shouldGetResolvedFilter) PackageManager.GET_RESOLVED_FILTER else 0) or
+ (if (shouldGetActivityMetadata) PackageManager.GET_META_DATA else 0) or
+ PackageManager.MATCH_CLONE_PROFILE)
+ return getResolversForIntentAsUserInternal(
+ intents,
+ userHandle,
+ baseFlags,
+ )
+ }
+
+ private fun getResolversForIntentAsUserInternal(
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ baseFlags: Int,
+ ): List<ResolvedComponentInfo> = buildList {
+ for (intent in intents) {
+ var flags = baseFlags
+ if (intent.isWebIntent || intent.flags and Intent.FLAG_ACTIVITY_MATCH_EXTERNAL != 0) {
+ flags = flags or PackageManager.MATCH_INSTANT
+ }
+ // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent.
+ val fixedIntent =
+ if (intent.javaClass != Intent::class.java) {
+ Intent(intent)
+ } else {
+ intent
+ }
+ val infos = packageManager.queryIntentActivitiesAsUser(fixedIntent, flags, userHandle)
+ addToResolveListWithDedupe(this, fixedIntent, infos)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
new file mode 100644
index 00000000..b2856526
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.v2.listcontroller
+
+import android.app.AppGlobals
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.IPackageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.RemoteException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class that stores and retrieves the most recently chosen resolutions. */
+interface LastChosenManager {
+
+ /** Returns the most recently chosen resolution. */
+ suspend fun getLastChosen(): ResolveInfo
+
+ /** Sets the most recently chosen resolution. */
+ suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int)
+}
+
+/**
+ * Stores and retrieves the most recently chosen resolutions using the [PackageManager] provided by
+ * the [packageManagerProvider].
+ */
+class PackageManagerLastChosenManager(
+ private val contentResolver: ContentResolver,
+ private val bgDispatcher: CoroutineDispatcher,
+ private val targetIntent: Intent,
+ private val packageManagerProvider: () -> IPackageManager = AppGlobals::getPackageManager,
+) : LastChosenManager {
+
+ @Throws(RemoteException::class)
+ override suspend fun getLastChosen(): ResolveInfo {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .getLastChosenActivity(
+ targetIntent,
+ targetIntent.resolveTypeIfNeeded(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ )
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int) {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .setLastChosenActivity(
+ intent,
+ intent.resolveType(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ filter,
+ match,
+ intent.component,
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
new file mode 100644
index 00000000..4ddab755
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.v2.listcontroller
+
+/** Controller for managing lists of [com.android.intentresolver.ResolvedComponentInfo]s. */
+interface ListController :
+ LastChosenManager, IntentResolver, ResolvedComponentFiltering, ResolvedComponentSorting
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
new file mode 100644
index 00000000..cae2af95
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
@@ -0,0 +1,34 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.app.ActivityManager
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class for checking if a permission has been granted. */
+interface PermissionChecker {
+ /** Checks if the given [permission] has been granted. */
+ suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int
+}
+
+/**
+ * Class for checking if a permission has been granted using the static
+ * [ActivityManager.checkComponentPermission].
+ */
+class ActivityManagerPermissionChecker(
+ private val bgDispatcher: CoroutineDispatcher,
+) : PermissionChecker {
+ override suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int =
+ withContext(bgDispatcher) {
+ ActivityManager.checkComponentPermission(permission, uid, owningUid, exported)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
new file mode 100644
index 00000000..8be45ba2
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+
+/** A class that is able to identify components that should be pinned for the user. */
+interface PinnableComponents {
+ /** Whether this component is pinned by the user. */
+ fun isComponentPinned(name: ComponentName): Boolean
+}
+
+/** A class that never pins components. */
+class NoComponentPinning : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean = false
+}
+
+/** A class that determines pinnable components by user preferences. */
+class SharedPreferencesPinnedComponents(
+ private val pinnedSharedPreferences: SharedPreferences,
+) : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean =
+ pinnedSharedPreferences.getBoolean(name.flattenToString(), false)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
new file mode 100644
index 00000000..f0b4bf3f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
@@ -0,0 +1,69 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without duplicates. */
+interface ResolveListDeduper {
+ /**
+ * Adds [ResolveInfo]s in [from] to [ResolvedComponentInfo]s in [into], creating new
+ * [ResolvedComponentInfo]s when there is not already a corresponding one.
+ *
+ * This method may be destructive to both the given [into] list and the underlying
+ * [ResolvedComponentInfo]s.
+ */
+ fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ )
+}
+
+/**
+ * Default implementation for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without
+ * duplicates. Uses the given [PinnableComponents] to determine the pinning state of newly created
+ * [ResolvedComponentInfo]s.
+ */
+class ResolveListDeduperImpl(pinnableComponents: PinnableComponents) :
+ ResolveListDeduper, PinnableComponents by pinnableComponents {
+ override fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ ) {
+ from.forEach { newInfo ->
+ if (newInfo.userHandle == null) {
+ Log.w(TAG, "Skipping ResolveInfo with no userHandle: $newInfo")
+ return@forEach
+ }
+ val oldInfo = into.firstOrNull { isSameResolvedComponent(newInfo, it) }
+ // If existing resolution found, add to existing and filter out
+ if (oldInfo != null) {
+ oldInfo.add(intent, newInfo)
+ } else {
+ with(newInfo.activityInfo) {
+ into.add(
+ ResolvedComponentInfo(
+ ComponentName(packageName, name),
+ intent,
+ newInfo,
+ )
+ .apply { isPinned = isComponentPinned(name) },
+ )
+ }
+ }
+ }
+ }
+
+ private fun isSameResolvedComponent(a: ResolveInfo, b: ResolvedComponentInfo): Boolean {
+ val ai = a.activityInfo
+ return ai.packageName == b.name.packageName && ai.name == b.name.className
+ }
+
+ companion object {
+ const val TAG = "ResolveListDeduper"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
new file mode 100644
index 00000000..e78bff00
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
@@ -0,0 +1,121 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+/** Provides filtering methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentFiltering {
+ /**
+ * Returns a list with all the [ResolvedComponentInfo] in [inputList], less the ones that are
+ * not eligible.
+ */
+ suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo>
+
+ /** Filter out any low priority items. */
+ fun filterLowPriority(inputList: List<ResolvedComponentInfo>): List<ResolvedComponentInfo>
+}
+
+/**
+ * Default instantiation of the filtering methods for lists of [ResolvedComponentInfo].
+ *
+ * Binder calls are performed on the given [bgDispatcher] and permissions are checked as if launched
+ * from the given [launchedFromUid] UID. Component filtering is handled by the given
+ * [FilterableComponents] and permission checking is handled by the given [PermissionChecker].
+ */
+class ResolvedComponentFilteringImpl(
+ private val launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ permissionChecker: PermissionChecker,
+) :
+ ResolvedComponentFiltering,
+ PermissionChecker by permissionChecker,
+ FilterableComponents by filterableComponents {
+ constructor(
+ bgDispatcher: CoroutineDispatcher,
+ launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ ) : this(
+ launchedFromUid = launchedFromUid,
+ filterableComponents = filterableComponents,
+ permissionChecker = ActivityManagerPermissionChecker(bgDispatcher),
+ )
+
+ /**
+ * Filter out items that are filtered by [FilterableComponents] or do not have the necessary
+ * permissions.
+ */
+ override suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> = coroutineScope {
+ inputList
+ .map {
+ val activityInfo = it.getResolveInfoAt(0).activityInfo
+ if (isComponentFiltered(activityInfo.componentName)) {
+ CompletableDeferred(value = null)
+ } else {
+ // Do all permission checks in parallel
+ async {
+ val granted =
+ checkComponentPermission(
+ activityInfo.permission,
+ launchedFromUid,
+ activityInfo.applicationInfo.uid,
+ activityInfo.exported,
+ ) == PackageManager.PERMISSION_GRANTED
+ if (granted) it else null
+ }
+ }
+ }
+ .awaitAll()
+ .filterNotNull()
+ }
+
+ /**
+ * Filters out all elements starting with the first elements with a different priority or
+ * default status than the first element.
+ */
+ override fun filterLowPriority(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> {
+ val firstResolveInfo = inputList[0].getResolveInfoAt(0)
+ // Only display the first matches that are either of equal
+ // priority or have asked to be default options.
+ val firstDiffIndex =
+ inputList.indexOfFirst { resolvedComponentInfo ->
+ val resolveInfo = resolvedComponentInfo.getResolveInfoAt(0)
+ if (firstResolveInfo == resolveInfo) {
+ false
+ } else {
+ if (DEBUG) {
+ Log.v(
+ TAG,
+ "${firstResolveInfo?.activityInfo?.name}=" +
+ "${firstResolveInfo?.priority}/${firstResolveInfo?.isDefault}" +
+ " vs ${resolveInfo?.activityInfo?.name}=" +
+ "${resolveInfo?.priority}/${resolveInfo?.isDefault}"
+ )
+ }
+ firstResolveInfo!!.priority != resolveInfo!!.priority ||
+ firstResolveInfo.isDefault != resolveInfo.isDefault
+ }
+ }
+ return if (firstDiffIndex == -1) {
+ inputList
+ } else {
+ inputList.subList(0, firstDiffIndex)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentFilter"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
new file mode 100644
index 00000000..8ab41ef0
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
@@ -0,0 +1,108 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.intentresolver.model.AbstractResolverComparator
+import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Provides sorting methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentSorting {
+ /** Returns the a copy of the [inputList] sorted by app share score. */
+ suspend fun sorted(inputList: List<ResolvedComponentInfo>?): List<ResolvedComponentInfo>?
+
+ /** Returns the app share score of the [target]. */
+ fun getScore(target: DisplayResolveInfo): Float
+
+ /** Returns the app share score of the [targetInfo]. */
+ fun getScore(targetInfo: TargetInfo): Float
+
+ /** Updates the model about [targetInfo]. */
+ suspend fun updateModel(targetInfo: TargetInfo)
+
+ /** Updates the model about Activity selection. */
+ suspend fun updateChooserCounts(packageName: String, user: UserHandle, action: String)
+
+ /** Cleans up resources. Nothing should be called after calling this. */
+ fun destroy()
+}
+
+/**
+ * Provides sorting methods using the given [resolverComparator].
+ *
+ * Long calculations and binder calls are performed on the given [bgDispatcher].
+ */
+class ResolvedComponentSortingImpl(
+ private val bgDispatcher: CoroutineDispatcher,
+ private val resolverComparator: AbstractResolverComparator,
+) : ResolvedComponentSorting {
+
+ private val computeComplete = AtomicReference<CompletableDeferred<Unit>?>(null)
+
+ @Throws(InterruptedException::class)
+ private suspend fun computeIfNeeded(inputList: List<ResolvedComponentInfo>) {
+ if (computeComplete.compareAndSet(null, CompletableDeferred())) {
+ resolverComparator.setCallBack { computeComplete.get()!!.complete(Unit) }
+ resolverComparator.compute(inputList)
+ }
+ with(computeComplete.get()!!) { if (isCompleted) return else return await() }
+ }
+
+ override suspend fun sorted(
+ inputList: List<ResolvedComponentInfo>?,
+ ): List<ResolvedComponentInfo>? {
+ if (inputList.isNullOrEmpty()) return inputList
+
+ return withContext(bgDispatcher) {
+ try {
+ val beforeRank = System.currentTimeMillis()
+ computeIfNeeded(inputList)
+ val sorted = inputList.sortedWith(resolverComparator)
+ val afterRank = System.currentTimeMillis()
+ if (DEBUG) {
+ Log.d(TAG, "Time Cost: ${afterRank - beforeRank}")
+ }
+ sorted
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Compute & Sort was interrupted: $e")
+ null
+ }
+ }
+ }
+
+ override fun getScore(target: DisplayResolveInfo): Float {
+ return resolverComparator.getScore(target)
+ }
+
+ override fun getScore(targetInfo: TargetInfo): Float {
+ return resolverComparator.getScore(targetInfo)
+ }
+
+ override suspend fun updateModel(targetInfo: TargetInfo) {
+ withContext(bgDispatcher) { resolverComparator.updateModel(targetInfo) }
+ }
+
+ override suspend fun updateChooserCounts(
+ packageName: String,
+ user: UserHandle,
+ action: String,
+ ) {
+ withContext(bgDispatcher) {
+ resolverComparator.updateChooserCounts(packageName, user, action)
+ }
+ }
+
+ override fun destroy() {
+ resolverComparator.destroy()
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentSort"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
index 5f0ead7b..2140a67d 100644
--- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
+++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
@@ -118,14 +118,14 @@ public class AbstractResolverComparatorTest {
Lists.newArrayList(context.getUser()), promoteToFirst) {
@Override
- int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
// Used for testing pinning, so we should never get here --- the overrides
// should determine the result instead.
return 1;
}
@Override
- void doCompute(List<ResolvedComponentInfo> targets) {}
+ public void doCompute(List<ResolvedComponentInfo> targets) {}
@Override
public float getScore(TargetInfo targetInfo) {
@@ -133,7 +133,7 @@ public class AbstractResolverComparatorTest {
}
@Override
- void handleResultMessage(Message message) {}
+ public void handleResultMessage(Message message) {}
};
return testComparator;
}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt
new file mode 100644
index 00000000..59494bed
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class ChooserRequestFilteredComponentsTest {
+
+ @Mock lateinit var mockChooserRequestParameters: ChooserRequestParameters
+
+ private lateinit var chooserRequestFilteredComponents: ChooserRequestFilteredComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ chooserRequestFilteredComponents =
+ ChooserRequestFilteredComponents(mockChooserRequestParameters)
+ }
+
+ @Test
+ fun isComponentFiltered_returnsRequestParametersFilteredState() {
+ // Arrange
+ whenever(mockChooserRequestParameters.filteredComponentNames)
+ .thenReturn(
+ ImmutableList.of(ComponentName("FilteredPackage", "FilteredClass")),
+ )
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val filteredComponent = ComponentName("FilteredPackage", "FilteredClass")
+
+ // Act
+ val result = chooserRequestFilteredComponents.isComponentFiltered(testComponent)
+ val filteredResult = chooserRequestFilteredComponents.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(result).isFalse()
+ assertThat(filteredResult).isTrue()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt
new file mode 100644
index 00000000..ce40567e
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Message
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.intentresolver.model.AbstractResolverComparator
+import com.android.intentresolver.whenever
+import java.util.Locale
+import org.mockito.Mockito
+
+class FakeResolverComparator(
+ context: Context =
+ Mockito.mock(Context::class.java).also {
+ val mockResources = Mockito.mock(Resources::class.java)
+ whenever(it.resources).thenReturn(mockResources)
+ whenever(mockResources.configuration)
+ .thenReturn(Configuration().apply { setLocale(Locale.US) })
+ },
+ targetIntent: Intent = Intent("TestAction"),
+ resolvedActivityUserSpaceList: List<UserHandle> = emptyList(),
+ promoteToFirst: ComponentName? = null,
+) :
+ AbstractResolverComparator(
+ context,
+ targetIntent,
+ resolvedActivityUserSpaceList,
+ promoteToFirst,
+ ) {
+ var lastUpdateModel: TargetInfo? = null
+ private set
+ var lastUpdateChooserCounts: Triple<String, UserHandle, String>? = null
+ private set
+ var destroyCalled = false
+ private set
+
+ override fun compare(lhs: ResolveInfo?, rhs: ResolveInfo?): Int =
+ lhs!!.activityInfo.packageName.compareTo(rhs!!.activityInfo.packageName)
+
+ override fun doCompute(targets: MutableList<ResolvedComponentInfo>?) {}
+
+ override fun getScore(targetInfo: TargetInfo?): Float = 1.23f
+
+ override fun handleResultMessage(message: Message?) {}
+
+ override fun updateModel(targetInfo: TargetInfo?) {
+ lastUpdateModel = targetInfo
+ }
+
+ override fun updateChooserCounts(
+ packageName: String,
+ user: UserHandle,
+ action: String,
+ ) {
+ lastUpdateChooserCounts = Triple(packageName, user, action)
+ }
+
+ override fun destroy() {
+ destroyCalled = true
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt
new file mode 100644
index 00000000..396505e6
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class FilterableComponentsTest {
+
+ @Mock lateinit var mockChooserRequestParameters: ChooserRequestParameters
+
+ private val unfilteredComponent = ComponentName("TestPackage", "TestClass")
+ private val filteredComponent = ComponentName("FilteredPackage", "FilteredClass")
+ private val noComponentFiltering = NoComponentFiltering()
+
+ private lateinit var chooserRequestFilteredComponents: ChooserRequestFilteredComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ chooserRequestFilteredComponents =
+ ChooserRequestFilteredComponents(mockChooserRequestParameters)
+ }
+
+ @Test
+ fun isComponentFiltered_noComponentFiltering_neverFilters() {
+ // Arrange
+
+ // Act
+ val unfilteredResult = noComponentFiltering.isComponentFiltered(unfilteredComponent)
+ val filteredResult = noComponentFiltering.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(unfilteredResult).isFalse()
+ assertThat(filteredResult).isFalse()
+ }
+
+ @Test
+ fun isComponentFiltered_chooserRequestFilteredComponents_filtersAccordingToChooserRequest() {
+ // Arrange
+ whenever(mockChooserRequestParameters.filteredComponentNames)
+ .thenReturn(
+ ImmutableList.of(filteredComponent),
+ )
+
+ // Act
+ val unfilteredResult =
+ chooserRequestFilteredComponents.isComponentFiltered(unfilteredComponent)
+ val filteredResult = chooserRequestFilteredComponents.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(unfilteredResult).isFalse()
+ assertThat(filteredResult).isTrue()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt
new file mode 100644
index 00000000..09f6d373
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt
@@ -0,0 +1,499 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.os.UserHandle
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.kotlinArgumentCaptor
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import java.lang.IndexOutOfBoundsException
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+class IntentResolverTest {
+
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ private lateinit var intentResolver: IntentResolver
+
+ private val fakePinnableComponents =
+ object : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean {
+ return name.packageName == "PinnedPackage"
+ }
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ intentResolver =
+ IntentResolverImpl(mockPackageManager, ResolveListDeduperImpl(fakePinnableComponents))
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_noIntents_returnsEmptyList() {
+ // Arrange
+ val testIntents = emptyList<Intent>()
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_noResolveInfo_returnsEmptyList() {
+ // Arrange
+ val testIntents = listOf(Intent("TestAction"))
+ val testResolveInfos = emptyList<ResolveInfo>()
+ whenever(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), any<UserHandle>()))
+ .thenReturn(testResolveInfos)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_returnsAllResolveComponentInfo() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ },
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage3"
+ activityInfo.name = "TestClass3"
+ },
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage4"
+ activityInfo.name = "TestClass4"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ result.forEachIndexed { index, it ->
+ val postfix = index + 1
+ assertThat(it.name.packageName).isEqualTo("TestPackage$postfix")
+ assertThat(it.name.className).isEqualTo("TestClass$postfix")
+ assertThrows(IndexOutOfBoundsException::class.java) { it.getIntentAt(1) }
+ }
+ assertThat(result.map { it.getIntentAt(0) })
+ .containsExactly(
+ testIntent1,
+ testIntent1,
+ testIntent2,
+ testIntent2,
+ )
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_resolveInfoWithoutUserHandle_isSkipped() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testIntents = listOf(testIntent)
+ val testResolveInfos =
+ listOf(
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage"
+ activityInfo.name = "TestClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ any(),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_duplicateComponents_areCombined() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).hasSize(1)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("DuplicatePackage")
+ assertThat(name.className).isEqualTo("DuplicateClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent1)
+ assertThat(getIntentAt(1)).isEqualTo(testIntent2)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(2) }
+ }
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_pinnedComponentsArePinned() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "UnpinnedPackage"
+ activityInfo.name = "UnpinnedClass"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "PinnedPackage"
+ activityInfo.name = "PinnedClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result.map { it.isPinned }).containsExactly(false, true)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenNoExtraBehavior_usesBaseFlags() {
+ // Arrange
+ val baseFlags =
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+ PackageManager.MATCH_CLONE_PROFILE
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value).isEqualTo(baseFlags)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetResolvedFilter_usesGetResolvedFilterFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = true,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.GET_RESOLVED_FILTER)
+ .isEqualTo(PackageManager.GET_RESOLVED_FILTER)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetActivityMetadata_usesGetMetaDataFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = true,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.GET_META_DATA)
+ .isEqualTo(PackageManager.GET_META_DATA)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetOnlyDefaultActivities_usesMatchDefaultOnlyFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = true,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_DEFAULT_ONLY)
+ .isEqualTo(PackageManager.MATCH_DEFAULT_ONLY)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenWebIntent_usesMatchInstantFlag() {
+ // Arrange
+ val testIntent = Intent(Intent.ACTION_VIEW, Uri.fromParts(IntentFilter.SCHEME_HTTP, "", ""))
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_INSTANT)
+ .isEqualTo(PackageManager.MATCH_INSTANT)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenActivityMatchExternalFlag_usesMatchInstantFlag() {
+ // Arrange
+ val testIntent = Intent().addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_INSTANT)
+ .isEqualTo(PackageManager.MATCH_INSTANT)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt
new file mode 100644
index 00000000..ce5e52b1
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.IPackageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.nullable
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class LastChosenManagerTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val testTargetIntent = Intent("TestAction")
+
+ @Mock lateinit var mockContentResolver: ContentResolver
+ @Mock lateinit var mockIPackageManager: IPackageManager
+
+ private lateinit var lastChosenManager: LastChosenManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ lastChosenManager =
+ PackageManagerLastChosenManager(mockContentResolver, testDispatcher, testTargetIntent) {
+ mockIPackageManager
+ }
+ }
+
+ @Test
+ fun getLastChosen_returnsLastChosenActivity() =
+ testScope.runTest {
+ // Arrange
+ val testResolveInfo = ResolveInfo()
+ whenever(mockIPackageManager.getLastChosenActivity(any(), nullable(), any()))
+ .thenReturn(testResolveInfo)
+
+ // Act
+ val lastChosen = lastChosenManager.getLastChosen()
+ runCurrent()
+
+ // Assert
+ verify(mockIPackageManager)
+ .getLastChosenActivity(
+ eq(testTargetIntent),
+ isNull(),
+ eq(PackageManager.MATCH_DEFAULT_ONLY),
+ )
+ assertThat(lastChosen).isSameInstanceAs(testResolveInfo)
+ }
+
+ @Test
+ fun setLastChosen_setsLastChosenActivity() =
+ testScope.runTest {
+ // Arrange
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val testIntent = Intent().apply { component = testComponent }
+ val testIntentFilter = IntentFilter()
+ val testMatch = 456
+
+ // Act
+ lastChosenManager.setLastChosen(testIntent, testIntentFilter, testMatch)
+ runCurrent()
+
+ // Assert
+ verify(mockIPackageManager)
+ .setLastChosenActivity(
+ eq(testIntent),
+ isNull(),
+ eq(PackageManager.MATCH_DEFAULT_ONLY),
+ eq(testIntentFilter),
+ eq(testMatch),
+ eq(testComponent),
+ )
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt
new file mode 100644
index 00000000..112342ad
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class PinnableComponentsTest {
+
+ @Mock lateinit var mockSharedPreferences: SharedPreferences
+
+ private val unpinnedComponent = ComponentName("TestPackage", "TestClass")
+ private val pinnedComponent = ComponentName("PinnedPackage", "PinnedClass")
+ private val noComponentPinning = NoComponentPinning()
+
+ private lateinit var sharedPreferencesPinnedComponents: PinnableComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ sharedPreferencesPinnedComponents = SharedPreferencesPinnedComponents(mockSharedPreferences)
+ }
+
+ @Test
+ fun isComponentPinned_noComponentPinning_neverPins() {
+ // Arrange
+
+ // Act
+ val unpinnedResult = noComponentPinning.isComponentPinned(unpinnedComponent)
+ val pinnedResult = noComponentPinning.isComponentPinned(pinnedComponent)
+
+ // Assert
+ assertThat(unpinnedResult).isFalse()
+ assertThat(pinnedResult).isFalse()
+ }
+
+ @Test
+ fun isComponentFiltered_chooserRequestFilteredComponents_filtersAccordingToChooserRequest() {
+ // Arrange
+ whenever(mockSharedPreferences.getBoolean(eq(pinnedComponent.flattenToString()), any()))
+ .thenReturn(true)
+
+ // Act
+ val unpinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(unpinnedComponent)
+ val pinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(pinnedComponent)
+
+ // Assert
+ assertThat(unpinnedResult).isFalse()
+ assertThat(pinnedResult).isTrue()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt
new file mode 100644
index 00000000..26f0199e
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.google.common.truth.Truth.assertThat
+import java.lang.IndexOutOfBoundsException
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+
+class ResolveListDeduperTest {
+
+ private lateinit var resolveListDeduper: ResolveListDeduper
+
+ @Before
+ fun setup() {
+ resolveListDeduper = ResolveListDeduperImpl(NoComponentPinning())
+ }
+
+ @Test
+ fun addResolveListDedupe_addsDifferentComponents() {
+ // Arrange
+ val testIntent = Intent()
+ val testResolveInfo1 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ }
+ val testResolveInfo2 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ }
+ val testResolvedComponentInfo1 =
+ ResolvedComponentInfo(
+ ComponentName("TestPackage1", "TestClass1"),
+ testIntent,
+ testResolveInfo1,
+ )
+ .apply { isPinned = false }
+ val listUnderTest = mutableListOf(testResolvedComponentInfo1)
+ val listToAdd = listOf(testResolveInfo2)
+
+ // Act
+ resolveListDeduper.addToResolveListWithDedupe(
+ into = listUnderTest,
+ intent = testIntent,
+ from = listToAdd,
+ )
+
+ // Assert
+ listUnderTest.forEachIndexed { index, it ->
+ val postfix = index + 1
+ assertThat(it.name.packageName).isEqualTo("TestPackage$postfix")
+ assertThat(it.name.className).isEqualTo("TestClass$postfix")
+ assertThat(it.getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { it.getIntentAt(1) }
+ }
+ }
+
+ @Test
+ fun addResolveListDedupe_combinesDuplicateComponents() {
+ // Arrange
+ val testIntent = Intent()
+ val testResolveInfo1 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ }
+ val testResolveInfo2 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ }
+ val testResolvedComponentInfo1 =
+ ResolvedComponentInfo(
+ ComponentName("DuplicatePackage", "DuplicateClass"),
+ testIntent,
+ testResolveInfo1,
+ )
+ .apply { isPinned = false }
+ val listUnderTest = mutableListOf(testResolvedComponentInfo1)
+ val listToAdd = listOf(testResolveInfo2)
+
+ // Act
+ resolveListDeduper.addToResolveListWithDedupe(
+ into = listUnderTest,
+ intent = testIntent,
+ from = listToAdd,
+ )
+
+ // Assert
+ assertThat(listUnderTest).containsExactly(testResolvedComponentInfo1)
+ assertThat(testResolvedComponentInfo1.getResolveInfoAt(0)).isEqualTo(testResolveInfo1)
+ assertThat(testResolvedComponentInfo1.getResolveInfoAt(1)).isEqualTo(testResolveInfo2)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt
new file mode 100644
index 00000000..9786b801
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt
@@ -0,0 +1,309 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import com.android.intentresolver.ResolvedComponentInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+
+class ResolvedComponentFilteringTest {
+
+ private lateinit var resolvedComponentFiltering: ResolvedComponentFiltering
+
+ private val fakeFilterableComponents =
+ object : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean {
+ return name.packageName == "FilteredPackage"
+ }
+ }
+
+ private val fakePermissionChecker =
+ object : PermissionChecker {
+ override suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean
+ ): Int {
+ return if (permission == "MissingPermission") {
+ PackageManager.PERMISSION_DENIED
+ } else {
+ PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+
+ @Before
+ fun setup() {
+ resolvedComponentFiltering =
+ ResolvedComponentFilteringImpl(
+ launchedFromUid = 123,
+ filterableComponents = fakeFilterableComponents,
+ permissionChecker = fakePermissionChecker,
+ )
+ }
+
+ @Test
+ fun filterIneligibleActivities_returnsListWithoutFilteredComponents() = runTest {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage"
+ activityInfo.name = "TestClass"
+ activityInfo.permission = "TestPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val filteredResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "FilteredPackage"
+ activityInfo.name = "FilteredClass"
+ activityInfo.permission = "TestPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val missingPermissionResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "NoPermissionPackage"
+ activityInfo.name = "NoPermissionClass"
+ activityInfo.permission = "MissingPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("FilteredPackage", "FilteredClass"),
+ testIntent,
+ filteredResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("NoPermissionPackage", "NoPermissionClass"),
+ testIntent,
+ missingPermissionResolveInfo,
+ )
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterIneligibleActivities(testInput)
+
+ // Assert
+ assertThat(result).hasSize(1)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_filtersAfterFirstDifferentPriority() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val diffResolveInfo =
+ ResolveInfo().apply {
+ priority = 2
+ isDefault = true
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("DiffPackage", "DiffClass"),
+ testIntent,
+ diffResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_filtersAfterFirstDifferentDefault() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val diffResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = false
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("DiffPackage", "DiffClass"),
+ testIntent,
+ diffResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_whenNoDifference_returnsOriginal() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt
new file mode 100644
index 00000000..39b328ee
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ResolvedComponentSortingTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val fakeResolverComparator = FakeResolverComparator()
+
+ private val resolvedComponentSorting =
+ ResolvedComponentSortingImpl(testDispatcher, fakeResolverComparator)
+
+ @Test
+ fun sorted_onNullList_returnsNull() =
+ testScope.runTest {
+ // Arrange
+ val testInput: List<ResolvedComponentInfo>? = null
+
+ // Act
+ val result = resolvedComponentSorting.sorted(testInput)
+ runCurrent()
+
+ // Assert
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun sorted_onEmptyList_returnsEmptyList() =
+ testScope.runTest {
+ // Arrange
+ val testInput = emptyList<ResolvedComponentInfo>()
+
+ // Act
+ val result = resolvedComponentSorting.sorted(testInput)
+ runCurrent()
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun sorted_returnsListSortedByGivenComparator() =
+ testScope.runTest {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testInput =
+ listOf(
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage3"
+ activityInfo.name = "TestClass3"
+ },
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ },
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ },
+ )
+ .map {
+ it.targetUserId = UserHandle.USER_CURRENT
+ ResolvedComponentInfo(
+ ComponentName(it.activityInfo.packageName, it.activityInfo.name),
+ testIntent,
+ it,
+ )
+ }
+
+ // Act
+ val result = async { resolvedComponentSorting.sorted(testInput) }
+ runCurrent()
+
+ // Assert
+ assertThat(result.await()?.map { it.name.packageName })
+ .containsExactly("TestPackage1", "TestPackage2", "TestPackage3")
+ .inOrder()
+ }
+
+ @Test
+ fun getScore_displayResolveInfo_returnsTheScoreAccordingToTheResolverComparator() {
+ // Arrange
+ val testTarget =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ Intent(),
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.name = "TestClass"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.packageName = "TestPackage"
+ },
+ Intent(),
+ )
+
+ // Act
+ val result = resolvedComponentSorting.getScore(testTarget)
+
+ // Assert
+ assertThat(result).isEqualTo(1.23f)
+ }
+
+ @Test
+ fun getScore_targetInfo_returnsTheScoreAccordingToTheResolverComparator() {
+ // Arrange
+ val mockTargetInfo = Mockito.mock(TargetInfo::class.java)
+
+ // Act
+ val result = resolvedComponentSorting.getScore(mockTargetInfo)
+
+ // Assert
+ assertThat(result).isEqualTo(1.23f)
+ }
+
+ @Test
+ fun updateModel_updatesResolverComparatorModel() =
+ testScope.runTest {
+ // Arrange
+ val mockTargetInfo = Mockito.mock(TargetInfo::class.java)
+ assertThat(fakeResolverComparator.lastUpdateModel).isNull()
+
+ // Act
+ resolvedComponentSorting.updateModel(mockTargetInfo)
+ runCurrent()
+
+ // Assert
+ assertThat(fakeResolverComparator.lastUpdateModel).isSameInstanceAs(mockTargetInfo)
+ }
+
+ @Test
+ fun updateChooserCounts_updatesResolverComparaterChooserCounts() =
+ testScope.runTest {
+ // Arrange
+ val testPackageName = "TestPackage"
+ val testUser = UserHandle(456)
+ val testAction = "TestAction"
+ assertThat(fakeResolverComparator.lastUpdateChooserCounts).isNull()
+
+ // Act
+ resolvedComponentSorting.updateChooserCounts(testPackageName, testUser, testAction)
+ runCurrent()
+
+ // Assert
+ assertThat(fakeResolverComparator.lastUpdateChooserCounts)
+ .isEqualTo(Triple(testPackageName, testUser, testAction))
+ }
+
+ @Test
+ fun destroy_destroysResolverComparator() {
+ // Arrange
+ assertThat(fakeResolverComparator.destroyCalled).isFalse()
+
+ // Act
+ resolvedComponentSorting.destroy()
+
+ // Assert
+ assertThat(fakeResolverComparator.destroyCalled).isTrue()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt b/java/tests/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt
new file mode 100644
index 00000000..9d6394fa
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class SharedPreferencesPinnedComponentsTest {
+
+ @Mock lateinit var mockSharedPreferences: SharedPreferences
+
+ private lateinit var sharedPreferencesPinnedComponents: SharedPreferencesPinnedComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ sharedPreferencesPinnedComponents = SharedPreferencesPinnedComponents(mockSharedPreferences)
+ }
+
+ @Test
+ fun isComponentPinned_returnsSavedPinnedState() {
+ // Arrange
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val pinnedComponent = ComponentName("PinnedPackage", "PinnedClass")
+ whenever(mockSharedPreferences.getBoolean(eq(pinnedComponent.flattenToString()), any()))
+ .thenReturn(true)
+
+ // Act
+ val result = sharedPreferencesPinnedComponents.isComponentPinned(testComponent)
+ val pinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(pinnedComponent)
+
+ // Assert
+ Mockito.verify(mockSharedPreferences).getBoolean(eq(testComponent.flattenToString()), any())
+ Mockito.verify(mockSharedPreferences)
+ .getBoolean(eq(pinnedComponent.flattenToString()), any())
+ Truth.assertThat(result).isFalse()
+ Truth.assertThat(pinnedResult).isTrue()
+ }
+}