diff options
| author | 2022-10-18 22:59:35 -0700 | |
|---|---|---|
| committer | 2022-11-16 15:59:10 -0800 | |
| commit | 7697b5f3b4549749e55acdd930f87bcedb56b422 (patch) | |
| tree | 0ce37c6cafaf1f008e3e6efd7af119cc8e461b3f /java | |
| parent | a469f68342d66e35692488377f351a604553f322 (diff) | |
Extract shortcuts loading logic from ChooserActivity
Extract shortcut loading logic from ChooserActivity into a new class
mostly as-is. Major changes:
- run the logic on a background executor and deliver the result on the
  main thread;
- replace dependencies from ChooserListAdapter with the data it
  provided.
A number of tests thap previusly used
ChooserListAdapter#addServiceResults to provide shortcut results into
the view are updated and re-enabled.
Re-introduction of ag/20236439
Fix: 259462188
Test: manual tests
Test: atest IntentResolverUnitTests
Change-Id: I2555c3e486b9a443c5101bbda33b5b214c959b0f
Diffstat (limited to 'java')
11 files changed, 1174 insertions, 603 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 558dfcf7..d5a0c32c 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -45,12 +45,10 @@ import android.content.IntentSender;  import android.content.IntentSender.SendIntentException;  import android.content.SharedPreferences;  import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo;  import android.content.pm.PackageManager;  import android.content.pm.PackageManager.NameNotFoundException;  import android.content.pm.ResolveInfo;  import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager;  import android.content.res.Configuration;  import android.content.res.Resources;  import android.database.Cursor; @@ -60,7 +58,6 @@ import android.graphics.Insets;  import android.graphics.drawable.Drawable;  import android.metrics.LogMaker;  import android.net.Uri; -import android.os.AsyncTask;  import android.os.Bundle;  import android.os.Environment;  import android.os.Handler; @@ -110,6 +107,7 @@ import com.android.intentresolver.model.AbstractResolverComparator;  import com.android.intentresolver.model.AppPredictionServiceResolverComparator;  import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;  import com.android.intentresolver.shortcuts.AppPredictorFactory; +import com.android.intentresolver.shortcuts.ShortcutLoader;  import com.android.intentresolver.widget.ResolverDrawerLayout;  import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; @@ -136,6 +134,7 @@ import java.util.Map;  import java.util.Objects;  import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors; +import java.util.function.Consumer;  import java.util.function.Supplier;  /** @@ -187,8 +186,8 @@ public class ChooserActivity extends ResolverActivity implements      // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.      // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their      // intermediate data, and then these members can be removed. -    private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache; -    private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache; +    private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>(); +    private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();      public static final int TARGET_TYPE_DEFAULT = 0;      public static final int TARGET_TYPE_CHOOSER_TARGET = 1; @@ -279,8 +278,6 @@ public class ChooserActivity extends ResolverActivity implements      private View mContentView = null; -    private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter = -            new ShortcutToChooserTargetConverter();      private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();      private void setupPreDrawForSharedElementTransition(View v) { @@ -432,11 +429,13 @@ public class ChooserActivity extends ResolverActivity implements          mShouldDisplayLandscape =                  shouldDisplayLandscape(getResources().getConfiguration().orientation);          setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false)); +        IntentFilter targetIntentFilter = getTargetIntentFilter(target);          createProfileRecords(                  new AppPredictorFactory( -                        this, +                        getApplicationContext(),                          target.getStringExtra(Intent.EXTRA_TEXT), -                        getTargetIntentFilter(target))); +                        targetIntentFilter), +                targetIntentFilter);          mPreviewCoordinator = new ChooserContentPreviewCoordinator(                  mBackgroundThreadPoolExecutor, @@ -497,7 +496,6 @@ public class ChooserActivity extends ResolverActivity implements                          getTargetIntent(), getContentResolver(), this::isImageType),                  target.getAction()          ); -        mDirectShareShortcutInfoCache = new HashMap<>();          setEnterSharedElementCallback(new SharedElementCallback() {              @Override @@ -518,20 +516,31 @@ public class ChooserActivity extends ResolverActivity implements          return R.style.Theme_DeviceDefault_Chooser;      } -    private void createProfileRecords(AppPredictorFactory factory) { +    private void createProfileRecords( +            AppPredictorFactory factory, IntentFilter targetIntentFilter) {          UserHandle mainUserHandle = getPersonalProfileUserHandle(); -        createProfileRecord(mainUserHandle, factory); +        createProfileRecord(mainUserHandle, targetIntentFilter, factory);          UserHandle workUserHandle = getWorkProfileUserHandle();          if (workUserHandle != null) { -            createProfileRecord(workUserHandle, factory); +            createProfileRecord(workUserHandle, targetIntentFilter, factory);          }      } -    private void createProfileRecord(UserHandle userHandle, AppPredictorFactory factory) { +    private void createProfileRecord( +            UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {          AppPredictor appPredictor = factory.create(userHandle); +        ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() +                    ? null +                    : createShortcutLoader( +                            getApplicationContext(), +                            appPredictor, +                            userHandle, +                            targetIntentFilter, +                            shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));          mProfileRecords.put( -                userHandle.getIdentifier(), new ProfileRecord(appPredictor)); +                userHandle.getIdentifier(), +                new ProfileRecord(appPredictor, shortcutLoader));      }      @Nullable @@ -539,50 +548,19 @@ public class ChooserActivity extends ResolverActivity implements          return mProfileRecords.get(userHandle.getIdentifier(), null);      } -    private void setupAppPredictorForUser( -            UserHandle userHandle, AppPredictor.Callback appPredictorCallback) { -        AppPredictor appPredictor = getAppPredictor(userHandle); -        if (appPredictor == null) { -            return; -        } -        mDirectShareAppTargetCache = new HashMap<>(); -        appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback); -    } - -    private AppPredictor.Callback createAppPredictorCallback( -            ChooserListAdapter chooserListAdapter) { -        return resultList -> { -            if (isFinishing() || isDestroyed()) { -                return; -            } -            if (chooserListAdapter.getCount() == 0) { -                return; -            } -            if (resultList.isEmpty() -                    && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { -                // APS may be disabled, so try querying targets ourselves. -                queryDirectShareTargets(chooserListAdapter, true); -                return; -            } -            final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos = -                    new ArrayList<>(); - -            List<AppTarget> shortcutResults = new ArrayList<>(); -            for (AppTarget appTarget : resultList) { -                if (appTarget.getShortcutInfo() == null) { -                    continue; -                } -                shortcutResults.add(appTarget); -            } -            resultList = shortcutResults; -            for (AppTarget appTarget : resultList) { -                shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo( -                        appTarget.getShortcutInfo(), -                        new ComponentName( -                                appTarget.getPackageName(), appTarget.getClassName()))); -            } -            sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList); -        }; +    @VisibleForTesting +    protected ShortcutLoader createShortcutLoader( +            Context context, +            AppPredictor appPredictor, +            UserHandle userHandle, +            IntentFilter targetIntentFilter, +            Consumer<ShortcutLoader.Result> callback) { +        return new ShortcutLoader( +                context, +                appPredictor, +                userHandle, +                targetIntentFilter, +                callback);      }      static SharedPreferences getPinnedSharedPrefs(Context context) { @@ -1482,147 +1460,6 @@ public class ChooserActivity extends ResolverActivity implements          }      } -    @VisibleForTesting -    protected void queryDirectShareTargets( -                ChooserListAdapter adapter, boolean skipAppPredictionService) { -        ProfileRecord record = getProfileRecord(adapter.getUserHandle()); -        if (record == null) { -            return; -        } - -        record.loadingStartTime = SystemClock.elapsedRealtime(); - -        UserHandle userHandle = adapter.getUserHandle(); -        if (!skipAppPredictionService) { -            if (record.appPredictor != null) { -                record.appPredictor.requestPredictionUpdate(); -                return; -            } -        } -        // Default to just querying ShortcutManager if AppPredictor not present. -        final IntentFilter filter = getTargetIntentFilter(); -        if (filter == null) { -            return; -        } - -        AsyncTask.execute(() -> { -            Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); -            ShortcutManager sm = (ShortcutManager) selectedProfileContext -                    .getSystemService(Context.SHORTCUT_SERVICE); -            List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter); -            sendShareShortcutInfoList(resultList, adapter, null); -        }); -    } - -    /** -     * Returns {@code false} if {@code userHandle} is the work profile and it's either -     * in quiet mode or not running. -     */ -    private boolean shouldQueryShortcutManager(UserHandle userHandle) { -        if (!shouldShowTabs()) { -            return true; -        } -        if (!getWorkProfileUserHandle().equals(userHandle)) { -            return true; -        } -        if (!isUserRunning(userHandle)) { -            return false; -        } -        if (!isUserUnlocked(userHandle)) { -            return false; -        } -        if (isQuietModeEnabled(userHandle)) { -            return false; -        } -        return true; -    } - -    private void sendShareShortcutInfoList( -                List<ShortcutManager.ShareShortcutInfo> resultList, -                ChooserListAdapter chooserListAdapter, -                @Nullable List<AppTarget> appTargets) { -        if (appTargets != null && appTargets.size() != resultList.size()) { -            throw new RuntimeException("resultList and appTargets must have the same size." -                    + " resultList.size()=" + resultList.size() -                    + " appTargets.size()=" + appTargets.size()); -        } -        UserHandle userHandle = chooserListAdapter.getUserHandle(); -        Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */); -        for (int i = resultList.size() - 1; i >= 0; i--) { -            final String packageName = resultList.get(i).getTargetComponent().getPackageName(); -            if (!isPackageEnabled(selectedProfileContext, packageName)) { -                resultList.remove(i); -                if (appTargets != null) { -                    appTargets.remove(i); -                } -            } -        } - -        // If |appTargets| is not null, results are from AppPredictionService and already sorted. -        final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER : -                TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); - -        // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path -        // for direct share targets. After ShareSheet is refactored we should use the -        // ShareShortcutInfos directly. -        List<ServiceResultInfo> resultRecords = new ArrayList<>(); -        for (DisplayResolveInfo displayResolveInfo : chooserListAdapter.getDisplayResolveInfos()) { -            List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = -                    filterShortcutsByTargetComponentName( -                            resultList, displayResolveInfo.getResolvedComponentName()); -            if (matchingShortcuts.isEmpty()) { -                continue; -            } - -            List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter -                    .convertToChooserTarget( -                        matchingShortcuts, -                        resultList, -                        appTargets, -                        mDirectShareAppTargetCache, -                        mDirectShareShortcutInfoCache); - -            ServiceResultInfo resultRecord = new ServiceResultInfo( -                    displayResolveInfo, chooserTargets); -            resultRecords.add(resultRecord); -        } - -        runOnUiThread(() -> { -            if (!isDestroyed()) { -                onShortcutsLoaded(chooserListAdapter, shortcutType, resultRecords); -            } -        }); -    } - -    private List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName( -            List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) { -        List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>(); -        for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { -            if (requiredTarget.equals(shortcut.getTargetComponent())) { -                matchingShortcuts.add(shortcut); -            } -        } -        return matchingShortcuts; -    } - -    private boolean isPackageEnabled(Context context, String packageName) { -        if (TextUtils.isEmpty(packageName)) { -            return false; -        } -        ApplicationInfo appInfo; -        try { -            appInfo = context.getPackageManager().getApplicationInfo(packageName, 0); -        } catch (NameNotFoundException e) { -            return false; -        } - -        if (appInfo != null && appInfo.enabled -                && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) { -            return true; -        } -        return false; -    } -      private void logDirectShareTargetReceived(int logCategory, UserHandle forUser) {          ProfileRecord profileRecord = getProfileRecord(forUser);          if (profileRecord == null) { @@ -1656,7 +1493,7 @@ public class ChooserActivity extends ResolverActivity implements                      Log.d(TAG, "Action to be updated is " + targetIntent.getAction());                  }              } else if (DEBUG) { -                Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo"); +                Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");              }          }          mIsSuccessfullySelected = true; @@ -1701,9 +1538,6 @@ public class ChooserActivity extends ResolverActivity implements          if (directShareAppPredictor == null) {              return;          } -        if (!targetInfo.isChooserTargetInfo()) { -            return; -        }          AppTarget appTarget = targetInfo.getDirectShareAppTarget();          if (appTarget != null) {              // This is a direct share click that was provided by the APS @@ -1820,11 +1654,6 @@ public class ChooserActivity extends ResolverActivity implements          ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,                  initialIntents, rList, filterLastUsed,                  createListController(userHandle)); -        if (!ActivityManager.isLowRamDeviceStatic()) { -            AppPredictor.Callback appPredictorCallback = -                    createAppPredictorCallback(chooserListAdapter); -            setupAppPredictorForUser(userHandle, appPredictorCallback); -        }          return new ChooserGridAdapter(chooserListAdapter);      } @@ -2111,42 +1940,41 @@ public class ChooserActivity extends ResolverActivity implements      }      private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { -        // don't support direct share on low ram devices -        if (ActivityManager.isLowRamDeviceStatic()) { +        UserHandle userHandle = chooserListAdapter.getUserHandle(); +        ProfileRecord record = getProfileRecord(userHandle); +        if (record == null) {              return;          } - -        // no need to query direct share for work profile when its locked or disabled -        if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) { +        if (record.shortcutLoader == null) {              return;          } - -        if (DEBUG) { -            Log.d(TAG, "querying direct share targets from ShortcutManager"); -        } - -        queryDirectShareTargets(chooserListAdapter, false); +        record.loadingStartTime = SystemClock.elapsedRealtime(); +        record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());      } -    @VisibleForTesting      @MainThread -    protected void onShortcutsLoaded( -            ChooserListAdapter adapter, int targetType, List<ServiceResultInfo> resultInfos) { -        UserHandle userHandle = adapter.getUserHandle(); +    private void onShortcutsLoaded( +            UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {          if (DEBUG) {              Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);          } -        for (ServiceResultInfo resultInfo : resultInfos) { -            if (resultInfo.resultTargets != null) { +        mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache); +        mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache); +        ChooserListAdapter adapter = +                mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle); +        if (adapter != null) { +            for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {                  adapter.addServiceResults( -                        resultInfo.originalTarget, -                        resultInfo.resultTargets, -                        targetType, -                        emptyIfNull(mDirectShareShortcutInfoCache), -                        emptyIfNull(mDirectShareAppTargetCache)); +                        resultInfo.appTarget, +                        resultInfo.shortcuts, +                        shortcutsResult.isFromAppPredictor +                                ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE +                                : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, +                        mDirectShareShortcutInfoCache, +                        mDirectShareAppTargetCache);              } +            adapter.completeServiceTargetLoading();          } -        adapter.completeServiceTargetLoading();          logDirectShareTargetReceived(                  MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER, @@ -2156,24 +1984,6 @@ public class ChooserActivity extends ResolverActivity implements          getChooserActivityLogger().logSharesheetDirectLoadComplete();      } -    @VisibleForTesting -    protected boolean isUserRunning(UserHandle userHandle) { -        UserManager userManager = getSystemService(UserManager.class); -        return userManager.isUserRunning(userHandle); -    } - -    @VisibleForTesting -    protected boolean isUserUnlocked(UserHandle userHandle) { -        UserManager userManager = getSystemService(UserManager.class); -        return userManager.isUserUnlocked(userHandle); -    } - -    @VisibleForTesting -    protected boolean isQuietModeEnabled(UserHandle userHandle) { -        UserManager userManager = getSystemService(UserManager.class); -        return userManager.isQuietModeEnabled(userHandle); -    } -      private void setupScrollListener() {          if (mResolverDrawerLayout == null) {              return; @@ -3211,16 +3021,6 @@ public class ChooserActivity extends ResolverActivity implements          }      } -    static class ServiceResultInfo { -        public final DisplayResolveInfo originalTarget; -        public final List<ChooserTarget> resultTargets; - -        ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt) { -            originalTarget = ot; -            resultTargets = rt; -        } -    } -      static class ChooserTargetRankingInfo {          public final List<AppTarget> scores;          public final UserHandle userHandle; @@ -3396,22 +3196,28 @@ public class ChooserActivity extends ResolverActivity implements          getChooserActivityLogger().logSharesheetProfileChanged();      } -    private static <K, V> Map<K, V> emptyIfNull(@Nullable Map<K, V> map) { -        return map == null ? Collections.emptyMap() : map; -    } -      private static class ProfileRecord {          /** The {@link AppPredictor} for this profile, if any. */          @Nullable          public final AppPredictor appPredictor; - +        /** +         * null if we should not load shortcuts. +         */ +        @Nullable +        public final ShortcutLoader shortcutLoader;          public long loadingStartTime; -        ProfileRecord(@Nullable AppPredictor appPredictor) { +        private ProfileRecord( +                @Nullable AppPredictor appPredictor, +                @Nullable ShortcutLoader shortcutLoader) {              this.appPredictor = appPredictor; +            this.shortcutLoader = shortcutLoader;          }          public void destroy() { +            if (shortcutLoader != null) { +                shortcutLoader.destroy(); +            }              if (appPredictor != null) {                  appPredictor.destroy();              } diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java new file mode 100644 index 00000000..1cfa2c8d --- /dev/null +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.shortcuts; + +import android.app.ActivityManager; +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.content.ComponentName; +import android.content.Context; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.os.AsyncTask; +import android.os.UserHandle; +import android.os.UserManager; +import android.service.chooser.ChooserTarget; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.android.intentresolver.chooser.DisplayResolveInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. + * <p> + *     A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut + * updates. The shortcut loading is triggered by the {@link #queryShortcuts(DisplayResolveInfo[])}, + * the processing will happen on the {@link #mBackgroundExecutor} and the result is delivered + * through the {@link #mCallback} on the {@link #mCallbackExecutor}, the main thread. + * </p> + * <p> + *    The current version does not improve on the legacy in a way that it does not guarantee that + * each invocation of the {@link #queryShortcuts(DisplayResolveInfo[])} will be matched by an + * invocation of the callback (there are early terminations of the flow). Also, the fetched + * shortcuts would be matched against the last known input, i.e. two invocations of + * {@link #queryShortcuts(DisplayResolveInfo[])} may result in two callbacks where shortcuts are + * processed against the latest input. + * </p> + */ +public class ShortcutLoader { +    private static final String TAG = "ChooserActivity"; + +    private static final Request NO_REQUEST = new Request(new DisplayResolveInfo[0]); + +    private final Context mContext; +    @Nullable +    private final AppPredictorProxy mAppPredictor; +    private final UserHandle mUserHandle; +    @Nullable +    private final IntentFilter mTargetIntentFilter; +    private final Executor mBackgroundExecutor; +    private final Executor mCallbackExecutor; +    private final boolean mIsPersonalProfile; +    private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter = +            new ShortcutToChooserTargetConverter(); +    private final UserManager mUserManager; +    private final AtomicReference<Consumer<Result>> mCallback = new AtomicReference<>(); +    private final AtomicReference<Request> mActiveRequest = new AtomicReference<>(NO_REQUEST); + +    @Nullable +    private final AppPredictor.Callback mAppPredictorCallback; + +    @MainThread +    public ShortcutLoader( +            Context context, +            @Nullable AppPredictor appPredictor, +            UserHandle userHandle, +            @Nullable IntentFilter targetIntentFilter, +            Consumer<Result> callback) { +        this( +                context, +                appPredictor == null ? null : new AppPredictorProxy(appPredictor), +                userHandle, +                userHandle.equals(UserHandle.of(ActivityManager.getCurrentUser())), +                targetIntentFilter, +                AsyncTask.SERIAL_EXECUTOR, +                context.getMainExecutor(), +                callback); +    } + +    @VisibleForTesting +    ShortcutLoader( +            Context context, +            @Nullable AppPredictorProxy appPredictor, +            UserHandle userHandle, +            boolean isPersonalProfile, +            @Nullable IntentFilter targetIntentFilter, +            Executor backgroundExecutor, +            Executor callbackExecutor, +            Consumer<Result> callback) { +        mContext = context; +        mAppPredictor = appPredictor; +        mUserHandle = userHandle; +        mTargetIntentFilter = targetIntentFilter; +        mBackgroundExecutor = backgroundExecutor; +        mCallbackExecutor = callbackExecutor; +        mCallback.set(callback); +        mIsPersonalProfile = isPersonalProfile; +        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + +        if (mAppPredictor != null) { +            mAppPredictorCallback = createAppPredictorCallback(); +            mAppPredictor.registerPredictionUpdates(mCallbackExecutor, mAppPredictorCallback); +        } else { +            mAppPredictorCallback = null; +        } +    } + +    /** +     * Unsubscribe from app predictor if one was provided. +     */ +    @MainThread +    public void destroy() { +        if (mCallback.getAndSet(null) != null) { +            if (mAppPredictor != null) { +                mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback); +            } +        } +    } + +    private boolean isDestroyed() { +        return mCallback.get() == null; +    } + +    /** +     * Set new resolved targets. This will trigger shortcut loading. +     * @param appTargets a collection of application targets a loaded set of shortcuts will be +     *                   grouped against +     */ +    @MainThread +    public void queryShortcuts(DisplayResolveInfo[] appTargets) { +        if (isDestroyed()) { +            return; +        } +        mActiveRequest.set(new Request(appTargets)); +        mBackgroundExecutor.execute(this::loadShortcuts); +    } + +    @WorkerThread +    private void loadShortcuts() { +        // no need to query direct share for work profile when its locked or disabled +        if (!shouldQueryDirectShareTargets()) { +            return; +        } +        Log.d(TAG, "querying direct share targets"); +        queryDirectShareTargets(false); +    } + +    @WorkerThread +    private void queryDirectShareTargets(boolean skipAppPredictionService) { +        if (isDestroyed()) { +            return; +        } +        if (!skipAppPredictionService && mAppPredictor != null) { +            mAppPredictor.requestPredictionUpdate(); +            return; +        } +        // Default to just querying ShortcutManager if AppPredictor not present. +        if (mTargetIntentFilter == null) { +            return; +        } + +        Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); +        ShortcutManager sm = (ShortcutManager) selectedProfileContext +                .getSystemService(Context.SHORTCUT_SERVICE); +        List<ShortcutManager.ShareShortcutInfo> shortcuts = +                sm.getShareTargets(mTargetIntentFilter); +        sendShareShortcutInfoList(shortcuts, false, null); +    } + +    private AppPredictor.Callback createAppPredictorCallback() { +        return appPredictorTargets -> { +            if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { +                // APS may be disabled, so try querying targets ourselves. +                queryDirectShareTargets(true); +                return; +            } + +            final List<ShortcutManager.ShareShortcutInfo> shortcuts = new ArrayList<>(); +            List<AppTarget> shortcutResults = new ArrayList<>(); +            for (AppTarget appTarget : appPredictorTargets) { +                if (appTarget.getShortcutInfo() == null) { +                    continue; +                } +                shortcutResults.add(appTarget); +            } +            appPredictorTargets = shortcutResults; +            for (AppTarget appTarget : appPredictorTargets) { +                shortcuts.add(new ShortcutManager.ShareShortcutInfo( +                        appTarget.getShortcutInfo(), +                        new ComponentName(appTarget.getPackageName(), appTarget.getClassName()))); +            } +            sendShareShortcutInfoList(shortcuts, true, appPredictorTargets); +        }; +    } + +    @WorkerThread +    private void sendShareShortcutInfoList( +            List<ShortcutManager.ShareShortcutInfo> shortcuts, +            boolean isFromAppPredictor, +            @Nullable List<AppTarget> appPredictorTargets) { +        if (appPredictorTargets != null && appPredictorTargets.size() != shortcuts.size()) { +            throw new RuntimeException("resultList and appTargets must have the same size." +                    + " resultList.size()=" + shortcuts.size() +                    + " appTargets.size()=" + appPredictorTargets.size()); +        } +        Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */); +        for (int i = shortcuts.size() - 1; i >= 0; i--) { +            final String packageName = shortcuts.get(i).getTargetComponent().getPackageName(); +            if (!isPackageEnabled(selectedProfileContext, packageName)) { +                shortcuts.remove(i); +                if (appPredictorTargets != null) { +                    appPredictorTargets.remove(i); +                } +            } +        } + +        HashMap<ChooserTarget, AppTarget> directShareAppTargetCache = new HashMap<>(); +        HashMap<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache = new HashMap<>(); +        // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path +        // for direct share targets. After ShareSheet is refactored we should use the +        // ShareShortcutInfos directly. +        final DisplayResolveInfo[] appTargets = mActiveRequest.get().appTargets; +        List<ShortcutResultInfo> resultRecords = new ArrayList<>(); +        for (DisplayResolveInfo displayResolveInfo : appTargets) { +            List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = +                    filterShortcutsByTargetComponentName( +                            shortcuts, displayResolveInfo.getResolvedComponentName()); +            if (matchingShortcuts.isEmpty()) { +                continue; +            } + +            List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter +                    .convertToChooserTarget( +                            matchingShortcuts, +                            shortcuts, +                            appPredictorTargets, +                            directShareAppTargetCache, +                            directShareShortcutInfoCache); + +            ShortcutResultInfo resultRecord = +                    new ShortcutResultInfo(displayResolveInfo, chooserTargets); +            resultRecords.add(resultRecord); +        } + +        postReport( +                new Result( +                        isFromAppPredictor, +                        appTargets, +                        resultRecords.toArray(new ShortcutResultInfo[0]), +                        directShareAppTargetCache, +                        directShareShortcutInfoCache)); +    } + +    private void postReport(Result result) { +        mCallbackExecutor.execute(() -> report(result)); +    } + +    @MainThread +    private void report(Result result) { +        Consumer<Result> callback = mCallback.get(); +        if (callback != null) { +            callback.accept(result); +        } +    } + +    /** +     * Returns {@code false} if {@code userHandle} is the work profile and it's either +     * in quiet mode or not running. +     */ +    private boolean shouldQueryDirectShareTargets() { +        return mIsPersonalProfile || isProfileActive(); +    } + +    @VisibleForTesting +    protected boolean isProfileActive() { +        return mUserManager.isUserRunning(mUserHandle) +                && mUserManager.isUserUnlocked(mUserHandle) +                && !mUserManager.isQuietModeEnabled(mUserHandle); +    } + +    private static boolean isPackageEnabled(Context context, String packageName) { +        if (TextUtils.isEmpty(packageName)) { +            return false; +        } +        ApplicationInfo appInfo; +        try { +            appInfo = context.getPackageManager().getApplicationInfo( +                    packageName, +                    ApplicationInfoFlags.of(PackageManager.GET_META_DATA)); +        } catch (NameNotFoundException e) { +            return false; +        } + +        return appInfo != null && appInfo.enabled +                && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0; +    } + +    private static List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName( +            List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) { +        List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>(); +        for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) { +            if (requiredTarget.equals(shortcut.getTargetComponent())) { +                matchingShortcuts.add(shortcut); +            } +        } +        return matchingShortcuts; +    } + +    private static class Request { +        public final DisplayResolveInfo[] appTargets; + +        Request(DisplayResolveInfo[] targets) { +            appTargets = targets; +        } +    } + +    /** +     * Resolved shortcuts with corresponding app targets. +     */ +    public static class Result { +        public final boolean isFromAppPredictor; +        /** +         * Input app targets (see {@link ShortcutLoader#queryShortcuts(DisplayResolveInfo[])} the +         * shortcuts were process against. +         */ +        public final DisplayResolveInfo[] appTargets; +        /** +         * Shortcuts grouped by app target. +         */ +        public final ShortcutResultInfo[] shortcutsByApp; +        public final Map<ChooserTarget, AppTarget> directShareAppTargetCache; +        public final Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache; + +        @VisibleForTesting +        public Result( +                boolean isFromAppPredictor, +                DisplayResolveInfo[] appTargets, +                ShortcutResultInfo[] shortcutsByApp, +                Map<ChooserTarget, AppTarget> directShareAppTargetCache, +                Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) { +            this.isFromAppPredictor = isFromAppPredictor; +            this.appTargets = appTargets; +            this.shortcutsByApp = shortcutsByApp; +            this.directShareAppTargetCache = directShareAppTargetCache; +            this.directShareShortcutInfoCache = directShareShortcutInfoCache; +        } +    } + +    /** +     * Shortcuts grouped by app. +     */ +    public static class ShortcutResultInfo { +        public final DisplayResolveInfo appTarget; +        public final List<ChooserTarget> shortcuts; + +        public ShortcutResultInfo(DisplayResolveInfo appTarget, List<ChooserTarget> shortcuts) { +            this.appTarget = appTarget; +            this.shortcuts = shortcuts; +        } +    } + +    /** +     * A wrapper around AppPredictor to facilitate unit-testing. +     */ +    @VisibleForTesting +    public static class AppPredictorProxy { +        private final AppPredictor mAppPredictor; + +        AppPredictorProxy(AppPredictor appPredictor) { +            mAppPredictor = appPredictor; +        } + +        /** +         * {@link AppPredictor#registerPredictionUpdates} +         */ +        public void registerPredictionUpdates( +                Executor callbackExecutor, AppPredictor.Callback callback) { +            mAppPredictor.registerPredictionUpdates(callbackExecutor, callback); +        } + +        /** +         * {@link AppPredictor#unregisterPredictionUpdates} +         */ +        public void unregisterPredictionUpdates(AppPredictor.Callback callback) { +            mAppPredictor.unregisterPredictionUpdates(callback); +        } + +        /** +         * {@link AppPredictor#requestPredictionUpdate} +         */ +        public void requestPredictionUpdate() { +            mAppPredictor.requestPredictionUpdate(); +        } +    } +} diff --git a/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java index ac4270d3..a37d6558 100644 --- a/java/src/com/android/intentresolver/ShortcutToChooserTargetConverter.java +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java @@ -14,7 +14,7 @@   * limitations under the License.   */ -package com.android.intentresolver; +package com.android.intentresolver.shortcuts;  import android.annotation.NonNull;  import android.annotation.Nullable; diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml index b220d3ea..306eccb9 100644 --- a/java/tests/AndroidManifest.xml +++ b/java/tests/AndroidManifest.xml @@ -25,7 +25,7 @@      <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>      <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> -    <application> +    <application android:name="com.android.intentresolver.TestApplication">          <uses-library android:name="android.test.runner" />          <activity android:name="com.android.intentresolver.ChooserWrapperActivity" />          <activity android:name="com.android.intentresolver.ResolverWrapperActivity" /> diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index e474938b..dd78b69e 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -24,15 +24,17 @@ import android.content.res.Resources;  import android.database.Cursor;  import android.graphics.Bitmap;  import android.os.UserHandle; -import android.util.Pair;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.shortcuts.ShortcutLoader;  import com.android.internal.logging.MetricsLogger;  import java.util.List; -import java.util.function.BiFunction; +import java.util.function.Consumer;  import java.util.function.Function; +import kotlin.jvm.functions.Function2; +  /**   * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing.   * We cannot directly mock the activity created since instrumentation creates it, so instead we use @@ -51,10 +53,8 @@ public class ChooserActivityOverrideData {      @SuppressWarnings("Since15")      public Function<PackageManager, PackageManager> createPackageManager;      public Function<TargetInfo, Boolean> onSafelyStartCallback; -    public Function<ChooserListAdapter, Void> onQueryDirectShareTargets; -    public BiFunction< -            IChooserWrapper, ChooserListAdapter, Pair<Integer, ChooserActivity.ServiceResultInfo[]>> -            directShareTargets; +    public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> +            shortcutLoaderFactory = (userHandle, callback) -> null;      public ResolverListController resolverListController;      public ResolverListController workResolverListController;      public Boolean isVoiceInteraction; @@ -69,15 +69,11 @@ public class ChooserActivityOverrideData {      public UserHandle workProfileUserHandle;      public boolean hasCrossProfileIntents;      public boolean isQuietModeEnabled; -    public boolean isWorkProfileUserRunning; -    public boolean isWorkProfileUserUnlocked;      public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector;      public PackageManager packageManager;      public void reset() {          onSafelyStartCallback = null; -        onQueryDirectShareTargets = null; -        directShareTargets = null;          isVoiceInteraction = null;          createPackageManager = null;          previewThumbnail = null; @@ -93,8 +89,6 @@ public class ChooserActivityOverrideData {          workProfileUserHandle = null;          hasCrossProfileIntents = true;          isQuietModeEnabled = false; -        isWorkProfileUserRunning = true; -        isWorkProfileUserUnlocked = true;          packageManager = null;          multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() {              @Override @@ -114,6 +108,7 @@ public class ChooserActivityOverrideData {                  isQuietModeEnabled = enabled;              }          }; +        shortcutLoaderFactory = ((userHandle, resultConsumer) -> null);      }      private ChooserActivityOverrideData() {} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 8c7c28bb..6b74fcd4 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -19,11 +19,13 @@ package com.android.intentresolver;  import static org.mockito.Mockito.when;  import android.annotation.Nullable; +import android.app.prediction.AppPredictor;  import android.app.usage.UsageStatsManager;  import android.content.ComponentName;  import android.content.ContentResolver;  import android.content.Context;  import android.content.Intent; +import android.content.IntentFilter;  import android.content.pm.PackageManager;  import android.content.pm.ResolveInfo;  import android.content.res.Resources; @@ -31,7 +33,6 @@ import android.database.Cursor;  import android.graphics.Bitmap;  import android.net.Uri;  import android.os.UserHandle; -import android.util.Pair;  import android.util.Size;  import com.android.intentresolver.AbstractMultiProfilePagerAdapter; @@ -44,11 +45,12 @@ import com.android.intentresolver.ResolverListController;  import com.android.intentresolver.chooser.DisplayResolveInfo;  import com.android.intentresolver.chooser.NotSelectableTargetInfo;  import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.shortcuts.ShortcutLoader;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import java.util.Arrays;  import java.util.List; +import java.util.function.Consumer;  /**   * Simple wrapper around chooser activity to be able to initiate it under test. For more @@ -256,41 +258,18 @@ public class ChooserWrapperActivity      }      @Override -    protected void queryDirectShareTargets( -            ChooserListAdapter adapter, boolean skipAppPredictionService) { -        if (sOverrides.directShareTargets != null) { -            Pair<Integer, ServiceResultInfo[]> result = -                    sOverrides.directShareTargets.apply(this, adapter); -            // Imitate asynchronous shortcut loading -            getMainExecutor().execute( -                    () -> onShortcutsLoaded( -                            adapter, result.first, Arrays.asList(result.second))); -            return; -        } -        if (sOverrides.onQueryDirectShareTargets != null) { -            sOverrides.onQueryDirectShareTargets.apply(adapter); -        } -        super.queryDirectShareTargets(adapter, skipAppPredictionService); -    } - -    @Override -    protected boolean isQuietModeEnabled(UserHandle userHandle) { -        return sOverrides.isQuietModeEnabled; -    } - -    @Override -    protected boolean isUserRunning(UserHandle userHandle) { -        if (userHandle.equals(UserHandle.SYSTEM)) { -            return super.isUserRunning(userHandle); -        } -        return sOverrides.isWorkProfileUserRunning; -    } - -    @Override -    protected boolean isUserUnlocked(UserHandle userHandle) { -        if (userHandle.equals(UserHandle.SYSTEM)) { -            return super.isUserUnlocked(userHandle); +    protected ShortcutLoader createShortcutLoader( +            Context context, +            AppPredictor appPredictor, +            UserHandle userHandle, +            IntentFilter targetIntentFilter, +            Consumer<ShortcutLoader.Result> callback) { +        ShortcutLoader shortcutLoader = +                sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); +        if (shortcutLoader != null) { +            return shortcutLoader;          } -        return sOverrides.isWorkProfileUserUnlocked; +        return super.createShortcutLoader( +                context, appPredictor, userHandle, targetIntentFilter, callback);      }  } diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java index f81cd023..0d44e147 100644 --- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java +++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java @@ -25,6 +25,8 @@ import android.os.UserHandle;  import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;  import com.android.intentresolver.chooser.DisplayResolveInfo; +import java.util.concurrent.Executor; +  /**   * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in   * order to expose the internals for override/inspection. Implementations should apply the overrides @@ -41,4 +43,5 @@ public interface IChooserWrapper {              @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter);      UserHandle getCurrentUserHandle();      ChooserActivityLogger getChooserActivityLogger(); +    Executor getMainExecutor();  } diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt new file mode 100644 index 00000000..849cfbab --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestApplication.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver + +import android.app.Application +import android.content.Context +import android.os.UserHandle + +class TestApplication : Application() { + +    // return the current context as a work profile doesn't really exist in these tests +    override fun createContextAsUser(user: UserHandle, flags: Int): Context = this +}
\ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 7c304284..da72a749 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -38,7 +38,6 @@ import static com.android.intentresolver.MatcherUtils.first;  import static com.google.common.truth.Truth.assertThat; -import static junit.framework.Assert.assertFalse;  import static junit.framework.Assert.assertNull;  import static org.hamcrest.CoreMatchers.allOf; @@ -83,6 +82,7 @@ import android.os.UserHandle;  import android.provider.DeviceConfig;  import android.service.chooser.ChooserTarget;  import android.util.Pair; +import android.util.SparseArray;  import android.view.View;  import androidx.annotation.CallSuper; @@ -93,9 +93,9 @@ import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;  import androidx.test.platform.app.InstrumentationRegistry;  import androidx.test.rule.ActivityTestRule; -import com.android.intentresolver.ChooserActivity.ServiceResultInfo;  import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;  import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.shortcuts.ShortcutLoader;  import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;  import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -118,6 +118,7 @@ import java.util.Collection;  import java.util.HashMap;  import java.util.List;  import java.util.Map; +import java.util.function.Consumer;  import java.util.function.Function;  /** @@ -1279,7 +1280,7 @@ public class UnbundledChooserActivityTest {      }      // This test is too long and too slow and should not be taken as an example for future tests. -    @Test @Ignore +    @Test      public void testDirectTargetSelectionLogging() {          Intent sendIntent = createSendTextIntent();          // We need app targets for direct targets to get displayed @@ -1298,37 +1299,55 @@ public class UnbundledChooserActivityTest {          // Set up resources          MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;          ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + +        // create test shortcut loader factory, remember loaders and their callbacks +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                createShortcutLoaderFactory();          // Start activity          final IChooserWrapper activity = (IChooserWrapper)                  mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); +        waitForIdle(); -        // Insert the direct share target -        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); -        directShareToShortcutInfos.put(serviceTargets.get(0), null); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> activity.getAdapter().addServiceResults( -                        activity.createTestDisplayResolveInfo(sendIntent, -                                ri, -                                "testLabel", -                                "testInfo", -                                sendIntent, -                                /* resolveInfoPresentationGetter */ null), -                        serviceTargets, -                        TARGET_TYPE_CHOOSER_TARGET, -                        directShareToShortcutInfos, -                        /* directShareToAppTargets */ null) +        // verify that ShortcutLoader was queried +        ArgumentCaptor<DisplayResolveInfo[]> appTargets = +                ArgumentCaptor.forClass(DisplayResolveInfo[].class); +        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + +        // send shortcuts +        assertThat( +                "Wrong number of app targets", +                appTargets.getValue().length, +                is(resolvedComponentInfos.size())); +        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); +        ShortcutLoader.Result result = new ShortcutLoader.Result( +                true, +                appTargets.getValue(), +                new ShortcutLoader.ShortcutResultInfo[] { +                        new ShortcutLoader.ShortcutResultInfo( +                                appTargets.getValue()[0], +                                serviceTargets +                        ) +                }, +                new HashMap<>(), +                new HashMap<>()          ); +        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); +        waitForIdle(); -        assertThat("Chooser should have 3 targets (2 apps, 1 direct)", -                activity.getAdapter().getCount(), is(3)); -        assertThat("Chooser should have exactly one selectable direct target", -                activity.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat("The resolver info must match the resolver info used to create the target", -                activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); +        final ChooserListAdapter activeAdapter = activity.getAdapter(); +        assertThat( +                "Chooser should have 3 targets (2 apps, 1 direct)", +                activeAdapter.getCount(), +                is(3)); +        assertThat( +                "Chooser should have exactly one selectable direct target", +                activeAdapter.getSelectableServiceTargetCount(), +                is(1)); +        assertThat( +                "The resolver info must match the resolver info used to create the target", +                activeAdapter.getItem(0).getResolveInfo(), +                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));          // Click on the direct target          String name = serviceTargets.get(0).getTitle().toString(); @@ -1336,23 +1355,29 @@ public class UnbundledChooserActivityTest {                  .perform(click());          waitForIdle(); -        // Currently we're seeing 3 invocations -        //      1. ChooserActivity.onCreate() -        //      2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() -        //      3. ChooserActivity.startSelected -- which is the one we're after -        verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); -        assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), +        // Currently we're seeing 4 invocations +        //      1. ChooserActivity.logActionShareWithPreview() +        //      2. ChooserActivity.onCreate() +        //      3. ChooserActivity.logDirectShareTargetReceived() +        //      4. ChooserActivity.startSelected -- which is the one we're after +        verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture()); +        LogMaker selectionLog = logMakerCaptor.getAllValues().get(3); +        assertThat( +                selectionLog.getCategory(),                  is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); -        String hashedName = (String) logMakerCaptor -                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME); -        assertThat("Hash is not predictable but must be obfuscated", +        String hashedName = (String) selectionLog.getTaggedData( +                MetricsEvent.FIELD_HASHED_TARGET_NAME); +        assertThat( +                "Hash is not predictable but must be obfuscated",                  hashedName, is(not(name))); -        assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor -                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1)); +        assertThat( +                "The packages shouldn't match for app target and direct target", +                selectionLog.getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), +                is(-1));      }      // This test is too long and too slow and should not be taken as an example for future tests. -    @Test @Ignore +    @Test      public void testDirectTargetLoggingWithRankedAppTarget() {          Intent sendIntent = createSendTextIntent();          // We need app targets for direct targets to get displayed @@ -1371,38 +1396,57 @@ public class UnbundledChooserActivityTest {          // Set up resources          MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;          ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + +        // create test shortcut loader factory, remember loaders and their callbacks +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                createShortcutLoaderFactory();          // Start activity          final IChooserWrapper activity = (IChooserWrapper)                  mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); +        waitForIdle(); -        // Insert the direct share target -        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); -        directShareToShortcutInfos.put(serviceTargets.get(0), null); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> activity.getAdapter().addServiceResults( -                        activity.createTestDisplayResolveInfo(sendIntent, -                                ri, -                                "testLabel", -                                "testInfo", -                                sendIntent, -                                /* resolveInfoPresentationGetter */ null), -                        serviceTargets, -                        TARGET_TYPE_CHOOSER_TARGET, -                        directShareToShortcutInfos, -                        /* directShareToAppTargets */ null) +        // verify that ShortcutLoader was queried +        ArgumentCaptor<DisplayResolveInfo[]> appTargets = +                ArgumentCaptor.forClass(DisplayResolveInfo[].class); +        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + +        // send shortcuts +        assertThat( +                "Wrong number of app targets", +                appTargets.getValue().length, +                is(resolvedComponentInfos.size())); +        List<ChooserTarget> serviceTargets = createDirectShareTargets( +                1, +                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); +        ShortcutLoader.Result result = new ShortcutLoader.Result( +                true, +                appTargets.getValue(), +                new ShortcutLoader.ShortcutResultInfo[] { +                        new ShortcutLoader.ShortcutResultInfo( +                                appTargets.getValue()[0], +                                serviceTargets +                        ) +                }, +                new HashMap<>(), +                new HashMap<>()          ); +        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); +        waitForIdle(); -        assertThat("Chooser should have 3 targets (2 apps, 1 direct)", -                activity.getAdapter().getCount(), is(3)); -        assertThat("Chooser should have exactly one selectable direct target", -                activity.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat("The resolver info must match the resolver info used to create the target", -                activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); +        final ChooserListAdapter activeAdapter = activity.getAdapter(); +        assertThat( +                "Chooser should have 3 targets (2 apps, 1 direct)", +                activeAdapter.getCount(), +                is(3)); +        assertThat( +                "Chooser should have exactly one selectable direct target", +                activeAdapter.getSelectableServiceTargetCount(), +                is(1)); +        assertThat( +                "The resolver info must match the resolver info used to create the target", +                activeAdapter.getItem(0).getResolveInfo(), +                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));          // Click on the direct target          String name = serviceTargets.get(0).getTitle().toString(); @@ -1410,18 +1454,19 @@ public class UnbundledChooserActivityTest {                  .perform(click());          waitForIdle(); -        // Currently we're seeing 3 invocations -        //      1. ChooserActivity.onCreate() -        //      2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() -        //      3. ChooserActivity.startSelected -- which is the one we're after -        verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); -        assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), +        // Currently we're seeing 4 invocations +        //      1. ChooserActivity.logActionShareWithPreview() +        //      2. ChooserActivity.onCreate() +        //      3. ChooserActivity.logDirectShareTargetReceived() +        //      4. ChooserActivity.startSelected -- which is the one we're after +        verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture()); +        assertThat(logMakerCaptor.getAllValues().get(3).getCategory(),                  is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));          assertThat("The packages should match for app target and direct target", logMakerCaptor -                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0)); +                .getAllValues().get(3).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));      } -    @Test @Ignore +    @Test      public void testShortcutTargetWithApplyAppLimits() {          // Set up resources          ChooserActivityOverrideData.getInstance().resources = Mockito.spy( @@ -1445,48 +1490,64 @@ public class UnbundledChooserActivityTest {                                  Mockito.anyBoolean(),                                  Mockito.isA(List.class)))                  .thenReturn(resolvedComponentInfos); -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(2, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + +        // create test shortcut loader factory, remember loaders and their callbacks +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                createShortcutLoaderFactory();          // Start activity -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        final IChooserWrapper wrapper = (IChooserWrapper) activity; +        final IChooserWrapper activity = (IChooserWrapper) mActivityRule +                .launchActivity(Intent.createChooser(sendIntent, null)); +        waitForIdle(); -        // Insert the direct share target -        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); -        List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity); -        directShareToShortcutInfos.put(serviceTargets.get(0), -                shortcutInfos.get(0).getShortcutInfo()); -        directShareToShortcutInfos.put(serviceTargets.get(1), -                shortcutInfos.get(1).getShortcutInfo()); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> wrapper.getAdapter().addServiceResults( -                        wrapper.createTestDisplayResolveInfo(sendIntent, -                                ri, -                                "testLabel", -                                "testInfo", -                                sendIntent, -                                /* resolveInfoPresentationGetter */ null), -                        serviceTargets, -                        TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, -                        directShareToShortcutInfos, -                        /* directShareToAppTargets */ null) +        // verify that ShortcutLoader was queried +        ArgumentCaptor<DisplayResolveInfo[]> appTargets = +                ArgumentCaptor.forClass(DisplayResolveInfo[].class); +        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + +        // send shortcuts +        assertThat( +                "Wrong number of app targets", +                appTargets.getValue().length, +                is(resolvedComponentInfos.size())); +        List<ChooserTarget> serviceTargets = createDirectShareTargets( +                2, +                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); +        ShortcutLoader.Result result = new ShortcutLoader.Result( +                true, +                appTargets.getValue(), +                new ShortcutLoader.ShortcutResultInfo[] { +                        new ShortcutLoader.ShortcutResultInfo( +                                appTargets.getValue()[0], +                                serviceTargets +                        ) +                }, +                new HashMap<>(), +                new HashMap<>()          ); +        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); +        waitForIdle(); -        assertThat("Chooser should have 3 targets (2 apps, 1 direct)", -                wrapper.getAdapter().getCount(), is(3)); -        assertThat("Chooser should have exactly one selectable direct target", -                wrapper.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat("The resolver info must match the resolver info used to create the target", -                wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); -        assertThat("The display label must match", -                wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); +        final ChooserListAdapter activeAdapter = activity.getAdapter(); +        assertThat( +                "Chooser should have 3 targets (2 apps, 1 direct)", +                activeAdapter.getCount(), +                is(3)); +        assertThat( +                "Chooser should have exactly one selectable direct target", +                activeAdapter.getSelectableServiceTargetCount(), +                is(1)); +        assertThat( +                "The resolver info must match the resolver info used to create the target", +                activeAdapter.getItem(0).getResolveInfo(), +                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); +        assertThat( +                "The display label must match", +                activeAdapter.getItem(0).getDisplayLabel(), +                is("testTitle0"));      } -    @Test @Ignore +    @Test      public void testShortcutTargetWithoutApplyAppLimits() {          setDeviceConfigProperty(                  SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, @@ -1513,47 +1574,65 @@ public class UnbundledChooserActivityTest {                                  Mockito.anyBoolean(),                                  Mockito.isA(List.class)))                  .thenReturn(resolvedComponentInfos); -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(2, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + +        // create test shortcut loader factory, remember loaders and their callbacks +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                createShortcutLoaderFactory();          // Start activity -        final ChooserActivity activity = +        final IChooserWrapper activity = (IChooserWrapper)                  mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); -        final IChooserWrapper wrapper = (IChooserWrapper) activity; +        waitForIdle(); -        // Insert the direct share target -        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); -        List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity); -        directShareToShortcutInfos.put(serviceTargets.get(0), -                shortcutInfos.get(0).getShortcutInfo()); -        directShareToShortcutInfos.put(serviceTargets.get(1), -                shortcutInfos.get(1).getShortcutInfo()); -        InstrumentationRegistry.getInstrumentation().runOnMainSync( -                () -> wrapper.getAdapter().addServiceResults( -                        wrapper.createTestDisplayResolveInfo(sendIntent, -                                ri, -                                "testLabel", -                                "testInfo", -                                sendIntent, -                                /* resolveInfoPresentationGetter */ null), -                        serviceTargets, -                        TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, -                        directShareToShortcutInfos, -                        /* directShareToAppTargets */ null) +        // verify that ShortcutLoader was queried +        ArgumentCaptor<DisplayResolveInfo[]> appTargets = +                ArgumentCaptor.forClass(DisplayResolveInfo[].class); +        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + +        // send shortcuts +        assertThat( +                "Wrong number of app targets", +                appTargets.getValue().length, +                is(resolvedComponentInfos.size())); +        List<ChooserTarget> serviceTargets = createDirectShareTargets( +                2, +                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); +        ShortcutLoader.Result result = new ShortcutLoader.Result( +                true, +                appTargets.getValue(), +                new ShortcutLoader.ShortcutResultInfo[] { +                        new ShortcutLoader.ShortcutResultInfo( +                                appTargets.getValue()[0], +                                serviceTargets +                        ) +                }, +                new HashMap<>(), +                new HashMap<>()          ); +        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); +        waitForIdle(); -        assertThat("Chooser should have 4 targets (2 apps, 2 direct)", -                wrapper.getAdapter().getCount(), is(4)); -        assertThat("Chooser should have exactly two selectable direct target", -                wrapper.getAdapter().getSelectableServiceTargetCount(), is(2)); -        assertThat("The resolver info must match the resolver info used to create the target", -                wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); -        assertThat("The display label must match", -                wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); -        assertThat("The display label must match", -                wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1")); +        final ChooserListAdapter activeAdapter = activity.getAdapter(); +        assertThat( +                "Chooser should have 4 targets (2 apps, 2 direct)", +                activeAdapter.getCount(), +                is(4)); +        assertThat( +                "Chooser should have exactly two selectable direct target", +                activeAdapter.getSelectableServiceTargetCount(), +                is(2)); +        assertThat( +                "The resolver info must match the resolver info used to create the target", +                activeAdapter.getItem(0).getResolveInfo(), +                is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); +        assertThat( +                "The display label must match", +                activeAdapter.getItem(0).getDisplayLabel(), +                is("testTitle0")); +        assertThat( +                "The display label must match", +                activeAdapter.getItem(1).getDisplayLabel(), +                is("testTitle1"));      }      @Test @@ -1948,43 +2027,59 @@ public class UnbundledChooserActivityTest {                                  Mockito.isA(List.class)))                  .thenReturn(resolvedComponentInfos); -        // Create direct share target -        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, -                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); -        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); - -        ChooserActivityOverrideData -                .getInstance() -                .directShareTargets = (activity, adapter) -> { -                    DisplayResolveInfo displayInfo = activity.createTestDisplayResolveInfo( -                            sendIntent, -                            ri, -                             "testLabel", -                             "testInfo", -                            sendIntent, -                            /* resolveInfoPresentationGetter */ null); -                    ServiceResultInfo[] results = { -                            new ServiceResultInfo(displayInfo, serviceTargets) }; -                    // TODO: consider covering the other type. -                    //  Only 2 types are expected out of the shortcut loading logic: -                    //  - TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, if shortcuts were loaded from -                    //    the ShortcutManager, and; -                    //  - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, if shortcuts were loaded -                    //    from AppPredictor. -                    //  Ideally, our tests should cover all of them. -                    return new Pair<>(TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER, results); +        // create test shortcut loader factory, remember loaders and their callbacks +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                new SparseArray<>(); +        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = +                (userHandle, callback) -> { +                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = +                            new Pair<>(mock(ShortcutLoader.class), callback); +                    shortcutLoaders.put(userHandle.getIdentifier(), pair); +                    return pair.first;                  };          // Start activity          final IChooserWrapper activity = (IChooserWrapper)                  mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); +        waitForIdle(); + +        // verify that ShortcutLoader was queried +        ArgumentCaptor<DisplayResolveInfo[]> appTargets = +                ArgumentCaptor.forClass(DisplayResolveInfo[].class); +        verify(shortcutLoaders.get(0).first, times(1)) +                .queryShortcuts(appTargets.capture()); + +        // send shortcuts +        assertThat( +                "Wrong number of app targets", +                appTargets.getValue().length, +                is(resolvedComponentInfos.size())); +        List<ChooserTarget> serviceTargets = createDirectShareTargets(1, +                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); +        ShortcutLoader.Result result = new ShortcutLoader.Result( +                // TODO: test another value as well +                false, +                appTargets.getValue(), +                new ShortcutLoader.ShortcutResultInfo[] { +                        new ShortcutLoader.ShortcutResultInfo( +                                appTargets.getValue()[0], +                                serviceTargets +                        ) +                }, +                new HashMap<>(), +                new HashMap<>() +        ); +        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); +        waitForIdle();          assertThat("Chooser should have 3 targets (2 apps, 1 direct)",                  activity.getAdapter().getCount(), is(3));          assertThat("Chooser should have exactly one selectable direct target",                  activity.getAdapter().getSelectableServiceTargetCount(), is(1)); -        assertThat("The resolver info must match the resolver info used to create the target", -                activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); +        assertThat( +                "The resolver info must match the resolver info used to create the target", +                activity.getAdapter().getItem(0).getResolveInfo(), +                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));          // Click on the direct target          String name = serviceTargets.get(0).getTitle().toString(); @@ -2098,7 +2193,7 @@ public class UnbundledChooserActivityTest {              return true;          }; -        mActivityRule.launchActivity(sendIntent); +        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));          waitForIdle();          assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); @@ -2273,21 +2368,20 @@ public class UnbundledChooserActivityTest {      }      @Test -    public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() { +    public void test_query_shortcut_loader_for_the_selected_tab() {          markWorkProfileUserAvailable();          List<ResolvedComponentInfo> personalResolvedComponentInfos =                  createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);          List<ResolvedComponentInfo> workResolvedComponentInfos =                  createResolvedComponentsForTest(3);          setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; -        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; -        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = -                chooserListAdapter -> { -                    isQueryDirectShareCalledOnWorkProfile[0] = -                            (chooserListAdapter.getUserHandle().getIdentifier() == 10); -                    return null; -                }; +        ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); +        ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); +        final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>(); +        shortcutLoaders.put(0, personalProfileShortcutLoader); +        shortcutLoaders.put(10, workProfileShortcutLoader); +        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = +                (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null);          Intent sendIntent = createSendTextIntent();          sendIntent.setType(TEST_MIME_TYPE); @@ -2295,118 +2389,14 @@ public class UnbundledChooserActivityTest {          waitForIdle();          onView(withId(com.android.internal.R.id.contentPanel))                  .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click());          waitForIdle(); -        assertFalse("Direct share targets were queried on a paused work profile", -                isQueryDirectShareCalledOnWorkProfile[0]); -    } - -    @Test -    public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; -        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; -        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = -                chooserListAdapter -> { -                    isQueryDirectShareCalledOnWorkProfile[0] = -                            (chooserListAdapter.getUserHandle().getIdentifier() == 10); -                    return null; -                }; -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); +        verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any()); -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp());          onView(withText(R.string.resolver_work_tab)).perform(click());          waitForIdle(); -        assertFalse("Direct share targets were queried on a locked work profile user", -                isQueryDirectShareCalledOnWorkProfile[0]); -    } - -    @Test -    public void testWorkTab_workUserNotRunning_workTargetsShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; - -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        final IChooserWrapper wrapper = (IChooserWrapper) activity; -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        assertEquals(3, wrapper.getWorkListAdapter().getCount()); -    } - -    @Test -    public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; -        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; -        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = -                chooserListAdapter -> { -                    isQueryDirectShareCalledOnWorkProfile[0] = -                            (chooserListAdapter.getUserHandle().getIdentifier() == 10); -                    return null; -                }; -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); - -        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        assertFalse("Direct share targets were queried on a locked work profile user", -                isQueryDirectShareCalledOnWorkProfile[0]); -    } - -    @Test -    public void testWorkTab_workUserLocked_workTargetsShown() { -        markWorkProfileUserAvailable(); -        List<ResolvedComponentInfo> personalResolvedComponentInfos = -                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); -        List<ResolvedComponentInfo> workResolvedComponentInfos = -                createResolvedComponentsForTest(3); -        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); -        Intent sendIntent = createSendTextIntent(); -        sendIntent.setType(TEST_MIME_TYPE); -        ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; - -        final ChooserActivity activity = -                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); -        final IChooserWrapper wrapper = (IChooserWrapper) activity; -        waitForIdle(); -        onView(withId(com.android.internal.R.id.contentPanel)) -                .perform(swipeUp()); -        onView(withText(R.string.resolver_work_tab)).perform(click()); -        waitForIdle(); - -        assertEquals(3, wrapper.getWorkListAdapter().getCount()); +        verify(workProfileShortcutLoader, times(1)).queryShortcuts(any());      }      private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { @@ -2713,4 +2703,18 @@ public class UnbundledChooserActivityTest {                          .getInteger(R.integer.config_chooser_max_targets_per_row))                  .thenReturn(targetsPerRow);      } + +    private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> +            createShortcutLoaderFactory() { +        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = +                new SparseArray<>(); +        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = +                (userHandle, callback) -> { +                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = +                            new Pair<>(mock(ShortcutLoader.class), callback); +                    shortcutLoaders.put(userHandle.getIdentifier(), pair); +                    return pair.first; +                }; +        return shortcutLoaders; +    }  } diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt new file mode 100644 index 00000000..5756a0cd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.shortcuts + +import android.app.prediction.AppPredictor +import android.content.ComponentName +import android.content.Context +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.content.pm.ShortcutManager +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.intentresolver.any +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.createAppTarget +import com.android.intentresolver.createShareShortcutInfo +import com.android.intentresolver.createShortcutInfo +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.util.concurrent.Executor +import java.util.function.Consumer + +@SmallTest +class ShortcutLoaderTest { +    private val appInfo = ApplicationInfo().apply { +        enabled = true +        flags = 0 +    } +    private val pm = mock<PackageManager> { +        whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) +    } +    private val context = mock<Context> { +        whenever(packageManager).thenReturn(pm) +        whenever(createContextAsUser(any(), anyInt())).thenReturn(this) +    } +    private val executor = ImmediateExecutor() +    private val intentFilter = mock<IntentFilter>() +    private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() +    private val callback = mock<Consumer<ShortcutLoader.Result>>() + +    @Test +    fun test_app_predictor_result() { +        val componentName = ComponentName("pkg", "Class") +        val appTarget = mock<DisplayResolveInfo> { +            whenever(resolvedComponentName).thenReturn(componentName) +        } +        val appTargets = arrayOf(appTarget) +        val testSubject = ShortcutLoader( +            context, +            appPredictor, +            UserHandle.of(0), +            true, +            intentFilter, +            executor, +            executor, +            callback +        ) + +        testSubject.queryShortcuts(appTargets) + +        verify(appPredictor, times(1)).requestPredictionUpdate() +        val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) +        verify(appPredictor, times(1)) +            .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + +        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) +        val matchingAppTarget = createAppTarget(matchingShortcutInfo) +        val shortcuts = listOf( +            matchingAppTarget, +            // mismatching shortcut +            createAppTarget( +                createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) +            ) +        ) +        appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) + +        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) +        verify(callback, times(1)).accept(resultCaptor.capture()) + +        val result = resultCaptor.value +        assertTrue("An app predictor result is expected", result.isFromAppPredictor) +        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) +        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) +        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) +        for (shortcut in result.shortcutsByApp[0].shortcuts) { +            assertEquals( +                "Wrong AppTarget in the cache", +                matchingAppTarget, +                result.directShareAppTargetCache[shortcut] +            ) +            assertEquals( +                "Wrong ShortcutInfo in the cache", +                matchingShortcutInfo, +                result.directShareShortcutInfoCache[shortcut] +            ) +        } +    } + +    @Test +    fun test_shortcut_manager_result() { +        val componentName = ComponentName("pkg", "Class") +        val appTarget = mock<DisplayResolveInfo> { +            whenever(resolvedComponentName).thenReturn(componentName) +        } +        val appTargets = arrayOf(appTarget) +        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) +        val shortcutManagerResult = listOf( +            ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), +            // mismatching shortcut +            createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) +        ) +        val shortcutManager = mock<ShortcutManager> { +            whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) +        } +        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) +        val testSubject = ShortcutLoader( +            context, +            null, +            UserHandle.of(0), +            true, +            intentFilter, +            executor, +            executor, +            callback +        ) + +        testSubject.queryShortcuts(appTargets) + +        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) +        verify(callback, times(1)).accept(resultCaptor.capture()) + +        val result = resultCaptor.value +        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) +        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) +        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) +        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) +        for (shortcut in result.shortcutsByApp[0].shortcuts) { +            assertTrue( +                "AppTargets are not expected the cache of a ShortcutManager result", +                result.directShareAppTargetCache.isEmpty() +            ) +            assertEquals( +                "Wrong ShortcutInfo in the cache", +                matchingShortcutInfo, +                result.directShareShortcutInfoCache[shortcut] +            ) +        } +    } + +    @Test +    fun test_fallback_to_shortcut_manager() { +        val componentName = ComponentName("pkg", "Class") +        val appTarget = mock<DisplayResolveInfo> { +            whenever(resolvedComponentName).thenReturn(componentName) +        } +        val appTargets = arrayOf(appTarget) +        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) +        val shortcutManagerResult = listOf( +            ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), +            // mismatching shortcut +            createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) +        ) +        val shortcutManager = mock<ShortcutManager> { +            whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) +        } +        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) +        val testSubject = ShortcutLoader( +            context, +            appPredictor, +            UserHandle.of(0), +            true, +            intentFilter, +            executor, +            executor, +            callback +        ) + +        testSubject.queryShortcuts(appTargets) + +        verify(appPredictor, times(1)).requestPredictionUpdate() +        val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) +        verify(appPredictor, times(1)) +            .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) +        appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) + +        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) +        verify(callback, times(1)).accept(resultCaptor.capture()) + +        val result = resultCaptor.value +        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) +        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) +        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) +        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) +        for (shortcut in result.shortcutsByApp[0].shortcuts) { +            assertTrue( +                "AppTargets are not expected the cache of a ShortcutManager result", +                result.directShareAppTargetCache.isEmpty() +            ) +            assertEquals( +                "Wrong ShortcutInfo in the cache", +                matchingShortcutInfo, +                result.directShareShortcutInfoCache[shortcut] +            ) +        } +    } + +    @Test +    fun test_do_not_call_services_for_not_running_work_profile() { +        testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) +    } + +    @Test +    fun test_do_not_call_services_for_locked_work_profile() { +        testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) +    } + +    @Test +    fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { +        testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) +    } + +    @Test +    fun test_call_services_for_not_running_main_profile() { +        testAlwaysCallSystemForMainProfile(isUserRunning = false) +    } + +    @Test +    fun test_call_services_for_locked_main_profile() { +        testAlwaysCallSystemForMainProfile(isUserUnlocked = false) +    } + +    @Test +    fun test_call_services_if_quite_mode_is_enabled_for_main_profile() { +        testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) +    } + +    private fun testDisabledWorkProfileDoNotCallSystem( +        isUserRunning: Boolean = true, +        isUserUnlocked: Boolean = true, +        isQuietModeEnabled: Boolean = false +    ) { +        val userHandle = UserHandle.of(10) +        val userManager = mock<UserManager> { +            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) +            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) +            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) +        } +        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); +        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() +        val callback = mock<Consumer<ShortcutLoader.Result>>() +        val testSubject = ShortcutLoader( +            context, +            appPredictor, +            userHandle, +            false, +            intentFilter, +            executor, +            executor, +            callback +        ) + +        testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + +        verify(appPredictor, never()).requestPredictionUpdate() +    } + +    private fun testAlwaysCallSystemForMainProfile( +        isUserRunning: Boolean = true, +        isUserUnlocked: Boolean = true, +        isQuietModeEnabled: Boolean = false +    ) { +        val userHandle = UserHandle.of(10) +        val userManager = mock<UserManager> { +            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) +            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) +            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) +        } +        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); +        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() +        val callback = mock<Consumer<ShortcutLoader.Result>>() +        val testSubject = ShortcutLoader( +            context, +            appPredictor, +            userHandle, +            true, +            intentFilter, +            executor, +            executor, +            callback +        ) + +        testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + +        verify(appPredictor, times(1)).requestPredictionUpdate() +    } +} + +private class ImmediateExecutor : Executor { +    override fun execute(r: Runnable) { +        r.run() +    } +} diff --git a/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt index 5529e714..e0de005d 100644 --- a/java/tests/src/com/android/intentresolver/ShortcutToChooserTargetConverterTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt @@ -14,7 +14,7 @@   * limitations under the License.   */ -package com.android.intentresolver +package com.android.intentresolver.shortcuts  import android.app.prediction.AppTarget  import android.content.ComponentName @@ -22,6 +22,8 @@ import android.content.Intent  import android.content.pm.ShortcutInfo  import android.content.pm.ShortcutManager.ShareShortcutInfo  import android.service.chooser.ChooserTarget +import com.android.intentresolver.createAppTarget +import com.android.intentresolver.createShareShortcutInfo  import org.junit.Assert.assertEquals  import org.junit.Assert.assertNotNull  import org.junit.Test  |