summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2022-12-01 18:11:25 -0800
committer Andrey Epin <ayepin@google.com> 2023-01-22 20:31:04 -0800
commitafe103dfcbe593a79e2c5f9be5707011b23a6ace (patch)
treeff1e718b2ab56e341298da0b35e50434bb6e7f6e /java
parent4d9e604383917238f1674d858630ba420c6e0a29 (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')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java15
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt326
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt58
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)