diff options
author | 2023-11-22 09:21:08 -0500 | |
---|---|---|
committer | 2023-11-22 09:24:53 -0500 | |
commit | b34ae5d8650a1a4f074db68cb64c3b9648cb9275 (patch) | |
tree | 368b28971e081060582ff39ee234421acf417b02 | |
parent | 55da290431232e15b5a8c175561ea57796b572d5 (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
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() + } +} |