diff options
| author | 2022-12-01 18:11:25 -0800 | |
|---|---|---|
| committer | 2023-01-22 20:31:04 -0800 | |
| commit | afe103dfcbe593a79e2c5f9be5707011b23a6ace (patch) | |
| tree | ff1e718b2ab56e341298da0b35e50434bb6e7f6e /java | |
| parent | 4d9e604383917238f1674d858630ba420c6e0a29 (diff) | |
Convert ShortcutLoader to Kotlin
A preparation step to update ShortcutLoader logic and base it on
coroutines.
Utializing Gerrit modification tracking specific.
Patchsets:
1: Automatic conversion of ShortcutLoader from java to Kotlin; no other
modifications.
2. Fix compile errors and unit tests.
3. Polish auto-converted Kotlin code plus somall refactoring:
- some code snippets extracted into methods;
- move shortcuts filtering by app status close tho the shortcut
source.
Test: namual testing
Test: atest IntentResolverUnitTests
Change-Id: Ic0c265b41faba774964cc17c2912c4282f0a3c3a
Diffstat (limited to 'java')
4 files changed, 365 insertions, 460 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 6a94d56c..dce8bde1 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1871,21 +1871,20 @@ public class ChooserActivity extends ResolverActivity implements } @MainThread - private void onShortcutsLoaded( - UserHandle userHandle, ShortcutLoader.Result shortcutsResult) { + private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) { if (DEBUG) { Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); } - mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache); - mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache); + mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache()); + mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache()); ChooserListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); if (adapter != null) { - for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) { + for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) { adapter.addServiceResults( - resultInfo.appTarget, - resultInfo.shortcuts, - shortcutsResult.isFromAppPredictor + resultInfo.getAppTarget(), + resultInfo.getShortcuts(), + result.isFromAppPredictor() ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, mDirectShareShortcutInfoCache, diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java deleted file mode 100644 index 1cfa2c8d..00000000 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright (C) 2022 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.shortcuts; - -import android.app.ActivityManager; -import android.app.prediction.AppPredictor; -import android.app.prediction.AppTarget; -import android.content.ComponentName; -import android.content.Context; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.ApplicationInfoFlags; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.os.AsyncTask; -import android.os.UserHandle; -import android.os.UserManager; -import android.service.chooser.ChooserTarget; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; - -import com.android.intentresolver.chooser.DisplayResolveInfo; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -/** - * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. - * <p> - * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut - * updates. The shortcut loading is triggered by the {@link #queryShortcuts(DisplayResolveInfo[])}, - * the processing will happen on the {@link #mBackgroundExecutor} and the result is delivered - * through the {@link #mCallback} on the {@link #mCallbackExecutor}, the main thread. - * </p> - * <p> - * The current version does not improve on the legacy in a way that it does not guarantee that - * each invocation of the {@link #queryShortcuts(DisplayResolveInfo[])} will be matched by an - * invocation of the callback (there are early terminations of the flow). Also, the fetched - * shortcuts would be matched against the last known input, i.e. two invocations of - * {@link #queryShortcuts(DisplayResolveInfo[])} may result in two callbacks where shortcuts are - * processed against the latest input. - * </p> - */ -public class ShortcutLoader { - private static final String TAG = "ChooserActivity"; - - private static final Request NO_REQUEST = new Request(new DisplayResolveInfo[0]); - - private final Context mContext; - @Nullable - private final AppPredictorProxy mAppPredictor; - private final UserHandle mUserHandle; - @Nullable - private final IntentFilter mTargetIntentFilter; - private final Executor mBackgroundExecutor; - private final Executor mCallbackExecutor; - private final boolean mIsPersonalProfile; - private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter = - new ShortcutToChooserTargetConverter(); - private final UserManager mUserManager; - private final AtomicReference<Consumer<Result>> mCallback = new AtomicReference<>(); - private final AtomicReference<Request> mActiveRequest = new AtomicReference<>(NO_REQUEST); - - @Nullable - private final AppPredictor.Callback mAppPredictorCallback; - - @MainThread - public ShortcutLoader( - Context context, - @Nullable AppPredictor appPredictor, - UserHandle userHandle, - @Nullable IntentFilter targetIntentFilter, - Consumer<Result> callback) { - this( - context, - appPredictor == null ? null : new AppPredictorProxy(appPredictor), - userHandle, - userHandle.equals(UserHandle.of(ActivityManager.getCurrentUser())), - targetIntentFilter, - AsyncTask.SERIAL_EXECUTOR, - context.getMainExecutor(), - callback); - } - - @VisibleForTesting - ShortcutLoader( - Context context, - @Nullable AppPredictorProxy appPredictor, - UserHandle userHandle, - boolean isPersonalProfile, - @Nullable IntentFilter targetIntentFilter, - Executor backgroundExecutor, - Executor callbackExecutor, - Consumer<Result> callback) { - mContext = context; - mAppPredictor = appPredictor; - mUserHandle = userHandle; - mTargetIntentFilter = targetIntentFilter; - mBackgroundExecutor = backgroundExecutor; - mCallbackExecutor = callbackExecutor; - mCallback.set(callback); - mIsPersonalProfile = isPersonalProfile; - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - - if (mAppPredictor != null) { - mAppPredictorCallback = createAppPredictorCallback(); - mAppPredictor.registerPredictionUpdates(mCallbackExecutor, mAppPredictorCallback); - } else { - mAppPredictorCallback = null; - } - } - - /** - * Unsubscribe from app predictor if one was provided. - */ - @MainThread - public void destroy() { - if (mCallback.getAndSet(null) != null) { - if (mAppPredictor != null) { - mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback); - } - } - } - - private boolean isDestroyed() { - return mCallback.get() == null; - } - - /** - * Set new resolved targets. This will trigger shortcut loading. - * @param appTargets a collection of application targets a loaded set of shortcuts will be - * grouped against - */ - @MainThread - public void queryShortcuts(DisplayResolveInfo[] appTargets) { - if (isDestroyed()) { - return; - } - mActiveRequest.set(new Request(appTargets)); - mBackgroundExecutor.execute(this::loadShortcuts); - } - - @WorkerThread - private void loadShortcuts() { - // no need to query direct share for work profile when its locked or disabled - if (!shouldQueryDirectShareTargets()) { - return; - } - Log.d(TAG, "querying direct share targets"); - queryDirectShareTargets(false); - } - - @WorkerThread - private void queryDirectShareTargets(boolean skipAppPredictionService) { - if (isDestroyed()) { - return; - } - if (!skipAppPredictionService && mAppPredictor != null) { - mAppPredictor.requestPredictionUpdate(); - return; - } - // Default to just querying ShortcutManager if AppPredictor not present. - if (mTargetIntentFilter == null) { - return; - } - - Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); - ShortcutManager sm = (ShortcutManager) selectedProfileContext - .getSystemService(Context.SHORTCUT_SERVICE); - List<ShortcutManager.ShareShortcutInfo> shortcuts = - sm.getShareTargets(mTargetIntentFilter); - sendShareShortcutInfoList(shortcuts, false, null); - } - - private AppPredictor.Callback createAppPredictorCallback() { - return appPredictorTargets -> { - if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { - // APS may be disabled, so try querying targets ourselves. - queryDirectShareTargets(true); - return; - } - - final List<ShortcutManager.ShareShortcutInfo> shortcuts = new ArrayList<>(); - List<AppTarget> shortcutResults = new ArrayList<>(); - for (AppTarget appTarget : appPredictorTargets) { - if (appTarget.getShortcutInfo() == null) { - continue; - } - shortcutResults.add(appTarget); - } - appPredictorTargets = shortcutResults; - for (AppTarget appTarget : appPredictorTargets) { - shortcuts.add(new ShortcutManager.ShareShortcutInfo( - appTarget.getShortcutInfo(), - new ComponentName(appTarget.getPackageName(), appTarget.getClassName()))); - } - sendShareShortcutInfoList(shortcuts, true, appPredictorTargets); - }; - } - - @WorkerThread - private void sendShareShortcutInfoList( - List<ShortcutManager.ShareShortcutInfo> shortcuts, - boolean isFromAppPredictor, - @Nullable List<AppTarget> appPredictorTargets) { - if (appPredictorTargets != null && appPredictorTargets.size() != shortcuts.size()) { - throw new RuntimeException("resultList and appTargets must have the same size." - + " resultList.size()=" + shortcuts.size() - + " appTargets.size()=" + appPredictorTargets.size()); - } - Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); - for (int i = shortcuts.size() - 1; i >= 0; i--) { - final String packageName = shortcuts.get(i).getTargetComponent().getPackageName(); - if (!isPackageEnabled(selectedProfileContext, packageName)) { - shortcuts.remove(i); - if (appPredictorTargets != null) { - appPredictorTargets.remove(i); - } - } - } - - HashMap<ChooserTarget, AppTarget> directShareAppTargetCache = new HashMap<>(); - HashMap<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache = new HashMap<>(); - // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path - // for direct share targets. After ShareSheet is refactored we should use the - // ShareShortcutInfos directly. - final DisplayResolveInfo[] appTargets = mActiveRequest.get().appTargets; - List<ShortcutResultInfo> resultRecords = new ArrayList<>(); - for (DisplayResolveInfo displayResolveInfo : appTargets) { - List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = - filterShortcutsByTargetComponentName( - shortcuts, displayResolveInfo.getResolvedComponentName()); - if (matchingShortcuts.isEmpty()) { - continue; - } - - List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter - .convertToChooserTarget( - matchingShortcuts, - shortcuts, - appPredictorTargets, - directShareAppTargetCache, - directShareShortcutInfoCache); - - ShortcutResultInfo resultRecord = - new ShortcutResultInfo(displayResolveInfo, chooserTargets); - resultRecords.add(resultRecord); - } - - postReport( - new Result( - isFromAppPredictor, - appTargets, - resultRecords.toArray(new ShortcutResultInfo[0]), - directShareAppTargetCache, - directShareShortcutInfoCache)); - } - - private void postReport(Result result) { - mCallbackExecutor.execute(() -> report(result)); - } - - @MainThread - private void report(Result result) { - Consumer<Result> callback = mCallback.get(); - if (callback != null) { - callback.accept(result); - } - } - - /** - * Returns {@code false} if {@code userHandle} is the work profile and it's either - * in quiet mode or not running. - */ - private boolean shouldQueryDirectShareTargets() { - return mIsPersonalProfile || isProfileActive(); - } - - @VisibleForTesting - protected boolean isProfileActive() { - return mUserManager.isUserRunning(mUserHandle) - && mUserManager.isUserUnlocked(mUserHandle) - && !mUserManager.isQuietModeEnabled(mUserHandle); - } - - private static boolean isPackageEnabled(Context context, String packageName) { - if (TextUtils.isEmpty(packageName)) { - return false; - } - ApplicationInfo appInfo; - try { - appInfo = context.getPackageManager().getApplicationInfo( - packageName, - ApplicationInfoFlags.of(PackageManager.GET_META_DATA)); - } catch (NameNotFoundException e) { - return false; - } - - return appInfo != null && appInfo.enabled - && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0; - } - - private static List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName( - List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) { - List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>(); - for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { - if (requiredTarget.equals(shortcut.getTargetComponent())) { - matchingShortcuts.add(shortcut); - } - } - return matchingShortcuts; - } - - private static class Request { - public final DisplayResolveInfo[] appTargets; - - Request(DisplayResolveInfo[] targets) { - appTargets = targets; - } - } - - /** - * Resolved shortcuts with corresponding app targets. - */ - public static class Result { - public final boolean isFromAppPredictor; - /** - * Input app targets (see {@link ShortcutLoader#queryShortcuts(DisplayResolveInfo[])} the - * shortcuts were process against. - */ - public final DisplayResolveInfo[] appTargets; - /** - * Shortcuts grouped by app target. - */ - public final ShortcutResultInfo[] shortcutsByApp; - public final Map<ChooserTarget, AppTarget> directShareAppTargetCache; - public final Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache; - - @VisibleForTesting - public Result( - boolean isFromAppPredictor, - DisplayResolveInfo[] appTargets, - ShortcutResultInfo[] shortcutsByApp, - Map<ChooserTarget, AppTarget> directShareAppTargetCache, - Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) { - this.isFromAppPredictor = isFromAppPredictor; - this.appTargets = appTargets; - this.shortcutsByApp = shortcutsByApp; - this.directShareAppTargetCache = directShareAppTargetCache; - this.directShareShortcutInfoCache = directShareShortcutInfoCache; - } - } - - /** - * Shortcuts grouped by app. - */ - public static class ShortcutResultInfo { - public final DisplayResolveInfo appTarget; - public final List<ChooserTarget> shortcuts; - - public ShortcutResultInfo(DisplayResolveInfo appTarget, List<ChooserTarget> shortcuts) { - this.appTarget = appTarget; - this.shortcuts = shortcuts; - } - } - - /** - * A wrapper around AppPredictor to facilitate unit-testing. - */ - @VisibleForTesting - public static class AppPredictorProxy { - private final AppPredictor mAppPredictor; - - AppPredictorProxy(AppPredictor appPredictor) { - mAppPredictor = appPredictor; - } - - /** - * {@link AppPredictor#registerPredictionUpdates} - */ - public void registerPredictionUpdates( - Executor callbackExecutor, AppPredictor.Callback callback) { - mAppPredictor.registerPredictionUpdates(callbackExecutor, callback); - } - - /** - * {@link AppPredictor#unregisterPredictionUpdates} - */ - public void unregisterPredictionUpdates(AppPredictor.Callback callback) { - mAppPredictor.unregisterPredictionUpdates(callback); - } - - /** - * {@link AppPredictor#requestPredictionUpdate} - */ - public void requestPredictionUpdate() { - mAppPredictor.requestPredictionUpdate(); - } - } -} diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt new file mode 100644 index 00000000..6f7542f1 --- /dev/null +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2022 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.shortcuts + +import android.app.ActivityManager +import android.app.prediction.AppPredictor +import android.app.prediction.AppTarget +import android.content.ComponentName +import android.content.Context +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.content.pm.ShortcutManager.ShareShortcutInfo +import android.os.AsyncTask +import android.os.UserHandle +import android.os.UserManager +import android.service.chooser.ChooserTarget +import android.text.TextUtils +import android.util.Log +import androidx.annotation.MainThread +import androidx.annotation.OpenForTesting +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import com.android.intentresolver.chooser.DisplayResolveInfo +import java.lang.RuntimeException +import java.util.ArrayList +import java.util.HashMap +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Consumer + +/** + * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. + * + * + * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut + * updates. The shortcut loading is triggered by the [queryShortcuts], + * the processing will happen on the [backgroundExecutor] and the result is delivered + * through the [callback] on the [callbackExecutor], the main thread. + * + * + * The current version does not improve on the legacy in a way that it does not guarantee that + * each invocation of the [queryShortcuts] will be matched by an + * invocation of the callback (there are early terminations of the flow). Also, the fetched + * shortcuts would be matched against the last known input, i.e. two invocations of + * [queryShortcuts] may result in two callbacks where shortcuts are + * processed against the latest input. + * + */ +@OpenForTesting +open class ShortcutLoader @VisibleForTesting constructor( + private val context: Context, + private val appPredictor: AppPredictorProxy?, + private val userHandle: UserHandle, + private val isPersonalProfile: Boolean, + private val targetIntentFilter: IntentFilter?, + private val backgroundExecutor: Executor, + private val callbackExecutor: Executor, + private val callback: Consumer<Result> +) { + private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter() + private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + private val activeRequest = AtomicReference(NO_REQUEST) + private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) } + private var isDestroyed = false + + @MainThread + constructor( + context: Context, + appPredictor: AppPredictor?, + userHandle: UserHandle, + targetIntentFilter: IntentFilter?, + callback: Consumer<Result> + ) : this( + context, + appPredictor?.let { AppPredictorProxy(it) }, + userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()), + targetIntentFilter, + AsyncTask.SERIAL_EXECUTOR, + context.mainExecutor, + callback + ) + + init { + appPredictor?.registerPredictionUpdates(callbackExecutor, appPredictorCallback) + } + + /** + * Unsubscribe from app predictor if one was provided. + */ + @OpenForTesting + @MainThread + open fun destroy() { + isDestroyed = true + appPredictor?.unregisterPredictionUpdates(appPredictorCallback) + } + + /** + * Set new resolved targets. This will trigger shortcut loading. + * @param appTargets a collection of application targets a loaded set of shortcuts will be + * grouped against + */ + @OpenForTesting + @MainThread + open fun queryShortcuts(appTargets: Array<DisplayResolveInfo>) { + if (isDestroyed) return + activeRequest.set(Request(appTargets)) + backgroundExecutor.execute { loadShortcuts() } + } + + @WorkerThread + private fun loadShortcuts() { + // no need to query direct share for work profile when its locked or disabled + if (!shouldQueryDirectShareTargets()) return + Log.d(TAG, "querying direct share targets") + queryDirectShareTargets(false) + } + + @WorkerThread + private fun queryDirectShareTargets(skipAppPredictionService: Boolean) { + if (!skipAppPredictionService && appPredictor != null) { + appPredictor.requestPredictionUpdate() + return + } + // Default to just querying ShortcutManager if AppPredictor not present. + if (targetIntentFilter == null) return + val shortcuts = queryShortcutManager(targetIntentFilter) + sendShareShortcutInfoList(shortcuts, false, null) + } + + @WorkerThread + private fun queryShortcutManager(targetIntentFilter: IntentFilter): List<ShareShortcutInfo> { + val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */) + val sm = selectedProfileContext + .getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager? + val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager + return sm?.getShareTargets(targetIntentFilter) + ?.filter { pm.isPackageEnabled(it.targetComponent.packageName) } + ?: emptyList() + } + + @WorkerThread + private fun onAppPredictorCallback(appPredictorTargets: List<AppTarget>) { + if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { + // APS may be disabled, so try querying targets ourselves. + queryDirectShareTargets(true) + return + } + val pm = context.createContextAsUser(userHandle, 0).packageManager + val pair = appPredictorTargets.toShortcuts(pm) + sendShareShortcutInfoList(pair.shortcuts, true, pair.appTargets) + } + + @WorkerThread + private fun List<AppTarget>.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair = + fold( + ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size)) + ) { acc, appTarget -> + val shortcutInfo = appTarget.shortcutInfo + val packageName = appTarget.packageName + val className = appTarget.className + if (shortcutInfo != null && className != null && pm.isPackageEnabled(packageName)) { + (acc.shortcuts as ArrayList<ShareShortcutInfo>).add( + ShareShortcutInfo(shortcutInfo, ComponentName(packageName, className)) + ) + (acc.appTargets as ArrayList<AppTarget>).add(appTarget) + } + acc + } + + @WorkerThread + private fun sendShareShortcutInfoList( + shortcuts: List<ShareShortcutInfo>, + isFromAppPredictor: Boolean, + appPredictorTargets: List<AppTarget>? + ) { + if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) { + throw RuntimeException( + "resultList and appTargets must have the same size." + + " resultList.size()=" + shortcuts.size + + " appTargets.size()=" + appPredictorTargets.size + ) + } + val directShareAppTargetCache = HashMap<ChooserTarget, AppTarget>() + val directShareShortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() + // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path + // for direct share targets. After ShareSheet is refactored we should use the + // ShareShortcutInfos directly. + val appTargets = activeRequest.get().appTargets + val resultRecords: MutableList<ShortcutResultInfo> = ArrayList() + for (displayResolveInfo in appTargets) { + val matchingShortcuts = shortcuts.filter { + it.targetComponent == displayResolveInfo.resolvedComponentName + } + if (matchingShortcuts.isEmpty()) continue + val chooserTargets = shortcutToChooserTargetConverter.convertToChooserTarget( + matchingShortcuts, + shortcuts, + appPredictorTargets, + directShareAppTargetCache, + directShareShortcutInfoCache + ) + val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets) + resultRecords.add(resultRecord) + } + postReport( + Result( + isFromAppPredictor, + appTargets, + resultRecords.toTypedArray(), + directShareAppTargetCache, + directShareShortcutInfoCache + ) + ) + } + + private fun postReport(result: Result) = callbackExecutor.execute { report(result) } + + @MainThread + private fun report(result: Result) { + if (isDestroyed) return + callback.accept(result) + } + + /** + * Returns `false` if `userHandle` is the work profile and it's either + * in quiet mode or not running. + */ + private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive + + @get:VisibleForTesting + protected val isProfileActive: Boolean + get() = userManager.isUserRunning(userHandle) + && userManager.isUserUnlocked(userHandle) + && !userManager.isQuietModeEnabled(userHandle) + + private class Request(val appTargets: Array<DisplayResolveInfo>) + + /** + * Resolved shortcuts with corresponding app targets. + */ + class Result( + val isFromAppPredictor: Boolean, + /** + * Input app targets (see [ShortcutLoader.queryShortcuts] the + * shortcuts were process against. + */ + val appTargets: Array<DisplayResolveInfo>, + /** + * Shortcuts grouped by app target. + */ + val shortcutsByApp: Array<ShortcutResultInfo>, + val directShareAppTargetCache: Map<ChooserTarget, AppTarget>, + val directShareShortcutInfoCache: Map<ChooserTarget, ShortcutInfo> + ) + + /** + * Shortcuts grouped by app. + */ + class ShortcutResultInfo( + val appTarget: DisplayResolveInfo, + val shortcuts: List<ChooserTarget?> + ) + + private class ShortcutsAppTargetsPair( + val shortcuts: List<ShareShortcutInfo>, + val appTargets: List<AppTarget>? + ) + + /** + * A wrapper around AppPredictor to facilitate unit-testing. + */ + @VisibleForTesting + open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) { + /** + * [AppPredictor.registerPredictionUpdates] + */ + open fun registerPredictionUpdates( + callbackExecutor: Executor, callback: AppPredictor.Callback + ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback) + + /** + * [AppPredictor.unregisterPredictionUpdates] + */ + open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) = + mAppPredictor.unregisterPredictionUpdates(callback) + + /** + * [AppPredictor.requestPredictionUpdate] + */ + open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate() + } + + companion object { + private const val TAG = "ShortcutLoader" + private val NO_REQUEST = Request(arrayOf()) + + private fun PackageManager.isPackageEnabled(packageName: String): Boolean { + if (TextUtils.isEmpty(packageName)) { + return false + } + return runCatching { + val appInfo = getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + ) + appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0 + }.getOrDefault(false) + } + } +} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index 5756a0cd..0c817cb2 100644 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -28,6 +28,8 @@ import android.os.UserHandle import android.os.UserManager import androidx.test.filters.SmallTest import com.android.intentresolver.any +import com.android.intentresolver.argumentCaptor +import com.android.intentresolver.capture import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.createAppTarget import com.android.intentresolver.createShareShortcutInfo @@ -39,8 +41,8 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import org.mockito.ArgumentCaptor import org.mockito.Mockito.anyInt +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -56,9 +58,15 @@ class ShortcutLoaderTest { private val pm = mock<PackageManager> { whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) } + val userManager = mock<UserManager> { + whenever(isUserRunning(any<UserHandle>())).thenReturn(true) + whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true) + whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false) + } private val context = mock<Context> { whenever(packageManager).thenReturn(pm) whenever(createContextAsUser(any(), anyInt())).thenReturn(this) + whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager) } private val executor = ImmediateExecutor() private val intentFilter = mock<IntentFilter>() @@ -66,7 +74,7 @@ class ShortcutLoaderTest { private val callback = mock<Consumer<ShortcutLoader.Result>>() @Test - fun test_app_predictor_result() { + fun test_queryShortcuts_result_consistency_with_AppPredictor() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -85,24 +93,22 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) - verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) - verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val matchingAppTarget = createAppTarget(matchingShortcutInfo) val shortcuts = listOf( matchingAppTarget, - // mismatching shortcut + // an AppTarget that does not belong to any resolved application; should be ignored createAppTarget( createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) ) ) + val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() + verify(appPredictor, atLeastOnce()) + .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertTrue("An app predictor result is expected", result.isFromAppPredictor) @@ -124,7 +130,7 @@ class ShortcutLoaderTest { } @Test - fun test_shortcut_manager_result() { + fun test_queryShortcuts_result_consistency_with_ShortcutManager() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -153,8 +159,8 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) @@ -175,7 +181,7 @@ class ShortcutLoaderTest { } @Test - fun test_fallback_to_shortcut_manager() { + fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -205,13 +211,13 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) + val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) @@ -232,32 +238,32 @@ class ShortcutLoaderTest { } @Test - fun test_do_not_call_services_for_not_running_work_profile() { + fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() { testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) } @Test - fun test_do_not_call_services_for_locked_work_profile() { + fun test_queryShortcuts_do_not_call_services_for_locked_work_profile() { testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) } @Test - fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { + fun test_queryShortcuts_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) } @Test - fun test_call_services_for_not_running_main_profile() { + fun test_queryShortcuts_call_services_for_not_running_main_profile() { testAlwaysCallSystemForMainProfile(isUserRunning = false) } @Test - fun test_call_services_for_locked_main_profile() { + fun test_queryShortcuts_call_services_for_locked_main_profile() { testAlwaysCallSystemForMainProfile(isUserUnlocked = false) } @Test - fun test_call_services_if_quite_mode_is_enabled_for_main_profile() { + fun test_queryShortcuts_call_services_if_quite_mode_is_enabled_for_main_profile() { testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) } @@ -267,7 +273,7 @@ class ShortcutLoaderTest { isQuietModeEnabled: Boolean = false ) { val userHandle = UserHandle.of(10) - val userManager = mock<UserManager> { + with(userManager) { whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) @@ -297,7 +303,7 @@ class ShortcutLoaderTest { isQuietModeEnabled: Boolean = false ) { val userHandle = UserHandle.of(10) - val userManager = mock<UserManager> { + with(userManager) { whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) |