From 8ce4d4290029b8f94a35ad4ed2459dd4d980b36b Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Wed, 28 Sep 2022 14:37:37 -0700 Subject: Move shortcut processing logic into separate components. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../android/intentresolver/ChooserActivity.java | 92 +------ .../android/intentresolver/ChooserListAdapter.java | 156 +++--------- .../intentresolver/ShortcutSelectionLogic.java | 166 +++++++++++++ .../ShortcutToChooserTargetConverter.java | 109 +++++++++ .../chooser/SelectableTargetInfo.java | 5 +- java/tests/Android.bp | 2 +- .../android/intentresolver/MockitoKotlinHelpers.kt | 146 +++++++++++ .../intentresolver/ShortcutSelectionLogicTest.kt | 271 +++++++++++++++++++++ .../ShortcutToChooserTargetConverterTest.kt | 175 +++++++++++++ .../src/com/android/intentresolver/TestHelpers.kt | 71 ++++++ .../UnbundledChooserActivityTest.java | 101 -------- 11 files changed, 988 insertions(+), 306 deletions(-) create mode 100644 java/src/com/android/intentresolver/ShortcutSelectionLogic.java create mode 100644 java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java create mode 100644 java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt create mode 100644 java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt create mode 100644 java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt create mode 100644 java/tests/src/com/android/intentresolver/TestHelpers.kt (limited to 'java') 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 chooserTargets = convertToChooserTarget( - matchingShortcuts, resultList, appTargets, shortcutType); + List 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 convertToChooserTarget( - @NonNull List matchingShortcuts, - @NonNull List allShortcuts, - @Nullable List 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 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 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 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 { - @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 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 mServiceTargets = new ArrayList<>(); private final List 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 payloadIntents, - Intent[] initialIntents, List rList, - boolean filterLastUsed, ResolverListController resolverListController, + public ChooserListAdapter( + Context context, + List payloadIntents, + Intent[] initialIntents, + List 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,82 +555,34 @@ 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 targets, + public void addServiceResults( + @Nullable DisplayResolveInfo origTarget, + List targets, @ChooserActivity.ShareTargetType int targetType, Map 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: *
    @@ -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 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 targets, + boolean isShortcutResult, + Map directShareToShortcutInfos, + Context userContext, + SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator, + int maxRankedTargets, + List 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 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 convertToChooserTarget( + @NonNull List matchingShortcuts, + @NonNull List allShortcuts, + @Nullable List allAppTargets, + @Nullable Map directShareAppTargetCache, + @Nullable Map 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 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 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 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 eq(obj: T): T = Mockito.eq(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 any(type: Class): T = Mockito.any(type) +inline fun 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 argThat(matcher: ArgumentMatcher): T = Mockito.argThat(matcher) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun 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 capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun argumentCaptor(): ArgumentCaptor = + 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 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 whenever(methodCall: T): OngoingStubbing = 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 constructor(clazz: Class) { + private val wrapped: ArgumentCaptor = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value + val allValues: List + get() = wrapped.allValues +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun kotlinArgumentCaptor(): KotlinArgumentCaptor = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun withArgCaptor(block: KotlinArgumentCaptor.() -> Unit): T = + kotlinArgumentCaptor().apply { block() }.value + +/** + * Variant of [withArgCaptor] for capturing multiple arguments. + * + * val captor = argumentCaptor() + * verify(...).someMethod(captor.capture()) + * val captured: List = captor.allValues + * + * becomes: + * + * val capturedList = captureMany { verify(...).someMethod(capture()) } + */ +inline fun captureMany(block: KotlinArgumentCaptor.() -> Unit): List = + kotlinArgumentCaptor().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>().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>.get(pkg: String, idx: Int) = + this[pkg]?.get(idx) ?: error("missing package $pkg") + + @Test + fun testAddShortcuts_no_limits() { + val serviceResults = ArrayList() + 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() + 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() + 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() + 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() + 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() + 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 { + whenever(targetIntent).thenReturn(Intent()) + } + val context = mock { + 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, actual: List, 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(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() + val shortcutInfoCache = HashMap() + + 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() + + var chooserTargets = testSubject.convertToChooserTarget( + shortcuts, + shortcuts, + null, + null, + shortcutInfoCache, + ) + + assertCorrectShortcutToChooserTargetConversion( + shortcuts, chooserTargets, + expectedOrderAllShortcuts, expectedScoreAllShortcuts + ) + assertShortcutInfoCache(chooserTargets, shortcutInfoCache) + + val subset: MutableList = 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, + chooserTargets: List, + 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, cache: Map + ) { + for (ct in chooserTargets) { + val target = cache[ct] + assertNotNull("AppTarget is missing", target) + } + } + + private fun assertShortcutInfoCache( + chooserTargets: List, cache: Map + ) { + 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() + 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 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 shortcuts = createShortcuts(activity); - - int[] expectedOrderAllShortcuts = {0, 1, 2, 3}; - float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.98f, 0.97f}; - - List chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts, - null, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderAllShortcuts, expectedScoreAllShortcuts); - - List 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 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 shortcuts = createShortcuts(activity); - - int[] expectedOrderAllShortcuts = {2, 0, 3, 1}; - float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.99f, 0.98f}; - - List chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts, - null, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderAllShortcuts, expectedScoreAllShortcuts); - - List 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 shortcuts, - List 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); } -- cgit v1.2.3-59-g8ed1b