summaryrefslogtreecommitdiff
path: root/java/src/com
diff options
context:
space:
mode:
author Govinda Wasserman <gwasserman@google.com> 2023-11-27 18:31:51 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-11-27 18:31:51 +0000
commitc1c24280a0dfe39745704398ce8d13cf14404eb2 (patch)
tree1c03856622bdccb0bad98f91847ec34245972f59 /java/src/com
parentd43875a34e203f2ee25abfe7dba1f98d5a980d83 (diff)
parentb34ae5d8650a1a4f074db68cb64c3b9648cb9275 (diff)
Merge "Splits list controller into interfaces" into main
Diffstat (limited to 'java/src/com')
-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
11 files changed, 585 insertions, 7 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
+ }
+}