summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2022-10-18 22:59:35 -0700
committer Andrey Epin <ayepin@google.com> 2022-11-16 15:59:10 -0800
commit7697b5f3b4549749e55acdd930f87bcedb56b422 (patch)
tree0ce37c6cafaf1f008e3e6efd7af119cc8e461b3f /java
parenta469f68342d66e35692488377f351a604553f322 (diff)
Extract shortcuts loading logic from ChooserActivity
Extract shortcut loading logic from ChooserActivity into a new class mostly as-is. Major changes: - run the logic on a background executor and deliver the result on the main thread; - replace dependencies from ChooserListAdapter with the data it provided. A number of tests thap previusly used ChooserListAdapter#addServiceResults to provide shortcut results into the view are updated and re-enabled. Re-introduction of ag/20236439 Fix: 259462188 Test: manual tests Test: atest IntentResolverUnitTests Change-Id: I2555c3e486b9a443c5101bbda33b5b214c959b0f
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java338
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java (renamed from java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java)2
-rw-r--r--java/tests/AndroidManifest.xml2
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java19
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java53
-rw-r--r--java/tests/src/com/android/intentresolver/IChooserWrapper.java3
-rw-r--r--java/tests/src/com/android/intentresolver/TestApplication.kt27
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java574
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt329
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt (renamed from java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt)4
11 files changed, 1174 insertions, 603 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 558dfcf7..d5a0c32c 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -45,12 +45,10 @@ 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;
@@ -60,7 +58,6 @@ 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;
@@ -110,6 +107,7 @@ 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;
@@ -136,6 +134,7 @@ 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;
/**
@@ -187,8 +186,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 Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
- private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
+ private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
+ private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
public static final int TARGET_TYPE_DEFAULT = 0;
public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
@@ -279,8 +278,6 @@ 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) {
@@ -432,11 +429,13 @@ 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(
- this,
+ getApplicationContext(),
target.getStringExtra(Intent.EXTRA_TEXT),
- getTargetIntentFilter(target)));
+ targetIntentFilter),
+ targetIntentFilter);
mPreviewCoordinator = new ChooserContentPreviewCoordinator(
mBackgroundThreadPoolExecutor,
@@ -497,7 +496,6 @@ public class ChooserActivity extends ResolverActivity implements
getTargetIntent(), getContentResolver(), this::isImageType),
target.getAction()
);
- mDirectShareShortcutInfoCache = new HashMap<>();
setEnterSharedElementCallback(new SharedElementCallback() {
@Override
@@ -518,20 +516,31 @@ public class ChooserActivity extends ResolverActivity implements
return R.style.Theme_DeviceDefault_Chooser;
}
- private void createProfileRecords(AppPredictorFactory factory) {
+ private void createProfileRecords(
+ AppPredictorFactory factory, IntentFilter targetIntentFilter) {
UserHandle mainUserHandle = getPersonalProfileUserHandle();
- createProfileRecord(mainUserHandle, factory);
+ createProfileRecord(mainUserHandle, targetIntentFilter, factory);
UserHandle workUserHandle = getWorkProfileUserHandle();
if (workUserHandle != null) {
- createProfileRecord(workUserHandle, factory);
+ createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
}
- private void createProfileRecord(UserHandle userHandle, AppPredictorFactory factory) {
+ private void createProfileRecord(
+ UserHandle userHandle, IntentFilter targetIntentFilter, 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));
+ userHandle.getIdentifier(),
+ new ProfileRecord(appPredictor, shortcutLoader));
}
@Nullable
@@ -539,50 +548,19 @@ public class ChooserActivity extends ResolverActivity implements
return mProfileRecords.get(userHandle.getIdentifier(), null);
}
- 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);
- };
+ @VisibleForTesting
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ return new ShortcutLoader(
+ context,
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ callback);
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
@@ -1482,147 +1460,6 @@ 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) {
@@ -1656,7 +1493,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 ResovleInfo");
+ Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
}
}
mIsSuccessfullySelected = true;
@@ -1701,9 +1538,6 @@ 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
@@ -1820,11 +1654,6 @@ 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);
}
@@ -2111,42 +1940,41 @@ public class ChooserActivity extends ResolverActivity implements
}
private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
- // don't support direct share on low ram devices
- if (ActivityManager.isLowRamDeviceStatic()) {
+ UserHandle userHandle = chooserListAdapter.getUserHandle();
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record == null) {
return;
}
-
- // no need to query direct share for work profile when its locked or disabled
- if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+ if (record.shortcutLoader == null) {
return;
}
-
- if (DEBUG) {
- Log.d(TAG, "querying direct share targets from ShortcutManager");
- }
-
- queryDirectShareTargets(chooserListAdapter, false);
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+ record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());
}
- @VisibleForTesting
@MainThread
- protected void onShortcutsLoaded(
- ChooserListAdapter adapter, int targetType, List<ServiceResultInfo> resultInfos) {
- UserHandle userHandle = adapter.getUserHandle();
+ private void onShortcutsLoaded(
+ UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {
if (DEBUG) {
Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
}
- for (ServiceResultInfo resultInfo : resultInfos) {
- if (resultInfo.resultTargets != null) {
+ mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache);
+ mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache);
+ ChooserListAdapter adapter =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+ if (adapter != null) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {
adapter.addServiceResults(
- resultInfo.originalTarget,
- resultInfo.resultTargets,
- targetType,
- emptyIfNull(mDirectShareShortcutInfoCache),
- emptyIfNull(mDirectShareAppTargetCache));
+ resultInfo.appTarget,
+ resultInfo.shortcuts,
+ shortcutsResult.isFromAppPredictor
+ ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ mDirectShareShortcutInfoCache,
+ mDirectShareAppTargetCache);
}
+ adapter.completeServiceTargetLoading();
}
- adapter.completeServiceTargetLoading();
logDirectShareTargetReceived(
MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
@@ -2156,24 +1984,6 @@ 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;
@@ -3211,16 +3021,6 @@ 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;
@@ -3396,22 +3196,28 @@ 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;
- ProfileRecord(@Nullable AppPredictor appPredictor) {
+ private ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
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/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
new file mode 100644
index 00000000..1cfa2c8d
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
@@ -0,0 +1,426 @@
+/*
+ * 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/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
index ac4270d3..a37d6558 100644
--- a/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.shortcuts;
import android.annotation.NonNull;
import android.annotation.Nullable;
diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index b220d3ea..306eccb9 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <application>
+ <application android:name="com.android.intentresolver.TestApplication">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
<activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index e474938b..dd78b69e 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -24,15 +24,17 @@ import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.UserHandle;
-import android.util.Pair;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.MetricsLogger;
import java.util.List;
-import java.util.function.BiFunction;
+import java.util.function.Consumer;
import java.util.function.Function;
+import kotlin.jvm.functions.Function2;
+
/**
* Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing.
* We cannot directly mock the activity created since instrumentation creates it, so instead we use
@@ -51,10 +53,8 @@ public class ChooserActivityOverrideData {
@SuppressWarnings("Since15")
public Function<PackageManager, PackageManager> createPackageManager;
public Function<TargetInfo, Boolean> onSafelyStartCallback;
- public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
- public BiFunction<
- IChooserWrapper, ChooserListAdapter, Pair<Integer, ChooserActivity.ServiceResultInfo[]>>
- directShareTargets;
+ public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader>
+ shortcutLoaderFactory = (userHandle, callback) -> null;
public ResolverListController resolverListController;
public ResolverListController workResolverListController;
public Boolean isVoiceInteraction;
@@ -69,15 +69,11 @@ public class ChooserActivityOverrideData {
public UserHandle workProfileUserHandle;
public boolean hasCrossProfileIntents;
public boolean isQuietModeEnabled;
- public boolean isWorkProfileUserRunning;
- public boolean isWorkProfileUserUnlocked;
public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector;
public PackageManager packageManager;
public void reset() {
onSafelyStartCallback = null;
- onQueryDirectShareTargets = null;
- directShareTargets = null;
isVoiceInteraction = null;
createPackageManager = null;
previewThumbnail = null;
@@ -93,8 +89,6 @@ public class ChooserActivityOverrideData {
workProfileUserHandle = null;
hasCrossProfileIntents = true;
isQuietModeEnabled = false;
- isWorkProfileUserRunning = true;
- isWorkProfileUserUnlocked = true;
packageManager = null;
multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() {
@Override
@@ -114,6 +108,7 @@ public class ChooserActivityOverrideData {
isQuietModeEnabled = enabled;
}
};
+ shortcutLoaderFactory = ((userHandle, resultConsumer) -> null);
}
private ChooserActivityOverrideData() {}
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 8c7c28bb..6b74fcd4 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -19,11 +19,13 @@ package com.android.intentresolver;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
+import android.app.prediction.AppPredictor;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
@@ -31,7 +33,6 @@ import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.UserHandle;
-import android.util.Pair;
import android.util.Size;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter;
@@ -44,11 +45,12 @@ import com.android.intentresolver.ResolverListController;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import java.util.Arrays;
import java.util.List;
+import java.util.function.Consumer;
/**
* Simple wrapper around chooser activity to be able to initiate it under test. For more
@@ -256,41 +258,18 @@ public class ChooserWrapperActivity
}
@Override
- protected void queryDirectShareTargets(
- ChooserListAdapter adapter, boolean skipAppPredictionService) {
- if (sOverrides.directShareTargets != null) {
- Pair<Integer, ServiceResultInfo[]> result =
- sOverrides.directShareTargets.apply(this, adapter);
- // Imitate asynchronous shortcut loading
- getMainExecutor().execute(
- () -> onShortcutsLoaded(
- adapter, result.first, Arrays.asList(result.second)));
- return;
- }
- if (sOverrides.onQueryDirectShareTargets != null) {
- sOverrides.onQueryDirectShareTargets.apply(adapter);
- }
- super.queryDirectShareTargets(adapter, skipAppPredictionService);
- }
-
- @Override
- protected boolean isQuietModeEnabled(UserHandle userHandle) {
- return sOverrides.isQuietModeEnabled;
- }
-
- @Override
- protected boolean isUserRunning(UserHandle userHandle) {
- if (userHandle.equals(UserHandle.SYSTEM)) {
- return super.isUserRunning(userHandle);
- }
- return sOverrides.isWorkProfileUserRunning;
- }
-
- @Override
- protected boolean isUserUnlocked(UserHandle userHandle) {
- if (userHandle.equals(UserHandle.SYSTEM)) {
- return super.isUserUnlocked(userHandle);
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ ShortcutLoader shortcutLoader =
+ sOverrides.shortcutLoaderFactory.invoke(userHandle, callback);
+ if (shortcutLoader != null) {
+ return shortcutLoader;
}
- return sOverrides.isWorkProfileUserUnlocked;
+ return super.createShortcutLoader(
+ context, appPredictor, userHandle, targetIntentFilter, callback);
}
}
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
index f81cd023..0d44e147 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -25,6 +25,8 @@ import android.os.UserHandle;
import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import java.util.concurrent.Executor;
+
/**
* Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in
* order to expose the internals for override/inspection. Implementations should apply the overrides
@@ -41,4 +43,5 @@ public interface IChooserWrapper {
@Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter);
UserHandle getCurrentUserHandle();
ChooserActivityLogger getChooserActivityLogger();
+ Executor getMainExecutor();
}
diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt
new file mode 100644
index 00000000..849cfbab
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestApplication.kt
@@ -0,0 +1,27 @@
+/*
+ * 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
+
+import android.app.Application
+import android.content.Context
+import android.os.UserHandle
+
+class TestApplication : Application() {
+
+ // return the current context as a work profile doesn't really exist in these tests
+ override fun createContextAsUser(user: UserHandle, flags: Int): Context = this
+} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index 7c304284..da72a749 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -38,7 +38,6 @@ import static com.android.intentresolver.MatcherUtils.first;
import static com.google.common.truth.Truth.assertThat;
-import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
import static org.hamcrest.CoreMatchers.allOf;
@@ -83,6 +82,7 @@ import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.util.Pair;
+import android.util.SparseArray;
import android.view.View;
import androidx.annotation.CallSuper;
@@ -93,9 +93,9 @@ import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
-import com.android.intentresolver.ChooserActivity.ServiceResultInfo;
import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -118,6 +118,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Function;
/**
@@ -1279,7 +1280,7 @@ public class UnbundledChooserActivityTest {
}
// This test is too long and too slow and should not be taken as an example for future tests.
- @Test @Ignore
+ @Test
public void testDirectTargetSelectionLogging() {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
@@ -1298,37 +1299,55 @@ public class UnbundledChooserActivityTest {
// Set up resources
MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
- ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
// Start activity
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
- // Insert the direct share target
- Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
- directShareToShortcutInfos.put(serviceTargets.get(0), null);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> activity.getAdapter().addServiceResults(
- activity.createTestDisplayResolveInfo(sendIntent,
- ri,
- "testLabel",
- "testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null),
- serviceTargets,
- TARGET_TYPE_CHOOSER_TARGET,
- directShareToShortcutInfos,
- /* directShareToAppTargets */ null)
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
);
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
- assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
- activity.getAdapter().getCount(), is(3));
- assertThat("Chooser should have exactly one selectable direct target",
- activity.getAdapter().getSelectableServiceTargetCount(), is(1));
- assertThat("The resolver info must match the resolver info used to create the target",
- activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
// Click on the direct target
String name = serviceTargets.get(0).getTitle().toString();
@@ -1336,23 +1355,29 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- // Currently we're seeing 3 invocations
- // 1. ChooserActivity.onCreate()
- // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
- // 3. ChooserActivity.startSelected -- which is the one we're after
- verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
+ // Currently we're seeing 4 invocations
+ // 1. ChooserActivity.logActionShareWithPreview()
+ // 2. ChooserActivity.onCreate()
+ // 3. ChooserActivity.logDirectShareTargetReceived()
+ // 4. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
+ LogMaker selectionLog = logMakerCaptor.getAllValues().get(3);
+ assertThat(
+ selectionLog.getCategory(),
is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
- String hashedName = (String) logMakerCaptor
- .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME);
- assertThat("Hash is not predictable but must be obfuscated",
+ String hashedName = (String) selectionLog.getTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_NAME);
+ assertThat(
+ "Hash is not predictable but must be obfuscated",
hashedName, is(not(name)));
- assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
- .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
+ assertThat(
+ "The packages shouldn't match for app target and direct target",
+ selectionLog.getTaggedData(MetricsEvent.FIELD_RANKED_POSITION),
+ is(-1));
}
// This test is too long and too slow and should not be taken as an example for future tests.
- @Test @Ignore
+ @Test
public void testDirectTargetLoggingWithRankedAppTarget() {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
@@ -1371,38 +1396,57 @@ public class UnbundledChooserActivityTest {
// Set up resources
MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
- resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
- ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
// Start activity
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
- // Insert the direct share target
- Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
- directShareToShortcutInfos.put(serviceTargets.get(0), null);
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> activity.getAdapter().addServiceResults(
- activity.createTestDisplayResolveInfo(sendIntent,
- ri,
- "testLabel",
- "testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null),
- serviceTargets,
- TARGET_TYPE_CHOOSER_TARGET,
- directShareToShortcutInfos,
- /* directShareToAppTargets */ null)
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
);
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
- assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
- activity.getAdapter().getCount(), is(3));
- assertThat("Chooser should have exactly one selectable direct target",
- activity.getAdapter().getSelectableServiceTargetCount(), is(1));
- assertThat("The resolver info must match the resolver info used to create the target",
- activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
// Click on the direct target
String name = serviceTargets.get(0).getTitle().toString();
@@ -1410,18 +1454,19 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- // Currently we're seeing 3 invocations
- // 1. ChooserActivity.onCreate()
- // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
- // 3. ChooserActivity.startSelected -- which is the one we're after
- verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
- assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
+ // Currently we're seeing 4 invocations
+ // 1. ChooserActivity.logActionShareWithPreview()
+ // 2. ChooserActivity.onCreate()
+ // 3. ChooserActivity.logDirectShareTargetReceived()
+ // 4. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(3).getCategory(),
is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
assertThat("The packages should match for app target and direct target", logMakerCaptor
- .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
+ .getAllValues().get(3).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
}
- @Test @Ignore
+ @Test
public void testShortcutTargetWithApplyAppLimits() {
// Set up resources
ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
@@ -1445,48 +1490,64 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
- resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
- ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
// Start activity
- final ChooserActivity activity =
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
- final IChooserWrapper wrapper = (IChooserWrapper) activity;
+ final IChooserWrapper activity = (IChooserWrapper) mActivityRule
+ .launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
- // Insert the direct share target
- Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
- List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
- directShareToShortcutInfos.put(serviceTargets.get(0),
- shortcutInfos.get(0).getShortcutInfo());
- directShareToShortcutInfos.put(serviceTargets.get(1),
- shortcutInfos.get(1).getShortcutInfo());
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> wrapper.getAdapter().addServiceResults(
- wrapper.createTestDisplayResolveInfo(sendIntent,
- ri,
- "testLabel",
- "testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null),
- serviceTargets,
- TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
- directShareToShortcutInfos,
- /* directShareToAppTargets */ null)
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
);
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
- assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
- wrapper.getAdapter().getCount(), is(3));
- assertThat("Chooser should have exactly one selectable direct target",
- wrapper.getAdapter().getSelectableServiceTargetCount(), is(1));
- assertThat("The resolver info must match the resolver info used to create the target",
- wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
- assertThat("The display label must match",
- wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(0).getDisplayLabel(),
+ is("testTitle0"));
}
- @Test @Ignore
+ @Test
public void testShortcutTargetWithoutApplyAppLimits() {
setDeviceConfigProperty(
SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
@@ -1513,47 +1574,65 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
- resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
- ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
// Start activity
- final ChooserActivity activity =
+ final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
- final IChooserWrapper wrapper = (IChooserWrapper) activity;
+ waitForIdle();
- // Insert the direct share target
- Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
- List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
- directShareToShortcutInfos.put(serviceTargets.get(0),
- shortcutInfos.get(0).getShortcutInfo());
- directShareToShortcutInfos.put(serviceTargets.get(1),
- shortcutInfos.get(1).getShortcutInfo());
- InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> wrapper.getAdapter().addServiceResults(
- wrapper.createTestDisplayResolveInfo(sendIntent,
- ri,
- "testLabel",
- "testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null),
- serviceTargets,
- TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
- directShareToShortcutInfos,
- /* directShareToAppTargets */ null)
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
);
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
- assertThat("Chooser should have 4 targets (2 apps, 2 direct)",
- wrapper.getAdapter().getCount(), is(4));
- assertThat("Chooser should have exactly two selectable direct target",
- wrapper.getAdapter().getSelectableServiceTargetCount(), is(2));
- assertThat("The resolver info must match the resolver info used to create the target",
- wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
- assertThat("The display label must match",
- wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
- assertThat("The display label must match",
- wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1"));
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 4 targets (2 apps, 2 direct)",
+ activeAdapter.getCount(),
+ is(4));
+ assertThat(
+ "Chooser should have exactly two selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(2));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(0).getDisplayLabel(),
+ is("testTitle0"));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(1).getDisplayLabel(),
+ is("testTitle1"));
}
@Test
@@ -1948,43 +2027,59 @@ public class UnbundledChooserActivityTest {
Mockito.isA(List.class)))
.thenReturn(resolvedComponentInfos);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
- resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
- ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
-
- ChooserActivityOverrideData
- .getInstance()
- .directShareTargets = (activity, adapter) -> {
- DisplayResolveInfo displayInfo = activity.createTestDisplayResolveInfo(
- sendIntent,
- ri,
- "testLabel",
- "testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null);
- ServiceResultInfo[] results = {
- new ServiceResultInfo(displayInfo, serviceTargets) };
- // TODO: consider covering the other type.
- // Only 2 types are expected out of the shortcut loading logic:
- // - TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, if shortcuts were loaded from
- // the ShortcutManager, and;
- // - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, if shortcuts were loaded
- // from AppPredictor.
- // Ideally, our tests should cover all of them.
- return new Pair<>(TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, results);
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
};
// Start activity
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1))
+ .queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ // TODO: test another value as well
+ false,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
activity.getAdapter().getCount(), is(3));
assertThat("Chooser should have exactly one selectable direct target",
activity.getAdapter().getSelectableServiceTargetCount(), is(1));
- assertThat("The resolver info must match the resolver info used to create the target",
- activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
// Click on the direct target
String name = serviceTargets.get(0).getTitle().toString();
@@ -2098,7 +2193,7 @@ public class UnbundledChooserActivityTest {
return true;
};
- mActivityRule.launchActivity(sendIntent);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
waitForIdle();
assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0)));
@@ -2273,21 +2368,20 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() {
+ public void test_query_shortcut_loader_for_the_selected_tab() {
markWorkProfileUserAvailable();
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(3);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
- boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
- ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
- chooserListAdapter -> {
- isQueryDirectShareCalledOnWorkProfile[0] =
- (chooserListAdapter.getUserHandle().getIdentifier() == 10);
- return null;
- };
+ ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class);
+ ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class);
+ final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>();
+ shortcutLoaders.put(0, personalProfileShortcutLoader);
+ shortcutLoaders.put(10, workProfileShortcutLoader);
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null);
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
@@ -2295,118 +2389,14 @@ public class UnbundledChooserActivityTest {
waitForIdle();
onView(withId(com.android.internal.R.id.contentPanel))
.perform(swipeUp());
- onView(withText(R.string.resolver_work_tab)).perform(click());
waitForIdle();
- assertFalse("Direct share targets were queried on a paused work profile",
- isQueryDirectShareCalledOnWorkProfile[0]);
- }
-
- @Test
- public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() {
- markWorkProfileUserAvailable();
- List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
- List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3);
- setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
- boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
- ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
- chooserListAdapter -> {
- isQueryDirectShareCalledOnWorkProfile[0] =
- (chooserListAdapter.getUserHandle().getIdentifier() == 10);
- return null;
- };
- Intent sendIntent = createSendTextIntent();
- sendIntent.setType(TEST_MIME_TYPE);
+ verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any());
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
- waitForIdle();
- onView(withId(com.android.internal.R.id.contentPanel))
- .perform(swipeUp());
onView(withText(R.string.resolver_work_tab)).perform(click());
waitForIdle();
- assertFalse("Direct share targets were queried on a locked work profile user",
- isQueryDirectShareCalledOnWorkProfile[0]);
- }
-
- @Test
- public void testWorkTab_workUserNotRunning_workTargetsShown() {
- markWorkProfileUserAvailable();
- List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
- List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3);
- setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- Intent sendIntent = createSendTextIntent();
- sendIntent.setType(TEST_MIME_TYPE);
- ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
-
- final ChooserActivity activity =
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
- final IChooserWrapper wrapper = (IChooserWrapper) activity;
- waitForIdle();
- onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp());
- onView(withText(R.string.resolver_work_tab)).perform(click());
- waitForIdle();
-
- assertEquals(3, wrapper.getWorkListAdapter().getCount());
- }
-
- @Test
- public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() {
- markWorkProfileUserAvailable();
- List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
- List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3);
- setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
- boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
- ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
- chooserListAdapter -> {
- isQueryDirectShareCalledOnWorkProfile[0] =
- (chooserListAdapter.getUserHandle().getIdentifier() == 10);
- return null;
- };
- Intent sendIntent = createSendTextIntent();
- sendIntent.setType(TEST_MIME_TYPE);
-
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
- waitForIdle();
- onView(withId(com.android.internal.R.id.contentPanel))
- .perform(swipeUp());
- onView(withText(R.string.resolver_work_tab)).perform(click());
- waitForIdle();
-
- assertFalse("Direct share targets were queried on a locked work profile user",
- isQueryDirectShareCalledOnWorkProfile[0]);
- }
-
- @Test
- public void testWorkTab_workUserLocked_workTargetsShown() {
- markWorkProfileUserAvailable();
- List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
- List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3);
- setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- Intent sendIntent = createSendTextIntent();
- sendIntent.setType(TEST_MIME_TYPE);
- ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
-
- final ChooserActivity activity =
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
- final IChooserWrapper wrapper = (IChooserWrapper) activity;
- waitForIdle();
- onView(withId(com.android.internal.R.id.contentPanel))
- .perform(swipeUp());
- onView(withText(R.string.resolver_work_tab)).perform(click());
- waitForIdle();
-
- assertEquals(3, wrapper.getWorkListAdapter().getCount());
+ verify(workProfileShortcutLoader, times(1)).queryShortcuts(any());
}
private Intent createChooserIntent(Intent intent, Intent[] initialIntents) {
@@ -2713,4 +2703,18 @@ public class UnbundledChooserActivityTest {
.getInteger(R.integer.config_chooser_max_targets_per_row))
.thenReturn(targetsPerRow);
}
+
+ private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>>
+ createShortcutLoaderFactory() {
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
+ };
+ return shortcutLoaders;
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
new file mode 100644
index 00000000..5756a0cd
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -0,0 +1,329 @@
+/*
+ * 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.prediction.AppPredictor
+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.ShortcutManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.intentresolver.any
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.createAppTarget
+import com.android.intentresolver.createShareShortcutInfo
+import com.android.intentresolver.createShortcutInfo
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import org.junit.Assert.assertArrayEquals
+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.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+@SmallTest
+class ShortcutLoaderTest {
+ private val appInfo = ApplicationInfo().apply {
+ enabled = true
+ flags = 0
+ }
+ private val pm = mock<PackageManager> {
+ whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
+ }
+ private val context = mock<Context> {
+ whenever(packageManager).thenReturn(pm)
+ whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
+ }
+ private val executor = ImmediateExecutor()
+ private val intentFilter = mock<IntentFilter>()
+ private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ private val callback = mock<Consumer<ShortcutLoader.Result>>()
+
+ @Test
+ fun test_app_predictor_result() {
+ val componentName = ComponentName("pkg", "Class")
+ val appTarget = mock<DisplayResolveInfo> {
+ whenever(resolvedComponentName).thenReturn(componentName)
+ }
+ val appTargets = arrayOf(appTarget)
+ val testSubject = ShortcutLoader(
+ context,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ executor,
+ executor,
+ callback
+ )
+
+ 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
+ createAppTarget(
+ createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ )
+ appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts)
+
+ val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+ verify(callback, times(1)).accept(resultCaptor.capture())
+
+ val result = resultCaptor.value
+ assertTrue("An app predictor result is expected", result.isFromAppPredictor)
+ assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertEquals(
+ "Wrong AppTarget in the cache",
+ matchingAppTarget,
+ result.directShareAppTargetCache[shortcut]
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_shortcut_manager_result() {
+ val componentName = ComponentName("pkg", "Class")
+ val appTarget = mock<DisplayResolveInfo> {
+ whenever(resolvedComponentName).thenReturn(componentName)
+ }
+ val appTargets = arrayOf(appTarget)
+ val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+ val shortcutManagerResult = listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager = mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject = ShortcutLoader(
+ context,
+ null,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ executor,
+ executor,
+ callback
+ )
+
+ testSubject.queryShortcuts(appTargets)
+
+ val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+ verify(callback, times(1)).accept(resultCaptor.capture())
+
+ val result = resultCaptor.value
+ assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+ assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertTrue(
+ "AppTargets are not expected the cache of a ShortcutManager result",
+ result.directShareAppTargetCache.isEmpty()
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_fallback_to_shortcut_manager() {
+ val componentName = ComponentName("pkg", "Class")
+ val appTarget = mock<DisplayResolveInfo> {
+ whenever(resolvedComponentName).thenReturn(componentName)
+ }
+ val appTargets = arrayOf(appTarget)
+ val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+ val shortcutManagerResult = listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager = mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject = ShortcutLoader(
+ context,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ executor,
+ executor,
+ callback
+ )
+
+ testSubject.queryShortcuts(appTargets)
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+ val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java)
+ verify(appPredictor, times(1))
+ .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture())
+ appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList())
+
+ val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+ verify(callback, times(1)).accept(resultCaptor.capture())
+
+ val result = resultCaptor.value
+ assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+ assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertTrue(
+ "AppTargets are not expected the cache of a ShortcutManager result",
+ result.directShareAppTargetCache.isEmpty()
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_do_not_call_services_for_not_running_work_profile() {
+ testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
+ }
+
+ @Test
+ fun test_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() {
+ testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true)
+ }
+
+ @Test
+ fun test_call_services_for_not_running_main_profile() {
+ testAlwaysCallSystemForMainProfile(isUserRunning = false)
+ }
+
+ @Test
+ fun test_call_services_for_locked_main_profile() {
+ testAlwaysCallSystemForMainProfile(isUserUnlocked = false)
+ }
+
+ @Test
+ fun test_call_services_if_quite_mode_is_enabled_for_main_profile() {
+ testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true)
+ }
+
+ private fun testDisabledWorkProfileDoNotCallSystem(
+ isUserRunning: Boolean = true,
+ isUserUnlocked: Boolean = true,
+ isQuietModeEnabled: Boolean = false
+ ) {
+ val userHandle = UserHandle.of(10)
+ val userManager = mock<UserManager> {
+ whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+ whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+ whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+ }
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+ val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ val callback = mock<Consumer<ShortcutLoader.Result>>()
+ val testSubject = ShortcutLoader(
+ context,
+ appPredictor,
+ userHandle,
+ false,
+ intentFilter,
+ executor,
+ executor,
+ callback
+ )
+
+ testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock()))
+
+ verify(appPredictor, never()).requestPredictionUpdate()
+ }
+
+ private fun testAlwaysCallSystemForMainProfile(
+ isUserRunning: Boolean = true,
+ isUserUnlocked: Boolean = true,
+ isQuietModeEnabled: Boolean = false
+ ) {
+ val userHandle = UserHandle.of(10)
+ val userManager = mock<UserManager> {
+ whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+ whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+ whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+ }
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+ val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ val callback = mock<Consumer<ShortcutLoader.Result>>()
+ val testSubject = ShortcutLoader(
+ context,
+ appPredictor,
+ userHandle,
+ true,
+ intentFilter,
+ executor,
+ executor,
+ callback
+ )
+
+ testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock()))
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+ }
+}
+
+private class ImmediateExecutor : Executor {
+ override fun execute(r: Runnable) {
+ r.run()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
index 5529e714..e0de005d 100644
--- a/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver
+package com.android.intentresolver.shortcuts
import android.app.prediction.AppTarget
import android.content.ComponentName
@@ -22,6 +22,8 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager.ShareShortcutInfo
import android.service.chooser.ChooserTarget
+import com.android.intentresolver.createAppTarget
+import com.android.intentresolver.createShareShortcutInfo
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test