summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java92
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java155
-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, 305 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index d4855382..5d169d24 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;
@@ -2067,8 +2067,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);
@@ -2117,75 +2122,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));
@@ -2704,14 +2640,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 6d0c8337..3d760129 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;
@@ -71,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
@@ -95,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.
@@ -106,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
@@ -137,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,
@@ -149,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++) {
@@ -207,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() {
@@ -243,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);
@@ -555,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
@@ -667,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 f30d2214..9598613c 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 e9baf893..7a590584 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -1370,92 +1370,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 {
@@ -3112,21 +3026,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);
}