summaryrefslogtreecommitdiff
path: root/java/src/com
diff options
context:
space:
mode:
author Aaron Liu <aaronliu@google.com> 2022-11-16 01:36:04 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2022-11-16 01:36:04 +0000
commita469f68342d66e35692488377f351a604553f322 (patch)
tree82a7c7906c8d0d43e834a566aba5c6b70901b55b /java/src/com
parent55032dc2f5969ef71fac98fc9936b4c4dccc94ac (diff)
parent3ae58b63d6c8cda8ff4a224d9d0394768b71e375 (diff)
Merge "Revert "Extract shortcuts loading logic from ChooserActivity"" into tm-qpr-dev
Diffstat (limited to 'java/src/com')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java338
-rw-r--r--java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java (renamed from java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java)2
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
3 files changed, 267 insertions, 499 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index d5a0c32c..558dfcf7 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -45,10 +45,12 @@ import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
@@ -58,6 +60,7 @@ import android.graphics.Insets;
import android.graphics.drawable.Drawable;
import android.metrics.LogMaker;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@@ -107,7 +110,6 @@ import com.android.intentresolver.model.AbstractResolverComparator;
import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
-import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -134,7 +136,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -186,8 +187,8 @@ public class ChooserActivity extends ResolverActivity implements
// `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
// That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
// intermediate data, and then these members can be removed.
- private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
- private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
+ private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
+ private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
public static final int TARGET_TYPE_DEFAULT = 0;
public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
@@ -278,6 +279,8 @@ public class ChooserActivity extends ResolverActivity implements
private View mContentView = null;
+ private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter =
+ new ShortcutToChooserTargetConverter();
private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
private void setupPreDrawForSharedElementTransition(View v) {
@@ -429,13 +432,11 @@ public class ChooserActivity extends ResolverActivity implements
mShouldDisplayLandscape =
shouldDisplayLandscape(getResources().getConfiguration().orientation);
setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false));
- IntentFilter targetIntentFilter = getTargetIntentFilter(target);
createProfileRecords(
new AppPredictorFactory(
- getApplicationContext(),
+ this,
target.getStringExtra(Intent.EXTRA_TEXT),
- targetIntentFilter),
- targetIntentFilter);
+ getTargetIntentFilter(target)));
mPreviewCoordinator = new ChooserContentPreviewCoordinator(
mBackgroundThreadPoolExecutor,
@@ -496,6 +497,7 @@ public class ChooserActivity extends ResolverActivity implements
getTargetIntent(), getContentResolver(), this::isImageType),
target.getAction()
);
+ mDirectShareShortcutInfoCache = new HashMap<>();
setEnterSharedElementCallback(new SharedElementCallback() {
@Override
@@ -516,31 +518,20 @@ public class ChooserActivity extends ResolverActivity implements
return R.style.Theme_DeviceDefault_Chooser;
}
- private void createProfileRecords(
- AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+ private void createProfileRecords(AppPredictorFactory factory) {
UserHandle mainUserHandle = getPersonalProfileUserHandle();
- createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+ createProfileRecord(mainUserHandle, factory);
UserHandle workUserHandle = getWorkProfileUserHandle();
if (workUserHandle != null) {
- createProfileRecord(workUserHandle, targetIntentFilter, factory);
+ createProfileRecord(workUserHandle, factory);
}
}
- private void createProfileRecord(
- UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+ private void createProfileRecord(UserHandle userHandle, AppPredictorFactory factory) {
AppPredictor appPredictor = factory.create(userHandle);
- ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
- ? null
- : createShortcutLoader(
- getApplicationContext(),
- appPredictor,
- userHandle,
- targetIntentFilter,
- shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
mProfileRecords.put(
- userHandle.getIdentifier(),
- new ProfileRecord(appPredictor, shortcutLoader));
+ userHandle.getIdentifier(), new ProfileRecord(appPredictor));
}
@Nullable
@@ -548,19 +539,50 @@ public class ChooserActivity extends ResolverActivity implements
return mProfileRecords.get(userHandle.getIdentifier(), null);
}
- @VisibleForTesting
- protected ShortcutLoader createShortcutLoader(
- Context context,
- AppPredictor appPredictor,
- UserHandle userHandle,
- IntentFilter targetIntentFilter,
- Consumer<ShortcutLoader.Result> callback) {
- return new ShortcutLoader(
- context,
- appPredictor,
- userHandle,
- targetIntentFilter,
- callback);
+ private void setupAppPredictorForUser(
+ UserHandle userHandle, AppPredictor.Callback appPredictorCallback) {
+ AppPredictor appPredictor = getAppPredictor(userHandle);
+ if (appPredictor == null) {
+ return;
+ }
+ mDirectShareAppTargetCache = new HashMap<>();
+ appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback);
+ }
+
+ private AppPredictor.Callback createAppPredictorCallback(
+ ChooserListAdapter chooserListAdapter) {
+ return resultList -> {
+ if (isFinishing() || isDestroyed()) {
+ return;
+ }
+ if (chooserListAdapter.getCount() == 0) {
+ return;
+ }
+ if (resultList.isEmpty()
+ && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+ // APS may be disabled, so try querying targets ourselves.
+ queryDirectShareTargets(chooserListAdapter, true);
+ return;
+ }
+ final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos =
+ new ArrayList<>();
+
+ List<AppTarget> shortcutResults = new ArrayList<>();
+ for (AppTarget appTarget : resultList) {
+ if (appTarget.getShortcutInfo() == null) {
+ continue;
+ }
+ shortcutResults.add(appTarget);
+ }
+ resultList = shortcutResults;
+ for (AppTarget appTarget : resultList) {
+ shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo(
+ appTarget.getShortcutInfo(),
+ new ComponentName(
+ appTarget.getPackageName(), appTarget.getClassName())));
+ }
+ sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList);
+ };
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
@@ -1460,6 +1482,147 @@ public class ChooserActivity extends ResolverActivity implements
}
}
+ @VisibleForTesting
+ protected void queryDirectShareTargets(
+ ChooserListAdapter adapter, boolean skipAppPredictionService) {
+ ProfileRecord record = getProfileRecord(adapter.getUserHandle());
+ if (record == null) {
+ return;
+ }
+
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+
+ UserHandle userHandle = adapter.getUserHandle();
+ if (!skipAppPredictionService) {
+ if (record.appPredictor != null) {
+ record.appPredictor.requestPredictionUpdate();
+ return;
+ }
+ }
+ // Default to just querying ShortcutManager if AppPredictor not present.
+ final IntentFilter filter = getTargetIntentFilter();
+ if (filter == null) {
+ return;
+ }
+
+ AsyncTask.execute(() -> {
+ Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
+ ShortcutManager sm = (ShortcutManager) selectedProfileContext
+ .getSystemService(Context.SHORTCUT_SERVICE);
+ List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter);
+ sendShareShortcutInfoList(resultList, adapter, null);
+ });
+ }
+
+ /**
+ * Returns {@code false} if {@code userHandle} is the work profile and it's either
+ * in quiet mode or not running.
+ */
+ private boolean shouldQueryShortcutManager(UserHandle userHandle) {
+ if (!shouldShowTabs()) {
+ return true;
+ }
+ if (!getWorkProfileUserHandle().equals(userHandle)) {
+ return true;
+ }
+ if (!isUserRunning(userHandle)) {
+ return false;
+ }
+ if (!isUserUnlocked(userHandle)) {
+ return false;
+ }
+ if (isQuietModeEnabled(userHandle)) {
+ return false;
+ }
+ return true;
+ }
+
+ private void sendShareShortcutInfoList(
+ List<ShortcutManager.ShareShortcutInfo> resultList,
+ ChooserListAdapter chooserListAdapter,
+ @Nullable List<AppTarget> appTargets) {
+ if (appTargets != null && appTargets.size() != resultList.size()) {
+ throw new RuntimeException("resultList and appTargets must have the same size."
+ + " resultList.size()=" + resultList.size()
+ + " appTargets.size()=" + appTargets.size());
+ }
+ UserHandle userHandle = chooserListAdapter.getUserHandle();
+ Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
+ for (int i = resultList.size() - 1; i >= 0; i--) {
+ final String packageName = resultList.get(i).getTargetComponent().getPackageName();
+ if (!isPackageEnabled(selectedProfileContext, packageName)) {
+ resultList.remove(i);
+ if (appTargets != null) {
+ appTargets.remove(i);
+ }
+ }
+ }
+
+ // If |appTargets| is not null, results are from AppPredictionService and already sorted.
+ final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER :
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+
+ // 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.
+ List<ServiceResultInfo> resultRecords = new ArrayList<>();
+ for (DisplayResolveInfo displayResolveInfo : chooserListAdapter.getDisplayResolveInfos()) {
+ List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
+ filterShortcutsByTargetComponentName(
+ resultList, displayResolveInfo.getResolvedComponentName());
+ if (matchingShortcuts.isEmpty()) {
+ continue;
+ }
+
+ List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter
+ .convertToChooserTarget(
+ matchingShortcuts,
+ resultList,
+ appTargets,
+ mDirectShareAppTargetCache,
+ mDirectShareShortcutInfoCache);
+
+ ServiceResultInfo resultRecord = new ServiceResultInfo(
+ displayResolveInfo, chooserTargets);
+ resultRecords.add(resultRecord);
+ }
+
+ runOnUiThread(() -> {
+ if (!isDestroyed()) {
+ onShortcutsLoaded(chooserListAdapter, shortcutType, resultRecords);
+ }
+ });
+ }
+
+ private 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 boolean isPackageEnabled(Context context, String packageName) {
+ if (TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+ ApplicationInfo appInfo;
+ try {
+ appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+
+ if (appInfo != null && appInfo.enabled
+ && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
+ return true;
+ }
+ return false;
+ }
+
private void logDirectShareTargetReceived(int logCategory, UserHandle forUser) {
ProfileRecord profileRecord = getProfileRecord(forUser);
if (profileRecord == null) {
@@ -1493,7 +1656,7 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
}
} else if (DEBUG) {
- Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
+ Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo");
}
}
mIsSuccessfullySelected = true;
@@ -1538,6 +1701,9 @@ public class ChooserActivity extends ResolverActivity implements
if (directShareAppPredictor == null) {
return;
}
+ if (!targetInfo.isChooserTargetInfo()) {
+ return;
+ }
AppTarget appTarget = targetInfo.getDirectShareAppTarget();
if (appTarget != null) {
// This is a direct share click that was provided by the APS
@@ -1654,6 +1820,11 @@ public class ChooserActivity extends ResolverActivity implements
ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
initialIntents, rList, filterLastUsed,
createListController(userHandle));
+ if (!ActivityManager.isLowRamDeviceStatic()) {
+ AppPredictor.Callback appPredictorCallback =
+ createAppPredictorCallback(chooserListAdapter);
+ setupAppPredictorForUser(userHandle, appPredictorCallback);
+ }
return new ChooserGridAdapter(chooserListAdapter);
}
@@ -1940,41 +2111,42 @@ public class ChooserActivity extends ResolverActivity implements
}
private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
- UserHandle userHandle = chooserListAdapter.getUserHandle();
- ProfileRecord record = getProfileRecord(userHandle);
- if (record == null) {
+ // don't support direct share on low ram devices
+ if (ActivityManager.isLowRamDeviceStatic()) {
return;
}
- if (record.shortcutLoader == null) {
+
+ // no need to query direct share for work profile when its locked or disabled
+ if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
return;
}
- record.loadingStartTime = SystemClock.elapsedRealtime();
- record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());
+
+ if (DEBUG) {
+ Log.d(TAG, "querying direct share targets from ShortcutManager");
+ }
+
+ queryDirectShareTargets(chooserListAdapter, false);
}
+ @VisibleForTesting
@MainThread
- private void onShortcutsLoaded(
- UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {
+ protected void onShortcutsLoaded(
+ ChooserListAdapter adapter, int targetType, List<ServiceResultInfo> resultInfos) {
+ UserHandle userHandle = adapter.getUserHandle();
if (DEBUG) {
Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
}
- mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache);
- mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache);
- ChooserListAdapter adapter =
- mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
- if (adapter != null) {
- for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {
+ for (ServiceResultInfo resultInfo : resultInfos) {
+ if (resultInfo.resultTargets != null) {
adapter.addServiceResults(
- resultInfo.appTarget,
- resultInfo.shortcuts,
- shortcutsResult.isFromAppPredictor
- ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
- : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
- mDirectShareShortcutInfoCache,
- mDirectShareAppTargetCache);
+ resultInfo.originalTarget,
+ resultInfo.resultTargets,
+ targetType,
+ emptyIfNull(mDirectShareShortcutInfoCache),
+ emptyIfNull(mDirectShareAppTargetCache));
}
- adapter.completeServiceTargetLoading();
}
+ adapter.completeServiceTargetLoading();
logDirectShareTargetReceived(
MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
@@ -1984,6 +2156,24 @@ public class ChooserActivity extends ResolverActivity implements
getChooserActivityLogger().logSharesheetDirectLoadComplete();
}
+ @VisibleForTesting
+ protected boolean isUserRunning(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isUserRunning(userHandle);
+ }
+
+ @VisibleForTesting
+ protected boolean isUserUnlocked(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isUserUnlocked(userHandle);
+ }
+
+ @VisibleForTesting
+ protected boolean isQuietModeEnabled(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isQuietModeEnabled(userHandle);
+ }
+
private void setupScrollListener() {
if (mResolverDrawerLayout == null) {
return;
@@ -3021,6 +3211,16 @@ public class ChooserActivity extends ResolverActivity implements
}
}
+ static class ServiceResultInfo {
+ public final DisplayResolveInfo originalTarget;
+ public final List<ChooserTarget> resultTargets;
+
+ ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt) {
+ originalTarget = ot;
+ resultTargets = rt;
+ }
+ }
+
static class ChooserTargetRankingInfo {
public final List<AppTarget> scores;
public final UserHandle userHandle;
@@ -3196,28 +3396,22 @@ public class ChooserActivity extends ResolverActivity implements
getChooserActivityLogger().logSharesheetProfileChanged();
}
+ private static <K, V> Map<K, V> emptyIfNull(@Nullable Map<K, V> map) {
+ return map == null ? Collections.emptyMap() : map;
+ }
+
private static class ProfileRecord {
/** The {@link AppPredictor} for this profile, if any. */
@Nullable
public final AppPredictor appPredictor;
- /**
- * null if we should not load shortcuts.
- */
- @Nullable
- public final ShortcutLoader shortcutLoader;
+
public long loadingStartTime;
- private ProfileRecord(
- @Nullable AppPredictor appPredictor,
- @Nullable ShortcutLoader shortcutLoader) {
+ ProfileRecord(@Nullable AppPredictor appPredictor) {
this.appPredictor = appPredictor;
- this.shortcutLoader = shortcutLoader;
}
public void destroy() {
- if (shortcutLoader != null) {
- shortcutLoader.destroy();
- }
if (appPredictor != null) {
appPredictor.destroy();
}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java
index a37d6558..ac4270d3 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
+++ b/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver.shortcuts;
+package com.android.intentresolver;
import android.annotation.NonNull;
import android.annotation.Nullable;
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();
- }
- }
-}