From 3ae58b63d6c8cda8ff4a224d9d0394768b71e375 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Wed, 16 Nov 2022 01:20:12 +0000 Subject: Revert "Extract shortcuts loading logic from ChooserActivity" This reverts commit 9f9dceaa1398c925b233bcc54fb47cf3531a88da. Reason for revert: DroidMonitor-triggered revert due to breakage b/259256230. Change-Id: I399625feb7ee3e6858db2863c8d66e62eccc98c4 --- .../android/intentresolver/ChooserActivity.java | 338 +++++++++--- .../ShortcutToChooserTargetConverter.java | 109 ++++ .../intentresolver/shortcuts/ShortcutLoader.java | 426 --------------- .../ShortcutToChooserTargetConverter.java | 109 ---- java/tests/AndroidManifest.xml | 2 +- .../ChooserActivityOverrideData.java | 19 +- .../intentresolver/ChooserWrapperActivity.java | 53 +- .../android/intentresolver/IChooserWrapper.java | 3 - .../ShortcutToChooserTargetConverterTest.kt | 175 +++++++ .../com/android/intentresolver/TestApplication.kt | 27 - .../UnbundledChooserActivityTest.java | 574 ++++++++++----------- .../intentresolver/shortcuts/ShortcutLoaderTest.kt | 329 ------------ .../ShortcutToChooserTargetConverterTest.kt | 177 ------- 13 files changed, 885 insertions(+), 1456 deletions(-) create mode 100644 java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java delete mode 100644 java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java delete mode 100644 java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java create mode 100644 java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt delete mode 100644 java/tests/src/com/android/intentresolver/TestApplication.kt delete mode 100644 java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt delete mode 100644 java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 40f04650..d954104e 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -47,10 +47,12 @@ import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; @@ -60,6 +62,7 @@ import android.graphics.Insets; import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -114,7 +117,6 @@ import com.android.intentresolver.model.AbstractResolverComparator; import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; -import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.intentresolver.widget.RoundedRectImageView; import com.android.internal.annotations.VisibleForTesting; @@ -143,7 +145,6 @@ import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -195,8 +196,8 @@ public class ChooserActivity extends ResolverActivity implements // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their // intermediate data, and then these members can be removed. - private final Map mDirectShareAppTargetCache = new HashMap<>(); - private final Map mDirectShareShortcutInfoCache = new HashMap<>(); + private Map mDirectShareAppTargetCache; + private Map mDirectShareShortcutInfoCache; public static final int TARGET_TYPE_DEFAULT = 0; public static final int TARGET_TYPE_CHOOSER_TARGET = 1; @@ -297,6 +298,8 @@ public class ChooserActivity extends ResolverActivity implements private View mContentView = null; + private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter = + new ShortcutToChooserTargetConverter(); private final SparseArray mProfileRecords = new SparseArray<>(); /** @@ -472,13 +475,11 @@ public class ChooserActivity extends ResolverActivity implements mShouldDisplayLandscape = shouldDisplayLandscape(getResources().getConfiguration().orientation); setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false)); - IntentFilter targetIntentFilter = getTargetIntentFilter(target); createProfileRecords( new AppPredictorFactory( - getApplicationContext(), + this, target.getStringExtra(Intent.EXTRA_TEXT), - targetIntentFilter), - targetIntentFilter); + getTargetIntentFilter(target))); mPreviewCoord = new ChooserContentPreviewCoordinator( mBackgroundThreadPoolExecutor, @@ -538,6 +539,7 @@ public class ChooserActivity extends ResolverActivity implements findPreferredContentPreview(getTargetIntent(), getContentResolver()), target.getAction() ); + mDirectShareShortcutInfoCache = new HashMap<>(); setEnterSharedElementCallback(new SharedElementCallback() { @Override @@ -558,31 +560,20 @@ public class ChooserActivity extends ResolverActivity implements return R.style.Theme_DeviceDefault_Chooser; } - private void createProfileRecords( - AppPredictorFactory factory, IntentFilter targetIntentFilter) { + private void createProfileRecords(AppPredictorFactory factory) { UserHandle mainUserHandle = getPersonalProfileUserHandle(); - createProfileRecord(mainUserHandle, targetIntentFilter, factory); + createProfileRecord(mainUserHandle, factory); UserHandle workUserHandle = getWorkProfileUserHandle(); if (workUserHandle != null) { - createProfileRecord(workUserHandle, targetIntentFilter, factory); + createProfileRecord(workUserHandle, factory); } } - private void createProfileRecord( - UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { + private void createProfileRecord(UserHandle userHandle, AppPredictorFactory factory) { AppPredictor appPredictor = factory.create(userHandle); - ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() - ? null - : createShortcutLoader( - getApplicationContext(), - appPredictor, - userHandle, - targetIntentFilter, - shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); mProfileRecords.put( - userHandle.getIdentifier(), - new ProfileRecord(appPredictor, shortcutLoader)); + userHandle.getIdentifier(), new ProfileRecord(appPredictor)); } @Nullable @@ -590,19 +581,50 @@ public class ChooserActivity extends ResolverActivity implements return mProfileRecords.get(userHandle.getIdentifier(), null); } - @VisibleForTesting - protected ShortcutLoader createShortcutLoader( - Context context, - AppPredictor appPredictor, - UserHandle userHandle, - IntentFilter targetIntentFilter, - Consumer callback) { - return new ShortcutLoader( - context, - appPredictor, - userHandle, - targetIntentFilter, - callback); + private void setupAppPredictorForUser( + UserHandle userHandle, AppPredictor.Callback appPredictorCallback) { + AppPredictor appPredictor = getAppPredictor(userHandle); + if (appPredictor == null) { + return; + } + mDirectShareAppTargetCache = new HashMap<>(); + appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback); + } + + private AppPredictor.Callback createAppPredictorCallback( + ChooserListAdapter chooserListAdapter) { + return resultList -> { + if (isFinishing() || isDestroyed()) { + return; + } + if (chooserListAdapter.getCount() == 0) { + return; + } + if (resultList.isEmpty() + && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { + // APS may be disabled, so try querying targets ourselves. + queryDirectShareTargets(chooserListAdapter, true); + return; + } + final List shareShortcutInfos = + new ArrayList<>(); + + List shortcutResults = new ArrayList<>(); + for (AppTarget appTarget : resultList) { + if (appTarget.getShortcutInfo() == null) { + continue; + } + shortcutResults.add(appTarget); + } + resultList = shortcutResults; + for (AppTarget appTarget : resultList) { + shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( + appTarget.getShortcutInfo(), + new ComponentName( + appTarget.getPackageName(), appTarget.getClassName()))); + } + sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList); + }; } static SharedPreferences getPinnedSharedPrefs(Context context) { @@ -1799,6 +1821,147 @@ public class ChooserActivity extends ResolverActivity implements } } + @VisibleForTesting + protected void queryDirectShareTargets( + ChooserListAdapter adapter, boolean skipAppPredictionService) { + ProfileRecord record = getProfileRecord(adapter.getUserHandle()); + if (record == null) { + return; + } + + record.loadingStartTime = SystemClock.elapsedRealtime(); + + UserHandle userHandle = adapter.getUserHandle(); + if (!skipAppPredictionService) { + if (record.appPredictor != null) { + record.appPredictor.requestPredictionUpdate(); + return; + } + } + // Default to just querying ShortcutManager if AppPredictor not present. + final IntentFilter filter = getTargetIntentFilter(); + if (filter == null) { + return; + } + + AsyncTask.execute(() -> { + Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); + ShortcutManager sm = (ShortcutManager) selectedProfileContext + .getSystemService(Context.SHORTCUT_SERVICE); + List resultList = sm.getShareTargets(filter); + sendShareShortcutInfoList(resultList, adapter, null); + }); + } + + /** + * Returns {@code false} if {@code userHandle} is the work profile and it's either + * in quiet mode or not running. + */ + private boolean shouldQueryShortcutManager(UserHandle userHandle) { + if (!shouldShowTabs()) { + return true; + } + if (!getWorkProfileUserHandle().equals(userHandle)) { + return true; + } + if (!isUserRunning(userHandle)) { + return false; + } + if (!isUserUnlocked(userHandle)) { + return false; + } + if (isQuietModeEnabled(userHandle)) { + return false; + } + return true; + } + + private void sendShareShortcutInfoList( + List resultList, + ChooserListAdapter chooserListAdapter, + @Nullable List appTargets) { + if (appTargets != null && appTargets.size() != resultList.size()) { + throw new RuntimeException("resultList and appTargets must have the same size." + + " resultList.size()=" + resultList.size() + + " appTargets.size()=" + appTargets.size()); + } + UserHandle userHandle = chooserListAdapter.getUserHandle(); + Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); + for (int i = resultList.size() - 1; i >= 0; i--) { + final String packageName = resultList.get(i).getTargetComponent().getPackageName(); + if (!isPackageEnabled(selectedProfileContext, packageName)) { + resultList.remove(i); + if (appTargets != null) { + appTargets.remove(i); + } + } + } + + // If |appTargets| is not null, results are from AppPredictionService and already sorted. + final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER : + TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); + + // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path + // for direct share targets. After ShareSheet is refactored we should use the + // ShareShortcutInfos directly. + List resultRecords = new ArrayList<>(); + for (DisplayResolveInfo displayResolveInfo : chooserListAdapter.getDisplayResolveInfos()) { + List matchingShortcuts = + filterShortcutsByTargetComponentName( + resultList, displayResolveInfo.getResolvedComponentName()); + if (matchingShortcuts.isEmpty()) { + continue; + } + + List chooserTargets = mShortcutToChooserTargetConverter + .convertToChooserTarget( + matchingShortcuts, + resultList, + appTargets, + mDirectShareAppTargetCache, + mDirectShareShortcutInfoCache); + + ServiceResultInfo resultRecord = new ServiceResultInfo( + displayResolveInfo, chooserTargets); + resultRecords.add(resultRecord); + } + + runOnUiThread(() -> { + if (!isDestroyed()) { + onShortcutsLoaded(chooserListAdapter, shortcutType, resultRecords); + } + }); + } + + private List filterShortcutsByTargetComponentName( + List allShortcuts, ComponentName requiredTarget) { + List matchingShortcuts = new ArrayList<>(); + for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { + if (requiredTarget.equals(shortcut.getTargetComponent())) { + matchingShortcuts.add(shortcut); + } + } + return matchingShortcuts; + } + + private boolean isPackageEnabled(Context context, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + ApplicationInfo appInfo; + try { + appInfo = context.getPackageManager().getApplicationInfo(packageName, 0); + } catch (NameNotFoundException e) { + return false; + } + + if (appInfo != null && appInfo.enabled + && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) { + return true; + } + return false; + } + private void logDirectShareTargetReceived(int logCategory, UserHandle forUser) { ProfileRecord profileRecord = getProfileRecord(forUser); if (profileRecord == null) { @@ -1832,7 +1995,7 @@ public class ChooserActivity extends ResolverActivity implements Log.d(TAG, "Action to be updated is " + targetIntent.getAction()); } } else if (DEBUG) { - Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo"); + Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo"); } } mIsSuccessfullySelected = true; @@ -1877,6 +2040,9 @@ public class ChooserActivity extends ResolverActivity implements if (directShareAppPredictor == null) { return; } + if (!targetInfo.isChooserTargetInfo()) { + return; + } AppTarget appTarget = targetInfo.getDirectShareAppTarget(); if (appTarget != null) { // This is a direct share click that was provided by the APS @@ -1993,6 +2159,11 @@ public class ChooserActivity extends ResolverActivity implements ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents, initialIntents, rList, filterLastUsed, createListController(userHandle)); + if (!ActivityManager.isLowRamDeviceStatic()) { + AppPredictor.Callback appPredictorCallback = + createAppPredictorCallback(chooserListAdapter); + setupAppPredictorForUser(userHandle, appPredictorCallback); + } return new ChooserGridAdapter(chooserListAdapter); } @@ -2279,41 +2450,42 @@ public class ChooserActivity extends ResolverActivity implements } private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { - UserHandle userHandle = chooserListAdapter.getUserHandle(); - ProfileRecord record = getProfileRecord(userHandle); - if (record == null) { + // don't support direct share on low ram devices + if (ActivityManager.isLowRamDeviceStatic()) { return; } - if (record.shortcutLoader == null) { + + // no need to query direct share for work profile when its locked or disabled + if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { return; } - record.loadingStartTime = SystemClock.elapsedRealtime(); - record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos()); + + if (DEBUG) { + Log.d(TAG, "querying direct share targets from ShortcutManager"); + } + + queryDirectShareTargets(chooserListAdapter, false); } + @VisibleForTesting @MainThread - private void onShortcutsLoaded( - UserHandle userHandle, ShortcutLoader.Result shortcutsResult) { + protected void onShortcutsLoaded( + ChooserListAdapter adapter, int targetType, List resultInfos) { + UserHandle userHandle = adapter.getUserHandle(); if (DEBUG) { Log.d(TAG, "onShortcutsLoaded for user: " + userHandle); } - mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache); - mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache); - ChooserListAdapter adapter = - mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); - if (adapter != null) { - for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) { + for (ServiceResultInfo resultInfo : resultInfos) { + if (resultInfo.resultTargets != null) { adapter.addServiceResults( - resultInfo.appTarget, - resultInfo.shortcuts, - shortcutsResult.isFromAppPredictor - ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE - : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, - mDirectShareShortcutInfoCache, - mDirectShareAppTargetCache); + resultInfo.originalTarget, + resultInfo.resultTargets, + targetType, + emptyIfNull(mDirectShareShortcutInfoCache), + emptyIfNull(mDirectShareAppTargetCache)); } - adapter.completeServiceTargetLoading(); } + adapter.completeServiceTargetLoading(); logDirectShareTargetReceived( MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, @@ -2323,6 +2495,24 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logSharesheetDirectLoadComplete(); } + @VisibleForTesting + protected boolean isUserRunning(UserHandle userHandle) { + UserManager userManager = getSystemService(UserManager.class); + return userManager.isUserRunning(userHandle); + } + + @VisibleForTesting + protected boolean isUserUnlocked(UserHandle userHandle) { + UserManager userManager = getSystemService(UserManager.class); + return userManager.isUserUnlocked(userHandle); + } + + @VisibleForTesting + protected boolean isQuietModeEnabled(UserHandle userHandle) { + UserManager userManager = getSystemService(UserManager.class); + return userManager.isQuietModeEnabled(userHandle); + } + private void setupScrollListener() { if (mResolverDrawerLayout == null) { return; @@ -3359,6 +3549,16 @@ public class ChooserActivity extends ResolverActivity implements } } + static class ServiceResultInfo { + public final DisplayResolveInfo originalTarget; + public final List resultTargets; + + ServiceResultInfo(DisplayResolveInfo ot, List rt) { + originalTarget = ot; + resultTargets = rt; + } + } + static class ChooserTargetRankingInfo { public final List scores; public final UserHandle userHandle; @@ -3534,28 +3734,22 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logSharesheetProfileChanged(); } + private static Map emptyIfNull(@Nullable Map map) { + return map == null ? Collections.emptyMap() : map; + } + private static class ProfileRecord { /** The {@link AppPredictor} for this profile, if any. */ @Nullable public final AppPredictor appPredictor; - /** - * null if we should not load shortcuts. - */ - @Nullable - public final ShortcutLoader shortcutLoader; + public long loadingStartTime; - private ProfileRecord( - @Nullable AppPredictor appPredictor, - @Nullable ShortcutLoader shortcutLoader) { + ProfileRecord(@Nullable AppPredictor appPredictor) { this.appPredictor = appPredictor; - this.shortcutLoader = shortcutLoader; } public void destroy() { - if (shortcutLoader != null) { - shortcutLoader.destroy(); - } if (appPredictor != null) { appPredictor.destroy(); } diff --git a/java/src/com/android/intentresolver/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/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java deleted file mode 100644 index 1cfa2c8d..00000000 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.shortcuts; - -import android.app.ActivityManager; -import android.app.prediction.AppPredictor; -import android.app.prediction.AppTarget; -import android.content.ComponentName; -import android.content.Context; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.ApplicationInfoFlags; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.os.AsyncTask; -import android.os.UserHandle; -import android.os.UserManager; -import android.service.chooser.ChooserTarget; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; - -import com.android.intentresolver.chooser.DisplayResolveInfo; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -/** - * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. - *

- * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut - * updates. The shortcut loading is triggered by the {@link #queryShortcuts(DisplayResolveInfo[])}, - * the processing will happen on the {@link #mBackgroundExecutor} and the result is delivered - * through the {@link #mCallback} on the {@link #mCallbackExecutor}, the main thread. - *

- *

- * The current version does not improve on the legacy in a way that it does not guarantee that - * each invocation of the {@link #queryShortcuts(DisplayResolveInfo[])} will be matched by an - * invocation of the callback (there are early terminations of the flow). Also, the fetched - * shortcuts would be matched against the last known input, i.e. two invocations of - * {@link #queryShortcuts(DisplayResolveInfo[])} may result in two callbacks where shortcuts are - * processed against the latest input. - *

- */ -public class ShortcutLoader { - private static final String TAG = "ChooserActivity"; - - private static final Request NO_REQUEST = new Request(new DisplayResolveInfo[0]); - - private final Context mContext; - @Nullable - private final AppPredictorProxy mAppPredictor; - private final UserHandle mUserHandle; - @Nullable - private final IntentFilter mTargetIntentFilter; - private final Executor mBackgroundExecutor; - private final Executor mCallbackExecutor; - private final boolean mIsPersonalProfile; - private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter = - new ShortcutToChooserTargetConverter(); - private final UserManager mUserManager; - private final AtomicReference> mCallback = new AtomicReference<>(); - private final AtomicReference mActiveRequest = new AtomicReference<>(NO_REQUEST); - - @Nullable - private final AppPredictor.Callback mAppPredictorCallback; - - @MainThread - public ShortcutLoader( - Context context, - @Nullable AppPredictor appPredictor, - UserHandle userHandle, - @Nullable IntentFilter targetIntentFilter, - Consumer callback) { - this( - context, - appPredictor == null ? null : new AppPredictorProxy(appPredictor), - userHandle, - userHandle.equals(UserHandle.of(ActivityManager.getCurrentUser())), - targetIntentFilter, - AsyncTask.SERIAL_EXECUTOR, - context.getMainExecutor(), - callback); - } - - @VisibleForTesting - ShortcutLoader( - Context context, - @Nullable AppPredictorProxy appPredictor, - UserHandle userHandle, - boolean isPersonalProfile, - @Nullable IntentFilter targetIntentFilter, - Executor backgroundExecutor, - Executor callbackExecutor, - Consumer callback) { - mContext = context; - mAppPredictor = appPredictor; - mUserHandle = userHandle; - mTargetIntentFilter = targetIntentFilter; - mBackgroundExecutor = backgroundExecutor; - mCallbackExecutor = callbackExecutor; - mCallback.set(callback); - mIsPersonalProfile = isPersonalProfile; - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - - if (mAppPredictor != null) { - mAppPredictorCallback = createAppPredictorCallback(); - mAppPredictor.registerPredictionUpdates(mCallbackExecutor, mAppPredictorCallback); - } else { - mAppPredictorCallback = null; - } - } - - /** - * Unsubscribe from app predictor if one was provided. - */ - @MainThread - public void destroy() { - if (mCallback.getAndSet(null) != null) { - if (mAppPredictor != null) { - mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback); - } - } - } - - private boolean isDestroyed() { - return mCallback.get() == null; - } - - /** - * Set new resolved targets. This will trigger shortcut loading. - * @param appTargets a collection of application targets a loaded set of shortcuts will be - * grouped against - */ - @MainThread - public void queryShortcuts(DisplayResolveInfo[] appTargets) { - if (isDestroyed()) { - return; - } - mActiveRequest.set(new Request(appTargets)); - mBackgroundExecutor.execute(this::loadShortcuts); - } - - @WorkerThread - private void loadShortcuts() { - // no need to query direct share for work profile when its locked or disabled - if (!shouldQueryDirectShareTargets()) { - return; - } - Log.d(TAG, "querying direct share targets"); - queryDirectShareTargets(false); - } - - @WorkerThread - private void queryDirectShareTargets(boolean skipAppPredictionService) { - if (isDestroyed()) { - return; - } - if (!skipAppPredictionService && mAppPredictor != null) { - mAppPredictor.requestPredictionUpdate(); - return; - } - // Default to just querying ShortcutManager if AppPredictor not present. - if (mTargetIntentFilter == null) { - return; - } - - Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); - ShortcutManager sm = (ShortcutManager) selectedProfileContext - .getSystemService(Context.SHORTCUT_SERVICE); - List shortcuts = - sm.getShareTargets(mTargetIntentFilter); - sendShareShortcutInfoList(shortcuts, false, null); - } - - private AppPredictor.Callback createAppPredictorCallback() { - return appPredictorTargets -> { - if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { - // APS may be disabled, so try querying targets ourselves. - queryDirectShareTargets(true); - return; - } - - final List shortcuts = new ArrayList<>(); - List shortcutResults = new ArrayList<>(); - for (AppTarget appTarget : appPredictorTargets) { - if (appTarget.getShortcutInfo() == null) { - continue; - } - shortcutResults.add(appTarget); - } - appPredictorTargets = shortcutResults; - for (AppTarget appTarget : appPredictorTargets) { - shortcuts.add(new ShortcutManager.ShareShortcutInfo( - appTarget.getShortcutInfo(), - new ComponentName(appTarget.getPackageName(), appTarget.getClassName()))); - } - sendShareShortcutInfoList(shortcuts, true, appPredictorTargets); - }; - } - - @WorkerThread - private void sendShareShortcutInfoList( - List shortcuts, - boolean isFromAppPredictor, - @Nullable List appPredictorTargets) { - if (appPredictorTargets != null && appPredictorTargets.size() != shortcuts.size()) { - throw new RuntimeException("resultList and appTargets must have the same size." - + " resultList.size()=" + shortcuts.size() - + " appTargets.size()=" + appPredictorTargets.size()); - } - Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); - for (int i = shortcuts.size() - 1; i >= 0; i--) { - final String packageName = shortcuts.get(i).getTargetComponent().getPackageName(); - if (!isPackageEnabled(selectedProfileContext, packageName)) { - shortcuts.remove(i); - if (appPredictorTargets != null) { - appPredictorTargets.remove(i); - } - } - } - - HashMap directShareAppTargetCache = new HashMap<>(); - HashMap directShareShortcutInfoCache = new HashMap<>(); - // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path - // for direct share targets. After ShareSheet is refactored we should use the - // ShareShortcutInfos directly. - final DisplayResolveInfo[] appTargets = mActiveRequest.get().appTargets; - List resultRecords = new ArrayList<>(); - for (DisplayResolveInfo displayResolveInfo : appTargets) { - List matchingShortcuts = - filterShortcutsByTargetComponentName( - shortcuts, displayResolveInfo.getResolvedComponentName()); - if (matchingShortcuts.isEmpty()) { - continue; - } - - List chooserTargets = mShortcutToChooserTargetConverter - .convertToChooserTarget( - matchingShortcuts, - shortcuts, - appPredictorTargets, - directShareAppTargetCache, - directShareShortcutInfoCache); - - ShortcutResultInfo resultRecord = - new ShortcutResultInfo(displayResolveInfo, chooserTargets); - resultRecords.add(resultRecord); - } - - postReport( - new Result( - isFromAppPredictor, - appTargets, - resultRecords.toArray(new ShortcutResultInfo[0]), - directShareAppTargetCache, - directShareShortcutInfoCache)); - } - - private void postReport(Result result) { - mCallbackExecutor.execute(() -> report(result)); - } - - @MainThread - private void report(Result result) { - Consumer callback = mCallback.get(); - if (callback != null) { - callback.accept(result); - } - } - - /** - * Returns {@code false} if {@code userHandle} is the work profile and it's either - * in quiet mode or not running. - */ - private boolean shouldQueryDirectShareTargets() { - return mIsPersonalProfile || isProfileActive(); - } - - @VisibleForTesting - protected boolean isProfileActive() { - return mUserManager.isUserRunning(mUserHandle) - && mUserManager.isUserUnlocked(mUserHandle) - && !mUserManager.isQuietModeEnabled(mUserHandle); - } - - private static boolean isPackageEnabled(Context context, String packageName) { - if (TextUtils.isEmpty(packageName)) { - return false; - } - ApplicationInfo appInfo; - try { - appInfo = context.getPackageManager().getApplicationInfo( - packageName, - ApplicationInfoFlags.of(PackageManager.GET_META_DATA)); - } catch (NameNotFoundException e) { - return false; - } - - return appInfo != null && appInfo.enabled - && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0; - } - - private static List filterShortcutsByTargetComponentName( - List allShortcuts, ComponentName requiredTarget) { - List matchingShortcuts = new ArrayList<>(); - for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { - if (requiredTarget.equals(shortcut.getTargetComponent())) { - matchingShortcuts.add(shortcut); - } - } - return matchingShortcuts; - } - - private static class Request { - public final DisplayResolveInfo[] appTargets; - - Request(DisplayResolveInfo[] targets) { - appTargets = targets; - } - } - - /** - * Resolved shortcuts with corresponding app targets. - */ - public static class Result { - public final boolean isFromAppPredictor; - /** - * Input app targets (see {@link ShortcutLoader#queryShortcuts(DisplayResolveInfo[])} the - * shortcuts were process against. - */ - public final DisplayResolveInfo[] appTargets; - /** - * Shortcuts grouped by app target. - */ - public final ShortcutResultInfo[] shortcutsByApp; - public final Map directShareAppTargetCache; - public final Map directShareShortcutInfoCache; - - @VisibleForTesting - public Result( - boolean isFromAppPredictor, - DisplayResolveInfo[] appTargets, - ShortcutResultInfo[] shortcutsByApp, - Map directShareAppTargetCache, - Map directShareShortcutInfoCache) { - this.isFromAppPredictor = isFromAppPredictor; - this.appTargets = appTargets; - this.shortcutsByApp = shortcutsByApp; - this.directShareAppTargetCache = directShareAppTargetCache; - this.directShareShortcutInfoCache = directShareShortcutInfoCache; - } - } - - /** - * Shortcuts grouped by app. - */ - public static class ShortcutResultInfo { - public final DisplayResolveInfo appTarget; - public final List shortcuts; - - public ShortcutResultInfo(DisplayResolveInfo appTarget, List shortcuts) { - this.appTarget = appTarget; - this.shortcuts = shortcuts; - } - } - - /** - * A wrapper around AppPredictor to facilitate unit-testing. - */ - @VisibleForTesting - public static class AppPredictorProxy { - private final AppPredictor mAppPredictor; - - AppPredictorProxy(AppPredictor appPredictor) { - mAppPredictor = appPredictor; - } - - /** - * {@link AppPredictor#registerPredictionUpdates} - */ - public void registerPredictionUpdates( - Executor callbackExecutor, AppPredictor.Callback callback) { - mAppPredictor.registerPredictionUpdates(callbackExecutor, callback); - } - - /** - * {@link AppPredictor#unregisterPredictionUpdates} - */ - public void unregisterPredictionUpdates(AppPredictor.Callback callback) { - mAppPredictor.unregisterPredictionUpdates(callback); - } - - /** - * {@link AppPredictor#requestPredictionUpdate} - */ - public void requestPredictionUpdate() { - mAppPredictor.requestPredictionUpdate(); - } - } -} diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java deleted file mode 100644 index a37d6558..00000000 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.shortcuts; - -import android.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/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml index 306eccb9..b220d3ea 100644 --- a/java/tests/AndroidManifest.xml +++ b/java/tests/AndroidManifest.xml @@ -25,7 +25,7 @@ - + diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index dd78b69e..e474938b 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -24,17 +24,15 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.os.UserHandle; +import android.util.Pair; import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.MetricsLogger; import java.util.List; -import java.util.function.Consumer; +import java.util.function.BiFunction; import java.util.function.Function; -import kotlin.jvm.functions.Function2; - /** * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing. * We cannot directly mock the activity created since instrumentation creates it, so instead we use @@ -53,8 +51,10 @@ public class ChooserActivityOverrideData { @SuppressWarnings("Since15") public Function createPackageManager; public Function onSafelyStartCallback; - public Function2, ShortcutLoader> - shortcutLoaderFactory = (userHandle, callback) -> null; + public Function onQueryDirectShareTargets; + public BiFunction< + IChooserWrapper, ChooserListAdapter, Pair> + directShareTargets; public ResolverListController resolverListController; public ResolverListController workResolverListController; public Boolean isVoiceInteraction; @@ -69,11 +69,15 @@ public class ChooserActivityOverrideData { public UserHandle workProfileUserHandle; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; + public boolean isWorkProfileUserRunning; + public boolean isWorkProfileUserUnlocked; public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector; public PackageManager packageManager; public void reset() { onSafelyStartCallback = null; + onQueryDirectShareTargets = null; + directShareTargets = null; isVoiceInteraction = null; createPackageManager = null; previewThumbnail = null; @@ -89,6 +93,8 @@ public class ChooserActivityOverrideData { workProfileUserHandle = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; + isWorkProfileUserRunning = true; + isWorkProfileUserUnlocked = true; packageManager = null; multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() { @Override @@ -108,7 +114,6 @@ public class ChooserActivityOverrideData { isQuietModeEnabled = enabled; } }; - shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); } private ChooserActivityOverrideData() {} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 6b74fcd4..8c7c28bb 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -19,13 +19,11 @@ package com.android.intentresolver; import static org.mockito.Mockito.when; import android.annotation.Nullable; -import android.app.prediction.AppPredictor; import android.app.usage.UsageStatsManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -33,6 +31,7 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.UserHandle; +import android.util.Pair; import android.util.Size; import com.android.intentresolver.AbstractMultiProfilePagerAdapter; @@ -45,12 +44,11 @@ import com.android.intentresolver.ResolverListController; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; /** * Simple wrapper around chooser activity to be able to initiate it under test. For more @@ -258,18 +256,41 @@ public class ChooserWrapperActivity } @Override - protected ShortcutLoader createShortcutLoader( - Context context, - AppPredictor appPredictor, - UserHandle userHandle, - IntentFilter targetIntentFilter, - Consumer callback) { - ShortcutLoader shortcutLoader = - sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); - if (shortcutLoader != null) { - return shortcutLoader; + protected void queryDirectShareTargets( + ChooserListAdapter adapter, boolean skipAppPredictionService) { + if (sOverrides.directShareTargets != null) { + Pair result = + sOverrides.directShareTargets.apply(this, adapter); + // Imitate asynchronous shortcut loading + getMainExecutor().execute( + () -> onShortcutsLoaded( + adapter, result.first, Arrays.asList(result.second))); + return; + } + if (sOverrides.onQueryDirectShareTargets != null) { + sOverrides.onQueryDirectShareTargets.apply(adapter); + } + super.queryDirectShareTargets(adapter, skipAppPredictionService); + } + + @Override + protected boolean isQuietModeEnabled(UserHandle userHandle) { + return sOverrides.isQuietModeEnabled; + } + + @Override + protected boolean isUserRunning(UserHandle userHandle) { + if (userHandle.equals(UserHandle.SYSTEM)) { + return super.isUserRunning(userHandle); + } + return sOverrides.isWorkProfileUserRunning; + } + + @Override + protected boolean isUserUnlocked(UserHandle userHandle) { + if (userHandle.equals(UserHandle.SYSTEM)) { + return super.isUserUnlocked(userHandle); } - return super.createShortcutLoader( - context, appPredictor, userHandle, targetIntentFilter, callback); + return sOverrides.isWorkProfileUserUnlocked; } } diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java index 0d44e147..f81cd023 100644 --- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java +++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java @@ -25,8 +25,6 @@ import android.os.UserHandle; import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.intentresolver.chooser.DisplayResolveInfo; -import java.util.concurrent.Executor; - /** * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in * order to expose the internals for override/inspection. Implementations should apply the overrides @@ -43,5 +41,4 @@ public interface IChooserWrapper { @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter); UserHandle getCurrentUserHandle(); ChooserActivityLogger getChooserActivityLogger(); - Executor getMainExecutor(); } 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/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt deleted file mode 100644 index 849cfbab..00000000 --- a/java/tests/src/com/android/intentresolver/TestApplication.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver - -import android.app.Application -import android.content.Context -import android.os.UserHandle - -class TestApplication : Application() { - - // return the current context as a work profile doesn't really exist in these tests - override fun createContextAsUser(user: UserHandle, flags: Int): Context = this -} \ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index da72a749..7c304284 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -38,6 +38,7 @@ import static com.android.intentresolver.MatcherUtils.first; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; import static org.hamcrest.CoreMatchers.allOf; @@ -82,7 +83,6 @@ import android.os.UserHandle; import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; import android.util.Pair; -import android.util.SparseArray; import android.view.View; import androidx.annotation.CallSuper; @@ -93,9 +93,9 @@ import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; +import com.android.intentresolver.ChooserActivity.ServiceResultInfo; import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -118,7 +118,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import java.util.function.Function; /** @@ -1280,7 +1279,7 @@ public class UnbundledChooserActivityTest { } // This test is too long and too slow and should not be taken as an example for future tests. - @Test + @Test @Ignore public void testDirectTargetSelectionLogging() { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed @@ -1299,55 +1298,37 @@ public class UnbundledChooserActivityTest { // Set up resources MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; ArgumentCaptor logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - - // create test shortcut loader factory, remember loaders and their callbacks - SparseArray>> shortcutLoaders = - createShortcutLoaderFactory(); + // Create direct share target + List serviceTargets = createDirectShareTargets(1, ""); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); // Start activity final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - // verify that ShortcutLoader was queried - ArgumentCaptor appTargets = - ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - - // send shortcuts - assertThat( - "Wrong number of app targets", - appTargets.getValue().length, - is(resolvedComponentInfos.size())); - List serviceTargets = createDirectShareTargets(1, ""); - ShortcutLoader.Result result = new ShortcutLoader.Result( - true, - appTargets.getValue(), - new ShortcutLoader.ShortcutResultInfo[] { - new ShortcutLoader.ShortcutResultInfo( - appTargets.getValue()[0], - serviceTargets - ) - }, - new HashMap<>(), - new HashMap<>() + // Insert the direct share target + Map directShareToShortcutInfos = new HashMap<>(); + directShareToShortcutInfos.put(serviceTargets.get(0), null); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activity.getAdapter().addServiceResults( + activity.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null), + serviceTargets, + TARGET_TYPE_CHOOSER_TARGET, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) ); - activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); - waitForIdle(); - final ChooserListAdapter activeAdapter = activity.getAdapter(); - assertThat( - "Chooser should have 3 targets (2 apps, 1 direct)", - activeAdapter.getCount(), - is(3)); - assertThat( - "Chooser should have exactly one selectable direct target", - activeAdapter.getSelectableServiceTargetCount(), - is(1)); - assertThat( - "The resolver info must match the resolver info used to create the target", - activeAdapter.getItem(0).getResolveInfo(), - is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + activity.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); // Click on the direct target String name = serviceTargets.get(0).getTitle().toString(); @@ -1355,29 +1336,23 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - // Currently we're seeing 4 invocations - // 1. ChooserActivity.logActionShareWithPreview() - // 2. ChooserActivity.onCreate() - // 3. ChooserActivity.logDirectShareTargetReceived() - // 4. ChooserActivity.startSelected -- which is the one we're after - verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture()); - LogMaker selectionLog = logMakerCaptor.getAllValues().get(3); - assertThat( - selectionLog.getCategory(), + // Currently we're seeing 3 invocations + // 1. ChooserActivity.onCreate() + // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() + // 3. ChooserActivity.startSelected -- which is the one we're after + verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); + assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); - String hashedName = (String) selectionLog.getTaggedData( - MetricsEvent.FIELD_HASHED_TARGET_NAME); - assertThat( - "Hash is not predictable but must be obfuscated", + String hashedName = (String) logMakerCaptor + .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME); + assertThat("Hash is not predictable but must be obfuscated", hashedName, is(not(name))); - assertThat( - "The packages shouldn't match for app target and direct target", - selectionLog.getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), - is(-1)); + assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor + .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1)); } // This test is too long and too slow and should not be taken as an example for future tests. - @Test + @Test @Ignore public void testDirectTargetLoggingWithRankedAppTarget() { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed @@ -1396,57 +1371,38 @@ public class UnbundledChooserActivityTest { // Set up resources MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; ArgumentCaptor logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - - // create test shortcut loader factory, remember loaders and their callbacks - SparseArray>> shortcutLoaders = - createShortcutLoaderFactory(); + // Create direct share target + List serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); // Start activity final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - - // verify that ShortcutLoader was queried - ArgumentCaptor appTargets = - ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - // send shortcuts - assertThat( - "Wrong number of app targets", - appTargets.getValue().length, - is(resolvedComponentInfos.size())); - List serviceTargets = createDirectShareTargets( - 1, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ShortcutLoader.Result result = new ShortcutLoader.Result( - true, - appTargets.getValue(), - new ShortcutLoader.ShortcutResultInfo[] { - new ShortcutLoader.ShortcutResultInfo( - appTargets.getValue()[0], - serviceTargets - ) - }, - new HashMap<>(), - new HashMap<>() + // Insert the direct share target + Map directShareToShortcutInfos = new HashMap<>(); + directShareToShortcutInfos.put(serviceTargets.get(0), null); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activity.getAdapter().addServiceResults( + activity.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null), + serviceTargets, + TARGET_TYPE_CHOOSER_TARGET, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) ); - activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); - waitForIdle(); - final ChooserListAdapter activeAdapter = activity.getAdapter(); - assertThat( - "Chooser should have 3 targets (2 apps, 1 direct)", - activeAdapter.getCount(), - is(3)); - assertThat( - "Chooser should have exactly one selectable direct target", - activeAdapter.getSelectableServiceTargetCount(), - is(1)); - assertThat( - "The resolver info must match the resolver info used to create the target", - activeAdapter.getItem(0).getResolveInfo(), - is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + activity.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); // Click on the direct target String name = serviceTargets.get(0).getTitle().toString(); @@ -1454,19 +1410,18 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - // Currently we're seeing 4 invocations - // 1. ChooserActivity.logActionShareWithPreview() - // 2. ChooserActivity.onCreate() - // 3. ChooserActivity.logDirectShareTargetReceived() - // 4. ChooserActivity.startSelected -- which is the one we're after - verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(3).getCategory(), + // Currently we're seeing 3 invocations + // 1. ChooserActivity.onCreate() + // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() + // 3. ChooserActivity.startSelected -- which is the one we're after + verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); + assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); assertThat("The packages should match for app target and direct target", logMakerCaptor - .getAllValues().get(3).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0)); + .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0)); } - @Test + @Test @Ignore public void testShortcutTargetWithApplyAppLimits() { // Set up resources ChooserActivityOverrideData.getInstance().resources = Mockito.spy( @@ -1490,64 +1445,48 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - - // create test shortcut loader factory, remember loaders and their callbacks - SparseArray>> shortcutLoaders = - createShortcutLoaderFactory(); + // Create direct share target + List serviceTargets = createDirectShareTargets(2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); // Start activity - final IChooserWrapper activity = (IChooserWrapper) mActivityRule - .launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - - // verify that ShortcutLoader was queried - ArgumentCaptor appTargets = - ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper wrapper = (IChooserWrapper) activity; - // send shortcuts - assertThat( - "Wrong number of app targets", - appTargets.getValue().length, - is(resolvedComponentInfos.size())); - List serviceTargets = createDirectShareTargets( - 2, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ShortcutLoader.Result result = new ShortcutLoader.Result( - true, - appTargets.getValue(), - new ShortcutLoader.ShortcutResultInfo[] { - new ShortcutLoader.ShortcutResultInfo( - appTargets.getValue()[0], - serviceTargets - ) - }, - new HashMap<>(), - new HashMap<>() + // Insert the direct share target + Map directShareToShortcutInfos = new HashMap<>(); + List shortcutInfos = createShortcuts(activity); + directShareToShortcutInfos.put(serviceTargets.get(0), + shortcutInfos.get(0).getShortcutInfo()); + directShareToShortcutInfos.put(serviceTargets.get(1), + shortcutInfos.get(1).getShortcutInfo()); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> wrapper.getAdapter().addServiceResults( + wrapper.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null), + serviceTargets, + TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) ); - activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); - waitForIdle(); - final ChooserListAdapter activeAdapter = activity.getAdapter(); - assertThat( - "Chooser should have 3 targets (2 apps, 1 direct)", - activeAdapter.getCount(), - is(3)); - assertThat( - "Chooser should have exactly one selectable direct target", - activeAdapter.getSelectableServiceTargetCount(), - is(1)); - assertThat( - "The resolver info must match the resolver info used to create the target", - activeAdapter.getItem(0).getResolveInfo(), - is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - assertThat( - "The display label must match", - activeAdapter.getItem(0).getDisplayLabel(), - is("testTitle0")); + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + wrapper.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + wrapper.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); + assertThat("The display label must match", + wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); } - @Test + @Test @Ignore public void testShortcutTargetWithoutApplyAppLimits() { setDeviceConfigProperty( SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, @@ -1574,65 +1513,47 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - - // create test shortcut loader factory, remember loaders and their callbacks - SparseArray>> shortcutLoaders = - createShortcutLoaderFactory(); + // Create direct share target + List serviceTargets = createDirectShareTargets(2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); // Start activity - final IChooserWrapper activity = (IChooserWrapper) + final ChooserActivity activity = mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - - // verify that ShortcutLoader was queried - ArgumentCaptor appTargets = - ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + final IChooserWrapper wrapper = (IChooserWrapper) activity; - // send shortcuts - assertThat( - "Wrong number of app targets", - appTargets.getValue().length, - is(resolvedComponentInfos.size())); - List serviceTargets = createDirectShareTargets( - 2, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ShortcutLoader.Result result = new ShortcutLoader.Result( - true, - appTargets.getValue(), - new ShortcutLoader.ShortcutResultInfo[] { - new ShortcutLoader.ShortcutResultInfo( - appTargets.getValue()[0], - serviceTargets - ) - }, - new HashMap<>(), - new HashMap<>() + // Insert the direct share target + Map directShareToShortcutInfos = new HashMap<>(); + List shortcutInfos = createShortcuts(activity); + directShareToShortcutInfos.put(serviceTargets.get(0), + shortcutInfos.get(0).getShortcutInfo()); + directShareToShortcutInfos.put(serviceTargets.get(1), + shortcutInfos.get(1).getShortcutInfo()); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> wrapper.getAdapter().addServiceResults( + wrapper.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null), + serviceTargets, + TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) ); - activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); - waitForIdle(); - final ChooserListAdapter activeAdapter = activity.getAdapter(); - assertThat( - "Chooser should have 4 targets (2 apps, 2 direct)", - activeAdapter.getCount(), - is(4)); - assertThat( - "Chooser should have exactly two selectable direct target", - activeAdapter.getSelectableServiceTargetCount(), - is(2)); - assertThat( - "The resolver info must match the resolver info used to create the target", - activeAdapter.getItem(0).getResolveInfo(), - is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - assertThat( - "The display label must match", - activeAdapter.getItem(0).getDisplayLabel(), - is("testTitle0")); - assertThat( - "The display label must match", - activeAdapter.getItem(1).getDisplayLabel(), - is("testTitle1")); + assertThat("Chooser should have 4 targets (2 apps, 2 direct)", + wrapper.getAdapter().getCount(), is(4)); + assertThat("Chooser should have exactly two selectable direct target", + wrapper.getAdapter().getSelectableServiceTargetCount(), is(2)); + assertThat("The resolver info must match the resolver info used to create the target", + wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); + assertThat("The display label must match", + wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); + assertThat("The display label must match", + wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1")); } @Test @@ -2027,59 +1948,43 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // create test shortcut loader factory, remember loaders and their callbacks - SparseArray>> shortcutLoaders = - new SparseArray<>(); - ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = - (userHandle, callback) -> { - Pair> pair = - new Pair<>(mock(ShortcutLoader.class), callback); - shortcutLoaders.put(userHandle.getIdentifier(), pair); - return pair.first; + // Create direct share target + List serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + + ChooserActivityOverrideData + .getInstance() + .directShareTargets = (activity, adapter) -> { + DisplayResolveInfo displayInfo = activity.createTestDisplayResolveInfo( + sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent, + /* resolveInfoPresentationGetter */ null); + ServiceResultInfo[] results = { + new ServiceResultInfo(displayInfo, serviceTargets) }; + // TODO: consider covering the other type. + // Only 2 types are expected out of the shortcut loading logic: + // - TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, if shortcuts were loaded from + // the ShortcutManager, and; + // - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, if shortcuts were loaded + // from AppPredictor. + // Ideally, our tests should cover all of them. + return new Pair<>(TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, results); }; // Start activity final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - - // verify that ShortcutLoader was queried - ArgumentCaptor appTargets = - ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)) - .queryShortcuts(appTargets.capture()); - - // send shortcuts - assertThat( - "Wrong number of app targets", - appTargets.getValue().length, - is(resolvedComponentInfos.size())); - List serviceTargets = createDirectShareTargets(1, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ShortcutLoader.Result result = new ShortcutLoader.Result( - // TODO: test another value as well - false, - appTargets.getValue(), - new ShortcutLoader.ShortcutResultInfo[] { - new ShortcutLoader.ShortcutResultInfo( - appTargets.getValue()[0], - serviceTargets - ) - }, - new HashMap<>(), - new HashMap<>() - ); - activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); - waitForIdle(); assertThat("Chooser should have 3 targets (2 apps, 1 direct)", activity.getAdapter().getCount(), is(3)); assertThat("Chooser should have exactly one selectable direct target", activity.getAdapter().getSelectableServiceTargetCount(), is(1)); - assertThat( - "The resolver info must match the resolver info used to create the target", - activity.getAdapter().getItem(0).getResolveInfo(), - is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); // Click on the direct target String name = serviceTargets.get(0).getTitle().toString(); @@ -2193,7 +2098,7 @@ public class UnbundledChooserActivityTest { return true; }; - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + mActivityRule.launchActivity(sendIntent); waitForIdle(); assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); @@ -2368,20 +2273,21 @@ public class UnbundledChooserActivityTest { } @Test - public void test_query_shortcut_loader_for_the_selected_tab() { + public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); List workResolvedComponentInfos = createResolvedComponentsForTest(3); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); - ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); - final SparseArray shortcutLoaders = new SparseArray<>(); - shortcutLoaders.put(0, personalProfileShortcutLoader); - shortcutLoaders.put(10, workProfileShortcutLoader); - ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = - (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; + ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = + chooserListAdapter -> { + isQueryDirectShareCalledOnWorkProfile[0] = + (chooserListAdapter.getUserHandle().getIdentifier() == 10); + return null; + }; Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); @@ -2389,14 +2295,118 @@ public class UnbundledChooserActivityTest { waitForIdle(); onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any()); + assertFalse("Direct share targets were queried on a paused work profile", + isQueryDirectShareCalledOnWorkProfile[0]); + } + + @Test + public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() { + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; + boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; + ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = + chooserListAdapter -> { + isQueryDirectShareCalledOnWorkProfile[0] = + (chooserListAdapter.getUserHandle().getIdentifier() == 10); + return null; + }; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - verify(workProfileShortcutLoader, times(1)).queryShortcuts(any()); + assertFalse("Direct share targets were queried on a locked work profile user", + isQueryDirectShareCalledOnWorkProfile[0]); + } + + @Test + public void testWorkTab_workUserNotRunning_workTargetsShown() { + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; + + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + final IChooserWrapper wrapper = (IChooserWrapper) activity; + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertEquals(3, wrapper.getWorkListAdapter().getCount()); + } + + @Test + public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() { + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; + boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; + ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = + chooserListAdapter -> { + isQueryDirectShareCalledOnWorkProfile[0] = + (chooserListAdapter.getUserHandle().getIdentifier() == 10); + return null; + }; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertFalse("Direct share targets were queried on a locked work profile user", + isQueryDirectShareCalledOnWorkProfile[0]); + } + + @Test + public void testWorkTab_workUserLocked_workTargetsShown() { + markWorkProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; + + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + final IChooserWrapper wrapper = (IChooserWrapper) activity; + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertEquals(3, wrapper.getWorkListAdapter().getCount()); } private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { @@ -2703,18 +2713,4 @@ public class UnbundledChooserActivityTest { .getInteger(R.integer.config_chooser_max_targets_per_row)) .thenReturn(targetsPerRow); } - - private SparseArray>> - createShortcutLoaderFactory() { - SparseArray>> shortcutLoaders = - new SparseArray<>(); - ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = - (userHandle, callback) -> { - Pair> pair = - new Pair<>(mock(ShortcutLoader.class), callback); - shortcutLoaders.put(userHandle.getIdentifier(), pair); - return pair.first; - }; - return shortcutLoaders; - } } diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt deleted file mode 100644 index 5756a0cd..00000000 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.shortcuts - -import android.app.prediction.AppPredictor -import android.content.ComponentName -import android.content.Context -import android.content.IntentFilter -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.content.pm.PackageManager.ApplicationInfoFlags -import android.content.pm.ShortcutManager -import android.os.UserHandle -import android.os.UserManager -import androidx.test.filters.SmallTest -import com.android.intentresolver.any -import com.android.intentresolver.chooser.DisplayResolveInfo -import com.android.intentresolver.createAppTarget -import com.android.intentresolver.createShareShortcutInfo -import com.android.intentresolver.createShortcutInfo -import com.android.intentresolver.mock -import com.android.intentresolver.whenever -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.mockito.ArgumentCaptor -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import java.util.concurrent.Executor -import java.util.function.Consumer - -@SmallTest -class ShortcutLoaderTest { - private val appInfo = ApplicationInfo().apply { - enabled = true - flags = 0 - } - private val pm = mock { - whenever(getApplicationInfo(any(), any())).thenReturn(appInfo) - } - private val context = mock { - whenever(packageManager).thenReturn(pm) - whenever(createContextAsUser(any(), anyInt())).thenReturn(this) - } - private val executor = ImmediateExecutor() - private val intentFilter = mock() - private val appPredictor = mock() - private val callback = mock>() - - @Test - fun test_app_predictor_result() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val testSubject = ShortcutLoader( - context, - appPredictor, - UserHandle.of(0), - true, - intentFilter, - executor, - executor, - callback - ) - - testSubject.queryShortcuts(appTargets) - - verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) - verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) - - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) - val matchingAppTarget = createAppTarget(matchingShortcutInfo) - val shortcuts = listOf( - matchingAppTarget, - // mismatching shortcut - createAppTarget( - createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) - ) - ) - appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) - - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) - - val result = resultCaptor.value - assertTrue("An app predictor result is expected", result.isFromAppPredictor) - assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) - assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) - assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) - for (shortcut in result.shortcutsByApp[0].shortcuts) { - assertEquals( - "Wrong AppTarget in the cache", - matchingAppTarget, - result.directShareAppTargetCache[shortcut] - ) - assertEquals( - "Wrong ShortcutInfo in the cache", - matchingShortcutInfo, - result.directShareShortcutInfoCache[shortcut] - ) - } - } - - @Test - fun test_shortcut_manager_result() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) - val shortcutManagerResult = listOf( - ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), - // mismatching shortcut - createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) - ) - val shortcutManager = mock { - whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) - } - whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) - val testSubject = ShortcutLoader( - context, - null, - UserHandle.of(0), - true, - intentFilter, - executor, - executor, - callback - ) - - testSubject.queryShortcuts(appTargets) - - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) - - val result = resultCaptor.value - assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) - assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) - assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) - assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) - for (shortcut in result.shortcutsByApp[0].shortcuts) { - assertTrue( - "AppTargets are not expected the cache of a ShortcutManager result", - result.directShareAppTargetCache.isEmpty() - ) - assertEquals( - "Wrong ShortcutInfo in the cache", - matchingShortcutInfo, - result.directShareShortcutInfoCache[shortcut] - ) - } - } - - @Test - fun test_fallback_to_shortcut_manager() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) - val shortcutManagerResult = listOf( - ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), - // mismatching shortcut - createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) - ) - val shortcutManager = mock { - whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) - } - whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) - val testSubject = ShortcutLoader( - context, - appPredictor, - UserHandle.of(0), - true, - intentFilter, - executor, - executor, - callback - ) - - testSubject.queryShortcuts(appTargets) - - verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) - verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) - appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) - - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) - - val result = resultCaptor.value - assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) - assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) - assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) - assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) - for (shortcut in result.shortcutsByApp[0].shortcuts) { - assertTrue( - "AppTargets are not expected the cache of a ShortcutManager result", - result.directShareAppTargetCache.isEmpty() - ) - assertEquals( - "Wrong ShortcutInfo in the cache", - matchingShortcutInfo, - result.directShareShortcutInfoCache[shortcut] - ) - } - } - - @Test - fun test_do_not_call_services_for_not_running_work_profile() { - testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) - } - - @Test - fun test_do_not_call_services_for_locked_work_profile() { - testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) - } - - @Test - fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { - testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) - } - - @Test - fun test_call_services_for_not_running_main_profile() { - testAlwaysCallSystemForMainProfile(isUserRunning = false) - } - - @Test - fun test_call_services_for_locked_main_profile() { - testAlwaysCallSystemForMainProfile(isUserUnlocked = false) - } - - @Test - fun test_call_services_if_quite_mode_is_enabled_for_main_profile() { - testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) - } - - private fun testDisabledWorkProfileDoNotCallSystem( - isUserRunning: Boolean = true, - isUserUnlocked: Boolean = true, - isQuietModeEnabled: Boolean = false - ) { - val userHandle = UserHandle.of(10) - val userManager = mock { - whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) - whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) - whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) - } - whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); - val appPredictor = mock() - val callback = mock>() - val testSubject = ShortcutLoader( - context, - appPredictor, - userHandle, - false, - intentFilter, - executor, - executor, - callback - ) - - testSubject.queryShortcuts(arrayOf(mock())) - - verify(appPredictor, never()).requestPredictionUpdate() - } - - private fun testAlwaysCallSystemForMainProfile( - isUserRunning: Boolean = true, - isUserUnlocked: Boolean = true, - isQuietModeEnabled: Boolean = false - ) { - val userHandle = UserHandle.of(10) - val userManager = mock { - whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) - whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) - whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) - } - whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); - val appPredictor = mock() - val callback = mock>() - val testSubject = ShortcutLoader( - context, - appPredictor, - userHandle, - true, - intentFilter, - executor, - executor, - callback - ) - - testSubject.queryShortcuts(arrayOf(mock())) - - verify(appPredictor, times(1)).requestPredictionUpdate() - } -} - -private class ImmediateExecutor : Executor { - override fun execute(r: Runnable) { - r.run() - } -} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt deleted file mode 100644 index e0de005d..00000000 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.shortcuts - -import android.app.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 com.android.intentresolver.createAppTarget -import com.android.intentresolver.createShareShortcutInfo -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) - } - } -} -- cgit v1.2.3-59-g8ed1b