summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2022-09-28 14:37:37 -0700
committer Andrey Epin <ayepin@google.com> 2022-10-03 14:09:24 -0700
commit8ce4d4290029b8f94a35ad4ed2459dd4d980b36b (patch)
tree5521e5e67ad0f09e0946ad52a608f5433cc891a0 /java
parent02632503da6f22a658bc7a069cfa485a5dc4419a (diff)
Move shortcut processing logic into separate components.
As part of the ongoing architectural change, move shortcut processing logic from ChooserListAdapter and ChooserActivity into spaparte components with no functional changes; add unit tests. The new ShrtcutToChooserTargetConverter class now contains previous ChooserActivity’s convertToChooserTarget method; two related test cases were moved from UnboundedChooserActivityTest class into the new component’s test class. The new ShortcutSelectionLogic class now contains previous ChooserListAdapter’s addServiceResults and insertServiceTarget methods; new unit tests added. Test: atest IntentResolverUnitTests:ShortcutSelectionLogicTest Test: atest IntentResolverUnitTests:ShortcutToChooserTargetConverterTest Test: manual test Fix: 249166394 Change-Id: I49baf7d4a435b1f270554fdf6e004875117a01d8
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java92
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java156
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java166
-rw-r--r--java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java109
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java5
-rw-r--r--java/tests/Android.bp2
-rw-r--r--java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt146
-rw-r--r--java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt271
-rw-r--r--java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt175
-rw-r--r--java/tests/src/com/android/intentresolver/TestHelpers.kt71
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java101
11 files changed, 988 insertions, 306 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 14d77427..36f7ad2a 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -26,7 +26,6 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
@@ -121,7 +120,6 @@ import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
import com.android.intentresolver.chooser.TargetInfo;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
@@ -142,7 +140,6 @@ import java.lang.annotation.RetentionPolicy;
import java.net.URISyntaxException;
import java.text.Collator;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -321,6 +318,9 @@ public class ChooserActivity extends ResolverActivity implements
private View mContentView = null;
+ private ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter =
+ new ShortcutToChooserTargetConverter();
+
private class ContentPreviewCoordinator {
private static final int IMAGE_FADE_IN_MILLIS = 150;
private static final int IMAGE_LOAD_TIMEOUT = 1;
@@ -2054,8 +2054,13 @@ public class ChooserActivity extends ResolverActivity implements
if (matchingShortcuts.isEmpty()) {
continue;
}
- List<ChooserTarget> chooserTargets = convertToChooserTarget(
- matchingShortcuts, resultList, appTargets, shortcutType);
+ List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter
+ .convertToChooserTarget(
+ matchingShortcuts,
+ resultList,
+ appTargets,
+ mDirectShareAppTargetCache,
+ mDirectShareShortcutInfoCache);
ServiceResultInfo resultRecord = new ServiceResultInfo(
displayResolveInfo, chooserTargets, userHandle);
@@ -2104,75 +2109,6 @@ public class ChooserActivity extends ResolverActivity implements
return false;
}
- /**
- * Converts a list of ShareShortcutInfos to ChooserTargets.
- * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
- * share intent filter.
- * @param allShortcuts List of all the shortcuts from all the packages on the device that are
- * returned for the current sharing action.
- * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
- * @param shortcutType One of the values TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER or
- * TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
- * @return A list of ChooserTargets sorted by score in descending order.
- */
- @VisibleForTesting
- @NonNull
- public List<ChooserTarget> convertToChooserTarget(
- @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
- @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
- @Nullable List<AppTarget> allAppTargets, @ShareTargetType int shortcutType) {
- // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
- // list instead of the actual rank value when converting a rank to a score.
- List<Integer> scoreList = new ArrayList<>();
- if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
- for (int i = 0; i < matchingShortcuts.size(); i++) {
- int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
- if (!scoreList.contains(shortcutRank)) {
- scoreList.add(shortcutRank);
- }
- }
- Collections.sort(scoreList);
- }
-
- List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
- for (int i = 0; i < matchingShortcuts.size(); i++) {
- ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
- int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
-
- float score;
- if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
- // Incoming results are ordered. Create a score based on index in the original list.
- score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
- } else {
- // Create a score based on the rank of the shortcut.
- int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
- score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
- }
-
- Bundle extras = new Bundle();
- extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
-
- ChooserTarget chooserTarget = new ChooserTarget(
- shortcutInfo.getLabel(),
- null, // Icon will be loaded later if this target is selected to be shown.
- score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
-
- chooserTargetList.add(chooserTarget);
- if (mDirectShareAppTargetCache != null && allAppTargets != null) {
- mDirectShareAppTargetCache.put(chooserTarget,
- allAppTargets.get(indexInAllShortcuts));
- }
- if (mDirectShareShortcutInfoCache != null) {
- mDirectShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
- }
- }
- // Sort ChooserTargets by score in descending order
- Comparator<ChooserTarget> byScore =
- (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
- Collections.sort(chooserTargetList, byScore);
- return chooserTargetList;
- }
-
private void logDirectShareTargetReceived(int logCategory) {
final int apiLatency = (int) (System.currentTimeMillis() - mQueriedSharingShortcutsTimeMs);
getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency));
@@ -2691,14 +2627,6 @@ public class ChooserActivity extends ResolverActivity implements
return mChooserMultiProfilePagerAdapter.getItem(currentPage).getEmptyStateView();
}
- static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
- @Override
- public int compare(ChooserTarget lhs, ChooserTarget rhs) {
- // Descending order
- return (int) Math.signum(rhs.getScore() - lhs.getScore());
- }
- }
-
@Override // ResolverListCommunicator
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 6ddaffd7..849fa43d 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,6 +19,7 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.prediction.AppPredictor;
import android.content.ComponentName;
@@ -48,7 +49,6 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -72,23 +72,18 @@ public class ChooserListAdapter extends ResolverListAdapter {
public static final int TARGET_STANDARD_AZ = 3;
private static final int MAX_SUGGESTED_APP_TARGETS = 4;
- private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
/** {@link #getBaseScore} */
public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
/** {@link #getBaseScore} */
public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
- private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
- private final int mMaxShortcutTargetsPerApp;
private final ChooserListCommunicator mChooserListCommunicator;
private final SelectableTargetInfo.SelectableTargetInfoCommunicator
mSelectableTargetInfoCommunicator;
private final ChooserActivityLogger mChooserActivityLogger;
- private int mNumShortcutResults = 0;
private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>();
- private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
private ChooserTargetInfo
@@ -96,8 +91,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
- private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
- new ChooserActivity.BaseChooserTargetComparator();
private boolean mListViewDataChanged = false;
// Sorted list of DisplayResolveInfos for the alphabetical app section.
@@ -107,6 +100,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
private LoadDirectShareIconTaskProvider mTestLoadDirectShareTaskProvider;
+ private final ShortcutSelectionLogic mShortcutSelectionLogic;
+
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume
// the full width, even if the characters actually take up less than that. Measure the actual
// line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -138,9 +133,13 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
};
- public ChooserListAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList,
- boolean filterLastUsed, ResolverListController resolverListController,
+ public ChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
ChooserListCommunicator chooserListCommunicator,
SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
PackageManager packageManager,
@@ -150,12 +149,17 @@ public class ChooserListAdapter extends ResolverListAdapter {
super(context, payloadIntents, null, rList, filterLastUsed,
resolverListController, chooserListCommunicator, false);
- mMaxShortcutTargetsPerApp =
- context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
mChooserListCommunicator = chooserListCommunicator;
createPlaceHolders();
mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
mChooserActivityLogger = chooserActivityLogger;
+ mShortcutSelectionLogic = new ShortcutSelectionLogic(
+ context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
+ DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ true)
+ );
if (initialIntents != null) {
for (int i = 0; i < initialIntents.length; i++) {
@@ -208,10 +212,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
}
- mApplySharingAppLimits = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
- true);
}
AppPredictor getAppPredictor() {
@@ -244,7 +244,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
private void createPlaceHolders() {
- mNumShortcutResults = 0;
mServiceTargets.clear();
for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) {
mServiceTargets.add(mPlaceHolderTargetInfo);
@@ -556,83 +555,35 @@ public class ChooserListAdapter extends ResolverListAdapter {
* Evaluate targets for inclusion in the direct share area. May not be included
* if score is too low.
*/
- public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+ public void addServiceResults(
+ @Nullable DisplayResolveInfo origTarget,
+ List<ChooserTarget> targets,
@ChooserActivity.ShareTargetType int targetType,
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
- if (DEBUG) {
- Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
- + targets.size()
- + " targets");
- }
- if (targets.size() == 0) {
+ // Avoid inserting any potentially late results
+ if (mServiceTargets.size() == 1
+ && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
return;
}
- final float baseScore = getBaseScore(origTarget, targetType);
- Collections.sort(targets, mBaseTargetComparator);
- final boolean isShortcutResult =
- (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
- || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
- final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
- : MAX_CHOOSER_TARGETS_PER_APP;
- final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
- : targets.size();
- float lastScore = 0;
- boolean shouldNotify = false;
- for (int i = 0, count = targetsLimit; i < count; i++) {
- final ChooserTarget target = targets.get(i);
- float targetScore = target.getScore();
- if (mApplySharingAppLimits) {
- targetScore *= baseScore;
- if (i > 0 && targetScore >= lastScore) {
- // Apply a decay so that the top app can't crowd out everything else.
- // This incents ChooserTargetServices to define what's truly better.
- targetScore = lastScore * 0.95f;
- }
- }
- ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
- : null;
- if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
- targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
- }
- UserHandle userHandle = getUserHandle();
- Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
- boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
- origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
- shortcutInfo));
-
- if (isInserted && isShortcutResult) {
- mNumShortcutResults++;
- }
-
- shouldNotify |= isInserted;
-
- if (DEBUG) {
- Log.d(TAG, " => " + target.toString() + " score=" + targetScore
- + " base=" + target.getScore()
- + " lastScore=" + lastScore
- + " baseScore=" + baseScore
- + " applyAppLimit=" + mApplySharingAppLimits);
- }
-
- lastScore = targetScore;
- }
- if (shouldNotify) {
+ boolean isShortcutResult = targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+ || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+ boolean isUpdated = mShortcutSelectionLogic.addServiceResults(
+ origTarget,
+ getBaseScore(origTarget, targetType),
+ targets,
+ isShortcutResult,
+ directShareToShortcutInfos,
+ mContext.createContextAsUser(getUserHandle(), 0),
+ mSelectableTargetInfoCommunicator,
+ mChooserListCommunicator.getMaxRankedTargets(),
+ mServiceTargets);
+ if (isUpdated) {
notifyDataSetChanged();
}
}
/**
- * The return number have to exceed a minimum limit to make direct share area expandable. When
- * append direct share targets is enabled, return count of all available targets parking in the
- * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
- * shuffling due to older-style direct share targets.
- */
- int getNumServiceTargetsForExpand() {
- return mNumShortcutResults;
- }
-
- /**
* Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
* <ol>
* <li>App-supplied targets
@@ -668,41 +619,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
notifyDataSetChanged();
}
- private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
- // Avoid inserting any potentially late results
- if (mServiceTargets.size() == 1
- && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
- return false;
- }
-
- // Check for duplicates and abort if found
- for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
- if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
- return false;
- }
- }
-
- int currentSize = mServiceTargets.size();
- final float newScore = chooserTargetInfo.getModifiedScore();
- for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets());
- i++) {
- final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
- if (serviceTarget == null) {
- mServiceTargets.set(i, chooserTargetInfo);
- return true;
- } else if (newScore > serviceTarget.getModifiedScore()) {
- mServiceTargets.add(i, chooserTargetInfo);
- return true;
- }
- }
-
- if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) {
- mServiceTargets.add(chooserTargetInfo);
- return true;
- }
-
- return false;
- }
public ChooserTarget getChooserTargetForValue(int value) {
return mServiceTargets.get(value).getChooserTarget();
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
new file mode 100644
index 00000000..e9470231
--- /dev/null
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -0,0 +1,166 @@
+/*
+ * 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.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.ChooserTargetInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutSelectionLogic {
+ private static final String TAG = "ShortcutSelectionLogic";
+ private static final boolean DEBUG = false;
+ private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
+ private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+
+ private final int mMaxShortcutTargetsPerApp;
+ private final boolean mApplySharingAppLimits;
+
+ // Descending order
+ private final Comparator<ChooserTarget> mBaseTargetComparator =
+ (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore());
+
+ ShortcutSelectionLogic(
+ int maxShortcutTargetsPerApp,
+ boolean applySharingAppLimits) {
+ mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp;
+ mApplySharingAppLimits = applySharingAppLimits;
+ }
+
+ /**
+ * Evaluate targets for inclusion in the direct share area. May not be included
+ * if score is too low.
+ */
+ public boolean addServiceResults(
+ @Nullable DisplayResolveInfo origTarget,
+ float origTargetScore,
+ List<ChooserTarget> targets,
+ boolean isShortcutResult,
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+ Context userContext,
+ SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator,
+ int maxRankedTargets,
+ List<ChooserTargetInfo> serviceTargets) {
+ if (DEBUG) {
+ Log.d(TAG, "addServiceResults "
+ + (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", "
+ + targets.size()
+ + " targets");
+ }
+ if (targets.size() == 0) {
+ return false;
+ }
+ Collections.sort(targets, mBaseTargetComparator);
+ final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
+ : MAX_CHOOSER_TARGETS_PER_APP;
+ final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
+ : targets.size();
+ float lastScore = 0;
+ boolean shouldNotify = false;
+ for (int i = 0, count = targetsLimit; i < count; i++) {
+ final ChooserTarget target = targets.get(i);
+ float targetScore = target.getScore();
+ if (mApplySharingAppLimits) {
+ targetScore *= origTargetScore;
+ if (i > 0 && targetScore >= lastScore) {
+ // Apply a decay so that the top app can't crowd out everything else.
+ // This incents ChooserTargetServices to define what's truly better.
+ targetScore = lastScore * 0.95f;
+ }
+ }
+ ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
+ : null;
+ if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
+ targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
+ }
+ boolean isInserted = insertServiceTarget(
+ new SelectableTargetInfo(
+ userContext,
+ origTarget,
+ target,
+ targetScore,
+ mSelectableTargetInfoCommunicator,
+ shortcutInfo),
+ maxRankedTargets,
+ serviceTargets);
+
+ shouldNotify |= isInserted;
+
+ if (DEBUG) {
+ Log.d(TAG, " => " + target + " score=" + targetScore
+ + " base=" + target.getScore()
+ + " lastScore=" + lastScore
+ + " baseScore=" + origTargetScore
+ + " applyAppLimit=" + mApplySharingAppLimits);
+ }
+
+ lastScore = targetScore;
+ }
+
+ return shouldNotify;
+ }
+
+ private boolean insertServiceTarget(
+ SelectableTargetInfo chooserTargetInfo,
+ int maxRankedTargets,
+ List<ChooserTargetInfo> serviceTargets) {
+
+ // Check for duplicates and abort if found
+ for (ChooserTargetInfo otherTargetInfo : serviceTargets) {
+ if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+ return false;
+ }
+ }
+
+ int currentSize = serviceTargets.size();
+ final float newScore = chooserTargetInfo.getModifiedScore();
+ for (int i = 0; i < Math.min(currentSize, maxRankedTargets);
+ i++) {
+ final ChooserTargetInfo serviceTarget = serviceTargets.get(i);
+ if (serviceTarget == null) {
+ serviceTargets.set(i, chooserTargetInfo);
+ return true;
+ } else if (newScore > serviceTarget.getModifiedScore()) {
+ serviceTargets.add(i, chooserTargetInfo);
+ return true;
+ }
+ }
+
+ if (currentSize < maxRankedTargets) {
+ serviceTargets.add(chooserTargetInfo);
+ return true;
+ }
+
+ return false;
+ }
+
+ public interface ScoreProvider {
+ float getScore(ComponentName componentName);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java
new file mode 100644
index 00000000..ac4270d3
--- /dev/null
+++ b/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.prediction.AppTarget;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.Bundle;
+import android.service.chooser.ChooserTarget;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutToChooserTargetConverter {
+
+ /**
+ * Converts a list of ShareShortcutInfos to ChooserTargets.
+ * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
+ * share intent filter.
+ * @param allShortcuts List of all the shortcuts from all the packages on the device that are
+ * returned for the current sharing action.
+ * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
+ * @param directShareAppTargetCache An optional map to store mapping for the new ChooserTarget
+ * instances back to original allAppTargets.
+ * @param directShareShortcutInfoCache An optional map to store mapping from the new
+ * ChooserTarget instances back to the original matchingShortcuts' {@code getShortcutInfo()}
+ * @return A list of ChooserTargets sorted by score in descending order.
+ */
+ @NonNull
+ public List<ChooserTarget> convertToChooserTarget(
+ @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
+ @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
+ @Nullable List<AppTarget> allAppTargets,
+ @Nullable Map<ChooserTarget, AppTarget> directShareAppTargetCache,
+ @Nullable Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) {
+ // If |appTargets| is not null, results are from AppPredictionService and already sorted.
+ final boolean isFromAppPredictor = allAppTargets != null;
+ // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
+ // list instead of the actual rank value when converting a rank to a score.
+ List<Integer> scoreList = new ArrayList<>();
+ if (!isFromAppPredictor) {
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
+ if (!scoreList.contains(shortcutRank)) {
+ scoreList.add(shortcutRank);
+ }
+ }
+ Collections.sort(scoreList);
+ }
+
+ List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
+ int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
+
+ float score;
+ if (isFromAppPredictor) {
+ // Incoming results are ordered. Create a score based on index in the original list.
+ score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
+ } else {
+ // Create a score based on the rank of the shortcut.
+ int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
+ score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
+ }
+
+ Bundle extras = new Bundle();
+ extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
+
+ ChooserTarget chooserTarget = new ChooserTarget(
+ shortcutInfo.getLabel(),
+ null, // Icon will be loaded later if this target is selected to be shown.
+ score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
+
+ chooserTargetList.add(chooserTarget);
+ if (directShareAppTargetCache != null && allAppTargets != null) {
+ directShareAppTargetCache.put(chooserTarget,
+ allAppTargets.get(indexInAllShortcuts));
+ }
+ if (directShareShortcutInfoCache != null) {
+ directShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
+ }
+ }
+ // Sort ChooserTargets by score in descending order
+ Comparator<ChooserTarget> byScore =
+ (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
+ Collections.sort(chooserTargetList, byScore);
+ return chooserTargetList;
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 1610d0fd..dffe2f6c 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -41,7 +41,6 @@ import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
import com.android.intentresolver.SimpleIconFactory;
-
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
@@ -55,6 +54,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
private static final String TAG = "SelectableTargetInfo";
private final Context mContext;
+ @Nullable
private final DisplayResolveInfo mSourceInfo;
private final ResolveInfo mBackupResolveInfo;
private final ChooserTarget mChooserTarget;
@@ -73,7 +73,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
private final float mModifiedScore;
private boolean mIsSuspended = false;
- public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+ public SelectableTargetInfo(Context context, @Nullable DisplayResolveInfo sourceInfo,
ChooserTarget chooserTarget,
float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator,
@Nullable ShortcutInfo shortcutInfo) {
@@ -144,6 +144,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return mIsSuspended;
}
+ @Nullable
public DisplayResolveInfo getDisplayResolveInfo() {
return mSourceInfo;
}
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index fdabc4e0..92974aba 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -7,7 +7,7 @@ android_test {
name: "IntentResolverUnitTests",
// Include all test java files.
- srcs: ["src/**/*.java"],
+ srcs: ["src/**/*.java", "src/**/*.kt"],
libs: [
"android.test.runner",
diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
new file mode 100644
index 00000000..159c6d6a
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -0,0 +1,146 @@
+/*
+ * 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
+
+/**
+ * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
+ * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
+ * be null"). To fix this, we can use methods that modify the return type to be nullable. This
+ * causes Kotlin to skip the null checks.
+ * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+ */
+
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
+import org.mockito.Mockito
+import org.mockito.stubbing.OngoingStubbing
+
+/**
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+inline fun <reified T> any(): T = any(T::class.java)
+
+/**
+ * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
+
+/**
+ * Kotlin type-inferred version of Mockito.nullable()
+ */
+inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
+ ArgumentCaptor.forClass(T::class.java)
+
+/**
+ * Helper function for creating new mocks, without the need to pass in a [Class] instance.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @param apply builder function to simplify stub configuration by improving type inference.
+ */
+inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java)
+ .apply(apply)
+
+/**
+ * Helper function for stubbing methods without the need to use backticks.
+ *
+ * @see Mockito.when
+ */
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+
+/**
+ * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
+ * kotlin tests are mocking kotlin objects and the methods take non-null parameters:
+ *
+ * java.lang.NullPointerException: capture() must not be null
+ */
+class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
+ private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
+ fun capture(): T = wrapped.capture()
+ val value: T
+ get() = wrapped.value
+ val allValues: List<T>
+ get() = wrapped.allValues
+}
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
+ KotlinArgumentCaptor(T::class.java)
+
+/**
+ * Helper function for creating and using a single-use ArgumentCaptor in kotlin.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured = captor.value
+ *
+ * becomes:
+ *
+ * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
+ *
+ * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
+ */
+inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
+ kotlinArgumentCaptor<T>().apply { block() }.value
+
+/**
+ * Variant of [withArgCaptor] for capturing multiple arguments.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured: List<Foo> = captor.allValues
+ *
+ * becomes:
+ *
+ * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ */
+inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
+ kotlinArgumentCaptor<T>().apply{ block() }.allValues
diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
new file mode 100644
index 00000000..052ad446
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -0,0 +1,271 @@
+/*
+ * 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.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.chooser.ChooserTargetInfo
+import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+private const val PACKAGE_A = "package.a"
+private const val PACKAGE_B = "package.b"
+private const val CLASS_NAME = "./MainActivity"
+
+class ShortcutSelectionLogicTest {
+ private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply {
+ arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
+ // shortcuts in reverse priority order
+ val targets = Array(3) { i ->
+ createChooserTarget(
+ "Shortcut $i",
+ (i + 1).toFloat() / 10f,
+ ComponentName(pkg, CLASS_NAME),
+ pkg.shortcutId(i),
+ )
+ }
+ this[pkg] = targets
+ }
+ }
+
+ private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
+ this[pkg]?.get(idx) ?: error("missing package $pkg")
+
+ @Test
+ fun testAddShortcuts_no_limits() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val sc1 = packageTargets[PACKAGE_A, 0]
+ val sc2 = packageTargets[PACKAGE_A, 1]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
+
+ val isUpdated = testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertTrue("Updates are expected", isUpdated)
+ assertShortcutsInOrder(
+ listOf(sc2, sc1),
+ serviceResults,
+ "Two shortcuts are expected as we do not apply per-app shortcut limit"
+ )
+ }
+
+ @Test
+ fun testAddShortcuts_same_package_with_per_package_limit() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val sc1 = packageTargets[PACKAGE_A, 0]
+ val sc2 = packageTargets[PACKAGE_A, 1]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
+
+ val isUpdated = testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertTrue("Updates are expected", isUpdated)
+ assertShortcutsInOrder(
+ listOf(sc2),
+ serviceResults,
+ "One shortcut is expected as we apply per-app shortcut limit"
+ )
+ }
+
+ @Test
+ fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val sc1 = packageTargets[PACKAGE_A, 0]
+ val sc2 = packageTargets[PACKAGE_A, 1]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
+
+ val isUpdated = testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 1,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertTrue("Updates are expected", isUpdated)
+ assertShortcutsInOrder(
+ listOf(sc2),
+ serviceResults,
+ "One shortcut is expected as we apply overall shortcut limit"
+ )
+ }
+
+ @Test
+ fun testAddShortcuts_different_packages_with_per_package_limit() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val pkgAsc1 = packageTargets[PACKAGE_A, 0]
+ val pkgAsc2 = packageTargets[PACKAGE_A, 1]
+ val pkgBsc1 = packageTargets[PACKAGE_B, 0]
+ val pkgBsc2 = packageTargets[PACKAGE_B, 1]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
+
+ testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(pkgAsc1, pkgAsc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+ testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.2f,
+ /* targets = */ listOf(pkgBsc1, pkgBsc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertShortcutsInOrder(
+ listOf(pkgBsc2, pkgAsc2),
+ serviceResults,
+ "Two shortcuts are expected as we apply per-app shortcut limit"
+ )
+ }
+
+ @Test
+ fun testAddShortcuts_pinned_shortcut() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val sc1 = packageTargets[PACKAGE_A, 0]
+ val sc2 = packageTargets[PACKAGE_A, 1]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
+
+ val isUpdated = testSubject.addServiceResults(
+ /* origTarget = */ mock(),
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ mapOf(
+ sc1 to createShortcutInfo(
+ PACKAGE_A.shortcutId(1),
+ sc1.componentName, 1).apply {
+ addFlags(ShortcutInfo.FLAG_PINNED)
+ }
+ ),
+ /* userContext = */ mock(),
+ /* mSelectableTargetInfoCommunicator = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertTrue("Updates are expected", isUpdated)
+ assertShortcutsInOrder(
+ listOf(sc1, sc2),
+ serviceResults,
+ "Two shortcuts are expected as we do not apply per-app shortcut limit"
+ )
+ }
+
+ @Test
+ fun test_available_caller_shortcuts_count_is_limited() {
+ val serviceResults = ArrayList<ChooserTargetInfo>()
+ val sc1 = packageTargets[PACKAGE_A, 0]
+ val sc2 = packageTargets[PACKAGE_A, 1]
+ val sc3 = packageTargets[PACKAGE_A, 2]
+ val testSubject = ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
+ val targetInfoCommunicator = mock<SelectableTargetInfoCommunicator> {
+ whenever(targetIntent).thenReturn(Intent())
+ }
+ val context = mock<Context> {
+ whenever(packageManager).thenReturn(mock())
+ }
+
+ testSubject.addServiceResults(
+ /* origTarget = */ null,
+ /* origTargetScore = */ 0f,
+ /* targets = */ listOf(sc1, sc2, sc3),
+ /* isShortcutResult = */ false /*isShortcutResult*/,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* userContext = */ context,
+ /* mSelectableTargetInfoCommunicator = */ targetInfoCommunicator,
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
+
+ assertShortcutsInOrder(
+ listOf(sc3, sc2),
+ serviceResults,
+ "At most two caller-provided shortcuts are allowed"
+ )
+ }
+
+ private fun assertShortcutsInOrder(
+ expected: List<ChooserTarget>, actual: List<ChooserTargetInfo>, msg: String? = ""
+ ) {
+ assertEquals(msg, expected.size, actual.size)
+ for (i in expected.indices) {
+ assertEquals(
+ "Unexpected item at position $i",
+ expected[i],
+ actual[i].chooserTarget
+ )
+ }
+ }
+
+ private fun String.shortcutId(id: Int) = "$this.$id"
+} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt
new file mode 100644
index 00000000..5529e714
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.prediction.AppTarget
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager.ShareShortcutInfo
+import android.service.chooser.ChooserTarget
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+private const val PACKAGE = "org.package"
+
+class ShortcutToChooserTargetConverterTest {
+ private val testSubject = ShortcutToChooserTargetConverter()
+ private val ranks = arrayOf(3 ,7, 1 ,3)
+ private val shortcuts = ranks
+ .foldIndexed(ArrayList<ShareShortcutInfo>(ranks.size)) { i, acc, rank ->
+ val id = i + 1
+ acc.add(
+ createShareShortcutInfo(
+ id = "id-$i",
+ componentName = ComponentName(PACKAGE, "Class$id"),
+ rank,
+ )
+ )
+ acc
+ }
+
+ @Test
+ fun testConvertToChooserTarget_predictionService() {
+ val appTargets = shortcuts.map { createAppTarget(it.shortcutInfo) }
+ val expectedOrderAllShortcuts = intArrayOf(0, 1, 2, 3)
+ val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.98f, 0.97f)
+ val appTargetCache = HashMap<ChooserTarget, AppTarget>()
+ val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
+
+ var chooserTargets = testSubject.convertToChooserTarget(
+ shortcuts,
+ shortcuts,
+ appTargets,
+ appTargetCache,
+ shortcutInfoCache,
+ )
+
+ assertCorrectShortcutToChooserTargetConversion(
+ shortcuts,
+ chooserTargets,
+ expectedOrderAllShortcuts,
+ expectedScoreAllShortcuts,
+ )
+ assertAppTargetCache(chooserTargets, appTargetCache)
+ assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+
+ val subset = shortcuts.subList(1, shortcuts.size)
+ val expectedOrderSubset = intArrayOf(1, 2, 3)
+ val expectedScoreSubset = floatArrayOf(0.99f, 0.98f, 0.97f)
+ appTargetCache.clear()
+ shortcutInfoCache.clear()
+
+ chooserTargets = testSubject.convertToChooserTarget(
+ subset,
+ shortcuts,
+ appTargets,
+ appTargetCache,
+ shortcutInfoCache,
+ )
+
+ assertCorrectShortcutToChooserTargetConversion(
+ shortcuts,
+ chooserTargets,
+ expectedOrderSubset,
+ expectedScoreSubset,
+ )
+ assertAppTargetCache(chooserTargets, appTargetCache)
+ assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+ }
+
+ @Test
+ fun testConvertToChooserTarget_shortcutManager() {
+ val testSubject = ShortcutToChooserTargetConverter()
+ val expectedOrderAllShortcuts = intArrayOf(2, 0, 3, 1)
+ val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.99f, 0.98f)
+ val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
+
+ var chooserTargets = testSubject.convertToChooserTarget(
+ shortcuts,
+ shortcuts,
+ null,
+ null,
+ shortcutInfoCache,
+ )
+
+ assertCorrectShortcutToChooserTargetConversion(
+ shortcuts, chooserTargets,
+ expectedOrderAllShortcuts, expectedScoreAllShortcuts
+ )
+ assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+
+ val subset: MutableList<ShareShortcutInfo> = java.util.ArrayList()
+ subset.add(shortcuts[1])
+ subset.add(shortcuts[2])
+ subset.add(shortcuts[3])
+ val expectedOrderSubset = intArrayOf(2, 3, 1)
+ val expectedScoreSubset = floatArrayOf(1.0f, 0.99f, 0.98f)
+ shortcutInfoCache.clear()
+
+ chooserTargets = testSubject.convertToChooserTarget(
+ subset,
+ shortcuts,
+ null,
+ null,
+ shortcutInfoCache,
+ )
+
+ assertCorrectShortcutToChooserTargetConversion(
+ shortcuts, chooserTargets,
+ expectedOrderSubset, expectedScoreSubset
+ )
+ assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+ }
+
+ private fun assertCorrectShortcutToChooserTargetConversion(
+ shortcuts: List<ShareShortcutInfo>,
+ chooserTargets: List<ChooserTarget>,
+ expectedOrder: IntArray,
+ expectedScores: FloatArray,
+ ) {
+ assertEquals("Unexpected ChooserTarget count", expectedOrder.size, chooserTargets.size)
+ for (i in chooserTargets.indices) {
+ val ct = chooserTargets[i]
+ val si = shortcuts[expectedOrder[i]].shortcutInfo
+ val cn = shortcuts[expectedOrder[i]].targetComponent
+ assertEquals(si.id, ct.intentExtras.getString(Intent.EXTRA_SHORTCUT_ID))
+ assertEquals(si.label, ct.title)
+ assertEquals(expectedScores[i], ct.score)
+ assertEquals(cn, ct.componentName)
+ }
+ }
+
+ private fun assertAppTargetCache(
+ chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, AppTarget>
+ ) {
+ for (ct in chooserTargets) {
+ val target = cache[ct]
+ assertNotNull("AppTarget is missing", target)
+ }
+ }
+
+ private fun assertShortcutInfoCache(
+ chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, ShortcutInfo>
+ ) {
+ for (ct in chooserTargets) {
+ val si = cache[ct]
+ assertNotNull("AppTarget is missing", si)
+ }
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/TestHelpers.kt b/java/tests/src/com/android/intentresolver/TestHelpers.kt
new file mode 100644
index 00000000..f4b83249
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestHelpers.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.prediction.AppTarget
+import android.app.prediction.AppTargetId
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager.ShareShortcutInfo
+import android.os.Bundle
+import android.service.chooser.ChooserTarget
+import org.mockito.Mockito.`when` as whenever
+
+internal fun createShareShortcutInfo(
+ id: String,
+ componentName: ComponentName,
+ rank: Int
+): ShareShortcutInfo =
+ ShareShortcutInfo(
+ createShortcutInfo(id, componentName, rank),
+ componentName
+ )
+
+internal fun createShortcutInfo(
+ id: String,
+ componentName: ComponentName,
+ rank: Int
+): ShortcutInfo {
+ val context = mock<Context>()
+ whenever(context.packageName).thenReturn(componentName.packageName)
+ return ShortcutInfo.Builder(context, id)
+ .setShortLabel("Short Label $id")
+ .setLongLabel("Long Label $id")
+ .setActivity(componentName)
+ .setRank(rank)
+ .build()
+}
+
+internal fun createAppTarget(shortcutInfo: ShortcutInfo) =
+ AppTarget(
+ AppTargetId(shortcutInfo.id),
+ shortcutInfo,
+ shortcutInfo.activity?.className ?: error("missing activity info")
+ )
+
+internal fun createChooserTarget(
+ title: String, score: Float, componentName: ComponentName, shortcutId: String
+): ChooserTarget =
+ ChooserTarget(
+ title,
+ null,
+ score,
+ componentName,
+ Bundle().apply { putString(Intent.EXTRA_SHORTCUT_ID, shortcutId) }
+ )
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index b901fc1e..089bb8bc 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -1347,92 +1347,6 @@ public class UnbundledChooserActivityTest {
is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
}
- @Test
- public void testConvertToChooserTarget_predictionService() {
- Intent sendIntent = createSendTextIntent();
- List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
-
- final ChooserActivity activity =
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
- waitForIdle();
-
- List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
-
- int[] expectedOrderAllShortcuts = {0, 1, 2, 3};
- float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.98f, 0.97f};
-
- List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
- null, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
- assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
- expectedOrderAllShortcuts, expectedScoreAllShortcuts);
-
- List<ShareShortcutInfo> subset = new ArrayList<>();
- subset.add(shortcuts.get(1));
- subset.add(shortcuts.get(2));
- subset.add(shortcuts.get(3));
-
- int[] expectedOrderSubset = {1, 2, 3};
- float[] expectedScoreSubset = {0.99f, 0.98f, 0.97f};
-
- chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
- TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
- assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
- expectedOrderSubset, expectedScoreSubset);
- }
-
- @Test
- public void testConvertToChooserTarget_shortcutManager() {
- Intent sendIntent = createSendTextIntent();
- List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
-
- final ChooserActivity activity =
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
- waitForIdle();
-
- List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
-
- int[] expectedOrderAllShortcuts = {2, 0, 3, 1};
- float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.99f, 0.98f};
-
- List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
- null, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
- assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
- expectedOrderAllShortcuts, expectedScoreAllShortcuts);
-
- List<ShareShortcutInfo> subset = new ArrayList<>();
- subset.add(shortcuts.get(1));
- subset.add(shortcuts.get(2));
- subset.add(shortcuts.get(3));
-
- int[] expectedOrderSubset = {2, 3, 1};
- float[] expectedScoreSubset = {1.0f, 0.99f, 0.98f};
-
- chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
- TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
- assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
- expectedOrderSubset, expectedScoreSubset);
- }
-
// This test is too long and too slow and should not be taken as an example for future tests.
@Test @Ignore
public void testDirectTargetSelectionLogging() throws InterruptedException {
@@ -3092,21 +3006,6 @@ public class UnbundledChooserActivityTest {
return shortcuts;
}
- private void assertCorrectShortcutToChooserTargetConversion(List<ShareShortcutInfo> shortcuts,
- List<ChooserTarget> chooserTargets, int[] expectedOrder, float[] expectedScores) {
- assertEquals(expectedOrder.length, chooserTargets.size());
- for (int i = 0; i < chooserTargets.size(); i++) {
- ChooserTarget ct = chooserTargets.get(i);
- ShortcutInfo si = shortcuts.get(expectedOrder[i]).getShortcutInfo();
- ComponentName cn = shortcuts.get(expectedOrder[i]).getTargetComponent();
-
- assertEquals(si.getId(), ct.getIntentExtras().getString(Intent.EXTRA_SHORTCUT_ID));
- assertEquals(si.getShortLabel(), ct.getTitle());
- assertThat(Math.abs(expectedScores[i] - ct.getScore()) < 0.000001, is(true));
- assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString());
- }
- }
-
private void markWorkProfileUserAvailable() {
ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
}