diff options
author | 2023-05-24 20:58:58 +0000 | |
---|---|---|
committer | 2023-05-24 20:58:58 +0000 | |
commit | c60ca38567d795f011d9263b07c6147d1245795c (patch) | |
tree | 567d86b5125b54eb05d3f7218b846c0803775b82 /java | |
parent | a30ab865f8caa6d90b906343c7a8c6bd92985edf (diff) | |
parent | 7e8e8d6afbc1d7220614635c69df26f37abcdb76 (diff) |
Merge "Encapsulate target icon and label loading in an isolated component." into udc-dev am: 426c97295d am: 7e8e8d6afb
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/23372796
Change-Id: I6a0a99bb53d8bc3244efb93590e8482d77fd3800
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java')
15 files changed, 822 insertions, 468 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 014aa2a2..a2dff970 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -90,6 +90,8 @@ import com.android.intentresolver.contentpreview.PreviewViewModel; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.DefaultTargetDataLoader; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.measurements.Tracer; import com.android.intentresolver.model.AbstractResolverComparator; import com.android.intentresolver.model.AppPredictionServiceResolverComparator; @@ -309,7 +311,8 @@ public class ChooserActivity extends ResolverActivity implements mChooserRequest.getDefaultTitleResource(), mChooserRequest.getInitialIntents(), /* rList: List<ResolveInfo> = */ null, - /* supportsAlwaysUseOption = */ false); + /* supportsAlwaysUseOption = */ false, + new DefaultTargetDataLoader(this, getLifecycle(), false)); mChooserShownTime = System.currentTimeMillis(); final long systemCost = mChooserShownTime - intentReceivedTime; @@ -442,13 +445,14 @@ public class ChooserActivity extends ResolverActivity implements protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { if (shouldShowTabs()) { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles( - initialIntents, rList, filterLastUsed); + initialIntents, rList, filterLastUsed, targetDataLoader); } else { mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile( - initialIntents, rList, filterLastUsed); + initialIntents, rList, filterLastUsed, targetDataLoader); } return mChooserMultiProfilePagerAdapter; } @@ -491,14 +495,16 @@ public class ChooserActivity extends ResolverActivity implements private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { ChooserGridAdapter adapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, filterLastUsed, - /* userHandle */ getPersonalProfileUserHandle()); + /* userHandle */ getPersonalProfileUserHandle(), + targetDataLoader); return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, @@ -512,7 +518,8 @@ public class ChooserActivity extends ResolverActivity implements private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { int selectedProfile = findSelectedProfile(); ChooserGridAdapter personalAdapter = createChooserGridAdapter( /* context */ this, @@ -520,14 +527,16 @@ public class ChooserActivity extends ResolverActivity implements selectedProfile == PROFILE_PERSONAL ? initialIntents : null, rList, filterLastUsed, - /* userHandle */ getPersonalProfileUserHandle()); + /* userHandle */ getPersonalProfileUserHandle(), + targetDataLoader); ChooserGridAdapter workAdapter = createChooserGridAdapter( /* context */ this, /* payloadIntents */ mIntents, selectedProfile == PROFILE_WORK ? initialIntents : null, rList, filterLastUsed, - /* userHandle */ getWorkProfileUserHandle()); + /* userHandle */ getWorkProfileUserHandle(), + targetDataLoader); return new ChooserMultiProfilePagerAdapter( /* context */ this, personalAdapter, @@ -1183,7 +1192,8 @@ public class ChooserActivity extends ResolverActivity implements Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, - UserHandle userHandle) { + UserHandle userHandle, + TargetDataLoader targetDataLoader) { ChooserListAdapter chooserListAdapter = createChooserListAdapter( context, payloadIntents, @@ -1194,7 +1204,8 @@ public class ChooserActivity extends ResolverActivity implements userHandle, getTargetIntent(), mChooserRequest, - mMaxTargetsPerRow); + mMaxTargetsPerRow, + targetDataLoader); return new ChooserGridAdapter( context, @@ -1252,7 +1263,8 @@ public class ChooserActivity extends ResolverActivity implements UserHandle userHandle, Intent targetIntent, ChooserRequestParameters chooserRequest, - int maxTargetsPerRow) { + int maxTargetsPerRow, + TargetDataLoader targetDataLoader) { UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() && userHandle.equals(getPersonalProfileUserHandle()) ? getCloneProfileUserHandle() : userHandle; @@ -1270,7 +1282,8 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger(), chooserRequest, maxTargetsPerRow, - initialIntentsUserSpace); + initialIntentsUserSpace, + targetDataLoader); } @Override diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index c20af20c..b1fa16b0 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -27,14 +27,10 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.LabeledIntent; -import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.Trace; import android.os.UserHandle; @@ -47,20 +43,20 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import androidx.annotation.WorkerThread; - import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import java.util.ArrayList; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class ChooserListAdapter extends ResolverListAdapter { @@ -86,10 +82,11 @@ public class ChooserListAdapter extends ResolverListAdapter { private final ChooserActivityLogger mChooserActivityLogger; - private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>(); + private final Set<TargetInfo> mRequestedIcons = new HashSet<>(); // Reserve spots for incoming direct share targets by adding placeholders private final TargetInfo mPlaceHolderTargetInfo; + private final TargetDataLoader mTargetDataLoader; private final List<TargetInfo> mServiceTargets = new ArrayList<>(); private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>(); @@ -145,7 +142,8 @@ public class ChooserListAdapter extends ResolverListAdapter { ChooserActivityLogger chooserActivityLogger, ChooserRequestParameters chooserRequest, int maxRankedTargets, - UserHandle initialIntentsUserSpace) { + UserHandle initialIntentsUserSpace, + TargetDataLoader targetDataLoader) { // Don't send the initial intents through the shared ResolverActivity path, // we want to separate them into a different section. super( @@ -158,13 +156,14 @@ public class ChooserListAdapter extends ResolverListAdapter { userHandle, targetIntent, resolverListCommunicator, - false, - initialIntentsUserSpace); + initialIntentsUserSpace, + targetDataLoader); mChooserRequest = chooserRequest; mMaxRankedTargets = maxRankedTargets; mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context); + mTargetDataLoader = targetDataLoader; createPlaceHolders(); mChooserActivityLogger = chooserActivityLogger; mShortcutSelectionLogic = new ShortcutSelectionLogic( @@ -227,8 +226,9 @@ public class ChooserListAdapter extends ResolverListAdapter { ri.icon = 0; } ri.userHandle = initialIntentsUserSpace; + // TODO: remove DisplayResolveInfo dependency on presentation getter DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( - ii, ri, ii, mPresentationFactory.makePresentationGetter(ri)); + ii, ri, ii, mTargetDataLoader.createPresentationGetter(ri)); mCallerTargets.add(displayResolveInfo); if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break; } @@ -344,19 +344,19 @@ public class ChooserListAdapter extends ResolverListAdapter { } private void loadDirectShareIcon(SelectableTargetInfo info) { - LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info); - if (task == null) { - task = createLoadDirectShareIconTask(info); - mIconLoaders.put(info, task); - task.loadIcon(); + if (mRequestedIcons.add(info)) { + mTargetDataLoader.loadDirectShareIcon( + info, + getUserHandle(), + (drawable) -> onDirectShareIconLoaded(info, drawable)); } } - @VisibleForTesting - protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) { - return new LoadDirectShareIconTask( - mContext.createContextAsUser(getUserHandle(), 0), - info); + private void onDirectShareIconLoaded(SelectableTargetInfo mTargetInfo, Drawable icon) { + if (icon != null && !mTargetInfo.hasDisplayIcon()) { + mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon); + notifyDataSetChanged(); + } } void updateAlphabeticalList() { @@ -365,6 +365,15 @@ public class ChooserListAdapter extends ResolverListAdapter { new AsyncTask<Void, Void, List<DisplayResolveInfo>>() { @Override protected List<DisplayResolveInfo> doInBackground(Void... voids) { + try { + Trace.beginSection("update-alphabetical-list"); + return updateList(); + } finally { + Trace.endSection(); + } + } + + private List<DisplayResolveInfo> updateList() { List<DisplayResolveInfo> allTargets = new ArrayList<>(); allTargets.addAll(getTargetsInCurrentDisplayList()); allTargets.addAll(mCallerTargets); @@ -660,98 +669,4 @@ public class ChooserListAdapter extends ResolverListAdapter { }; } - /** - * Loads direct share targets icons. - */ - @VisibleForTesting - public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Drawable> { - private final Context mContext; - private final SelectableTargetInfo mTargetInfo; - - private LoadDirectShareIconTask(Context context, SelectableTargetInfo targetInfo) { - mContext = context; - mTargetInfo = targetInfo; - } - - @Override - protected Drawable doInBackground(Void... voids) { - Drawable drawable; - Trace.beginSection("shortcut-icon"); - try { - drawable = getChooserTargetIconDrawable( - mContext, - mTargetInfo.getChooserTargetIcon(), - mTargetInfo.getChooserTargetComponentName(), - mTargetInfo.getDirectShareShortcutInfo()); - } catch (Exception e) { - Log.e(TAG, - "Failed to load shortcut icon for " - + mTargetInfo.getChooserTargetComponentName(), - e); - drawable = loadIconPlaceholder(); - } finally { - Trace.endSection(); - } - return drawable; - } - - @Override - protected void onPostExecute(@Nullable Drawable icon) { - if (icon != null && !mTargetInfo.hasDisplayIcon()) { - mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon); - notifyDataSetChanged(); - } - } - - @WorkerThread - private Drawable getChooserTargetIconDrawable( - Context context, - @Nullable Icon icon, - ComponentName targetComponentName, - @Nullable ShortcutInfo shortcutInfo) { - Drawable directShareIcon = null; - - // First get the target drawable and associated activity info - if (icon != null) { - directShareIcon = icon.loadDrawable(context); - } else if (shortcutInfo != null) { - LauncherApps launcherApps = context.getSystemService(LauncherApps.class); - if (launcherApps != null) { - directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0); - } - } - - if (directShareIcon == null) { - return null; - } - - ActivityInfo info = null; - try { - info = context.getPackageManager().getActivityInfo(targetComponentName, 0); - } catch (PackageManager.NameNotFoundException error) { - Log.e(TAG, "Could not find activity associated with ChooserTarget"); - } - - if (info == null) { - return null; - } - - // Now fetch app icon and raster with no badging even in work profile - Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null); - - // Raster target drawable with appIcon as a badge - SimpleIconFactory sif = SimpleIconFactory.obtain(context); - Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); - sif.recycle(); - - return new BitmapDrawable(context.getResources(), directShareBadgedIcon); - } - - /** - * An alias for execute to use with unit tests. - */ - public void loadIcon() { - execute(); - } - } } diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index ac3b9a61..57871532 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -60,7 +60,6 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Insets; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -108,6 +107,8 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.DefaultTargetDataLoader; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; @@ -333,7 +334,7 @@ public class ResolverActivity extends FragmentActivity implements setSafeForwardingMode(true); - onCreate(savedInstanceState, intent, null, 0, null, null, true); + onCreate(savedInstanceState, intent, null, 0, null, null, true, createIconLoader()); } /** @@ -343,13 +344,26 @@ public class ResolverActivity extends FragmentActivity implements protected void onCreate(Bundle savedInstanceState, Intent intent, CharSequence title, Intent[] initialIntents, List<ResolveInfo> rList, boolean supportsAlwaysUseOption) { - onCreate(savedInstanceState, intent, title, 0, initialIntents, rList, - supportsAlwaysUseOption); + onCreate( + savedInstanceState, + intent, + title, + 0, + initialIntents, + rList, + supportsAlwaysUseOption, + createIconLoader()); } - protected void onCreate(Bundle savedInstanceState, Intent intent, - CharSequence title, int defaultTitleRes, Intent[] initialIntents, - List<ResolveInfo> rList, boolean supportsAlwaysUseOption) { + protected void onCreate( + Bundle savedInstanceState, + Intent intent, + CharSequence title, + int defaultTitleRes, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean supportsAlwaysUseOption, + TargetDataLoader targetDataLoader) { setTheme(appliedThemeResId()); super.onCreate(savedInstanceState); @@ -384,8 +398,9 @@ public class ResolverActivity extends FragmentActivity implements // provide any more information to help us select between them. boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction() && !shouldShowTabs() && !hasCloneProfile(); - mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); - if (configureContentView()) { + mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( + initialIntents, rList, filterLastUsed, targetDataLoader); + if (configureContentView(targetDataLoader)) { return; } @@ -441,15 +456,16 @@ public class ResolverActivity extends FragmentActivity implements protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; if (shouldShowTabs()) { resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForTwoProfiles( - initialIntents, rList, filterLastUsed); + initialIntents, rList, filterLastUsed, targetDataLoader); } else { resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile( - initialIntents, rList, filterLastUsed); + initialIntents, rList, filterLastUsed, targetDataLoader); } return resolverMultiProfilePagerAdapter; } @@ -1023,12 +1039,14 @@ public class ResolverActivity extends FragmentActivity implements // @NonFinalForTesting @VisibleForTesting - protected ResolverListAdapter createResolverListAdapter(Context context, - List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed, UserHandle userHandle) { - Intent startIntent = getIntent(); - boolean isAudioCaptureDevice = - startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false); + protected ResolverListAdapter createResolverListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle, + TargetDataLoader targetDataLoader) { UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() && userHandle.equals(getPersonalProfileUserHandle()) ? getCloneProfileUserHandle() : userHandle; @@ -1042,8 +1060,15 @@ public class ResolverActivity extends FragmentActivity implements userHandle, getTargetIntent(), this, - isAudioCaptureDevice, - initialIntentsUserSpace); + initialIntentsUserSpace, + targetDataLoader); + } + + private TargetDataLoader createIconLoader() { + Intent startIntent = getIntent(); + boolean isAudioCaptureDevice = + startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false); + return new DefaultTargetDataLoader(this, getLifecycle(), isAudioCaptureDevice); } private LatencyTracker getLatencyTracker() { @@ -1118,14 +1143,16 @@ public class ResolverActivity extends FragmentActivity implements createResolverMultiProfilePagerAdapterForOneProfile( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { ResolverListAdapter adapter = createResolverListAdapter( /* context */ this, /* payloadIntents */ mIntents, initialIntents, rList, filterLastUsed, - /* userHandle */ getPersonalProfileUserHandle()); + /* userHandle */ getPersonalProfileUserHandle(), + targetDataLoader); return new ResolverMultiProfilePagerAdapter( /* context */ this, adapter, @@ -1144,7 +1171,8 @@ public class ResolverActivity extends FragmentActivity implements private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed) { + boolean filterLastUsed, + TargetDataLoader targetDataLoader) { // In the edge case when we have 0 apps in the current profile and >1 apps in the other, // the intent resolver is started in the other profile. Since this is the only case when // this happens, we check for it here and set the current profile's tab. @@ -1172,7 +1200,8 @@ public class ResolverActivity extends FragmentActivity implements rList, (filterLastUsed && UserHandle.myUserId() == getPersonalProfileUserHandle().getIdentifier()), - /* userHandle */ getPersonalProfileUserHandle()); + /* userHandle */ getPersonalProfileUserHandle(), + targetDataLoader); UserHandle workProfileUserHandle = getWorkProfileUserHandle(); ResolverListAdapter workAdapter = createResolverListAdapter( /* context */ this, @@ -1181,7 +1210,8 @@ public class ResolverActivity extends FragmentActivity implements rList, (filterLastUsed && UserHandle.myUserId() == workProfileUserHandle.getIdentifier()), - /* userHandle */ workProfileUserHandle); + /* userHandle */ workProfileUserHandle, + targetDataLoader); return new ResolverMultiProfilePagerAdapter( /* context */ this, personalAdapter, @@ -1698,7 +1728,7 @@ public class ResolverActivity extends FragmentActivity implements * Sets up the content view. * @return <code>true</code> if the activity is finishing and creation should halt. */ - private boolean configureContentView() { + private boolean configureContentView(TargetDataLoader targetDataLoader) { if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) { throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() " + "cannot be null."); @@ -1715,7 +1745,7 @@ public class ResolverActivity extends FragmentActivity implements } if (shouldUseMiniResolver()) { - configureMiniResolverContent(); + configureMiniResolverContent(targetDataLoader); Trace.endSection(); return false; } @@ -1738,7 +1768,7 @@ public class ResolverActivity extends FragmentActivity implements * and asks the user if they'd like to open that cross-profile app or use the in-profile * browser. */ - private void configureMiniResolverContent() { + private void configureMiniResolverContent(TargetDataLoader targetDataLoader) { mLayoutId = R.layout.miniresolver; setContentView(mLayoutId); @@ -1753,15 +1783,15 @@ public class ResolverActivity extends FragmentActivity implements // Load the icon asynchronously ImageView icon = findViewById(com.android.internal.R.id.icon); - inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) { - @Override - protected void onPostExecute(Drawable drawable) { - if (!isDestroyed()) { - otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); - new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); - } - } - }.execute(); + targetDataLoader.loadAppTargetIcon( + otherProfileResolveInfo, + inactiveAdapter.getUserHandle(), + (drawable) -> { + if (!isDestroyed()) { + otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); + new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); + } + }); ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText( getResources().getString( diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index a5fdd320..282a672f 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -16,15 +16,10 @@ package com.android.intentresolver; -import static android.content.Context.ACTIVITY_SERVICE; - import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.PermissionChecker; import android.content.pm.ActivityInfo; import android.content.pm.LabeledIntent; import android.content.pm.PackageManager; @@ -49,15 +44,15 @@ import android.widget.TextView; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.internal.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; +import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.Set; public class ResolverListAdapter extends BaseAdapter { private static final String TAG = "ResolverListAdapter"; @@ -69,30 +64,28 @@ public class ResolverListAdapter extends BaseAdapter { protected final LayoutInflater mInflater; protected final ResolverListCommunicator mResolverListCommunicator; protected final ResolverListController mResolverListController; - protected final TargetPresentationGetter.Factory mPresentationFactory; private final List<Intent> mIntents; private final Intent[] mInitialIntents; private final List<ResolveInfo> mBaseResolveList; private final PackageManager mPm; - private final int mIconDpi; - private final boolean mIsAudioCaptureDevice; + private final TargetDataLoader mTargetDataLoader; private final UserHandle mUserHandle; private final Intent mTargetIntent; - private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); - private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>(); + private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>(); + private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>(); private ResolveInfo mLastChosen; private DisplayResolveInfo mOtherProfile; private int mPlaceholderCount; // This one is the list that the Adapter will actually present. - private List<DisplayResolveInfo> mDisplayList; + private final List<DisplayResolveInfo> mDisplayList; private List<ResolvedComponentInfo> mUnfilteredResolveList; private int mLastChosenPosition = -1; - private boolean mFilterLastUsed; + private final boolean mFilterLastUsed; private Runnable mPostListReadyRunnable; private boolean mIsTabLoaded; // Represents the UserSpace in which the Initial Intents should be resolved. @@ -108,24 +101,21 @@ public class ResolverListAdapter extends BaseAdapter { UserHandle userHandle, Intent targetIntent, ResolverListCommunicator resolverListCommunicator, - boolean isAudioCaptureDevice, - UserHandle initialIntentsUserSpace) { + UserHandle initialIntentsUserSpace, + TargetDataLoader targetDataLoader) { mContext = context; mIntents = payloadIntents; mInitialIntents = initialIntents; mBaseResolveList = rList; mInflater = LayoutInflater.from(context); mPm = context.getPackageManager(); + mTargetDataLoader = targetDataLoader; mDisplayList = new ArrayList<>(); mFilterLastUsed = filterLastUsed; mResolverListController = resolverListController; mUserHandle = userHandle; mTargetIntent = targetIntent; mResolverListCommunicator = resolverListCommunicator; - mIsAudioCaptureDevice = isAudioCaptureDevice; - final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); - mIconDpi = am.getLauncherLargeIconDensity(); - mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi); mInitialIntentsUserSpace = initialIntentsUserSpace; } @@ -364,12 +354,11 @@ public class ResolverListAdapter extends BaseAdapter { if (otherProfileInfo != null) { mOtherProfile = makeOtherProfileDisplayResolveInfo( - mContext, otherProfileInfo, mPm, mTargetIntent, mResolverListCommunicator, - mIconDpi); + mTargetDataLoader); } else { mOtherProfile = null; try { @@ -483,7 +472,7 @@ public class ResolverListAdapter extends BaseAdapter { ri.loadLabel(mPm), null, ii, - mPresentationFactory.makePresentationGetter(ri))); + mTargetDataLoader.createPresentationGetter(ri))); } } @@ -536,7 +525,7 @@ public class ResolverListAdapter extends BaseAdapter { intent, add, (replaceIntent != null) ? replaceIntent : defaultIntent, - mPresentationFactory.makePresentationGetter(add)); + mTargetDataLoader.createPresentationGetter(add)); dri.setPinned(rci.isPinned()); if (rci.isPinned()) { Log.i(TAG, "Pinned item: " + rci.name); @@ -704,25 +693,37 @@ public class ResolverListAdapter extends BaseAdapter { } protected final void loadIcon(DisplayResolveInfo info) { - LoadIconTask task = mIconLoaders.get(info); - if (task == null) { - task = new LoadIconTask(info); - mIconLoaders.put(info, task); - task.execute(); + if (mRequestedIcons.add(info)) { + mTargetDataLoader.loadAppTargetIcon( + info, + getUserHandle(), + (drawable) -> onIconLoaded(info, drawable)); + } + } + + private void onIconLoaded(DisplayResolveInfo displayResolveInfo, Drawable drawable) { + if (getOtherProfile() == displayResolveInfo) { + mResolverListCommunicator.updateProfileViewButton(); + } else if (!displayResolveInfo.hasDisplayIcon()) { + displayResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable); + notifyDataSetChanged(); } } private void loadLabel(DisplayResolveInfo info) { - LoadLabelTask task = mLabelLoaders.get(info); - if (task == null) { - task = createLoadLabelTask(info); - mLabelLoaders.put(info, task); - task.execute(); + if (mRequestedLabels.add(info)) { + mTargetDataLoader.loadLabel(info, (result) -> onLabelLoaded(info, result)); } } - protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { - return new LoadLabelTask(info); + protected final void onLabelLoaded( + DisplayResolveInfo displayResolveInfo, CharSequence[] result) { + if (displayResolveInfo.hasDisplayLabel()) { + return; + } + displayResolveInfo.setDisplayLabel(result[0]); + displayResolveInfo.setExtendedInfo(result[1]); + notifyDataSetChanged(); } public void onDestroy() { @@ -733,16 +734,8 @@ public class ResolverListAdapter extends BaseAdapter { if (mResolverListController != null) { mResolverListController.destroy(); } - cancelTasks(mIconLoaders.values()); - cancelTasks(mLabelLoaders.values()); - mIconLoaders.clear(); - mLabelLoaders.clear(); - } - - private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) { - for (T task: tasks) { - task.cancel(false); - } + mRequestedIcons.clear(); + mRequestedLabels.clear(); } private static ColorMatrixColorFilter getSuspendedColorMatrix() { @@ -768,39 +761,15 @@ public class ResolverListAdapter extends BaseAdapter { return sSuspendedMatrixColorFilter; } - Drawable loadIconForResolveInfo(ResolveInfo ri) { - // Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons - // should be badged. - return mPresentationFactory.makePresentationGetter(ri) - .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, getUserHandle())); - } - protected final Drawable loadIconPlaceholder() { return mContext.getDrawable(R.drawable.resolver_icon_placeholder); } void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { final DisplayResolveInfo iconInfo = getFilteredItem(); - if (iconView != null && iconInfo != null) { - new AsyncTask<Void, Void, Drawable>() { - @Override - protected Drawable doInBackground(Void... params) { - Drawable drawable; - try { - drawable = loadIconForResolveInfo(iconInfo.getResolveInfo()); - } catch (Exception e) { - ComponentName componentName = iconInfo.getResolvedComponentName(); - Log.e(TAG, "Failed to load app icon for " + componentName, e); - drawable = loadIconPlaceholder(); - } - return drawable; - } - - @Override - protected void onPostExecute(Drawable d) { - iconView.setImageDrawable(d); - } - }.execute(); + if (iconInfo != null) { + mTargetDataLoader.loadAppTargetIcon( + iconInfo, getUserHandle(), iconView::setImageDrawable); } } @@ -856,12 +825,11 @@ public class ResolverListAdapter extends BaseAdapter { * of an element in the resolve list). */ private static DisplayResolveInfo makeOtherProfileDisplayResolveInfo( - Context context, ResolvedComponentInfo resolvedComponentInfo, PackageManager pm, Intent targetIntent, ResolverListCommunicator resolverListCommunicator, - int iconDpi) { + TargetDataLoader targetDataLoader) { ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0); Intent pOrigIntent = resolverListCommunicator.getReplacementIntent( @@ -871,8 +839,7 @@ public class ResolverListAdapter extends BaseAdapter { resolveInfo.activityInfo, targetIntent); TargetPresentationGetter presentationGetter = - new TargetPresentationGetter.Factory(context, iconDpi) - .makePresentationGetter(resolveInfo); + targetDataLoader.createPresentationGetter(resolveInfo); return DisplayResolveInfo.newDisplayResolveInfo( resolvedComponentInfo.getIntentAt(0), @@ -971,89 +938,4 @@ public class ResolverListAdapter extends BaseAdapter { } } } - - protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { - private final DisplayResolveInfo mDisplayResolveInfo; - - protected LoadLabelTask(DisplayResolveInfo dri) { - mDisplayResolveInfo = dri; - } - - @Override - protected CharSequence[] doInBackground(Void... voids) { - TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter( - mDisplayResolveInfo.getResolveInfo()); - - if (mIsAudioCaptureDevice) { - // This is an audio capture device, so check record permissions - ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; - String packageName = activityInfo.packageName; - - int uid = activityInfo.applicationInfo.uid; - boolean hasRecordPermission = - PermissionChecker.checkPermissionForPreflight( - mContext, - android.Manifest.permission.RECORD_AUDIO, -1, uid, - packageName) - == android.content.pm.PackageManager.PERMISSION_GRANTED; - - if (!hasRecordPermission) { - // Doesn't have record permission, so warn the user - return new CharSequence[] { - pg.getLabel(), - mContext.getString(R.string.usb_device_resolve_prompt_warn) - }; - } - } - - return new CharSequence[] { - pg.getLabel(), - pg.getSubLabel() - }; - } - - @Override - protected void onPostExecute(CharSequence[] result) { - if (mDisplayResolveInfo.hasDisplayLabel()) { - return; - } - mDisplayResolveInfo.setDisplayLabel(result[0]); - mDisplayResolveInfo.setExtendedInfo(result[1]); - notifyDataSetChanged(); - } - } - - class LoadIconTask extends AsyncTask<Void, Void, Drawable> { - protected final DisplayResolveInfo mDisplayResolveInfo; - private final ResolveInfo mResolveInfo; - - LoadIconTask(DisplayResolveInfo dri) { - mDisplayResolveInfo = dri; - mResolveInfo = dri.getResolveInfo(); - } - - @Override - protected Drawable doInBackground(Void... params) { - Trace.beginSection("app-icon"); - try { - return loadIconForResolveInfo(mResolveInfo); - } catch (Exception e) { - ComponentName componentName = mDisplayResolveInfo.getResolvedComponentName(); - Log.e(TAG, "Failed to load app icon for " + componentName, e); - return loadIconPlaceholder(); - } finally { - Trace.endSection(); - } - } - - @Override - protected void onPostExecute(Drawable d) { - if (getOtherProfile() == mDisplayResolveInfo) { - mResolverListCommunicator.updateProfileViewButton(); - } else if (!mDisplayResolveInfo.hasDisplayIcon()) { - mDisplayResolveInfo.getDisplayIconHolder().setDisplayIcon(d); - notifyDataSetChanged(); - } - } - } } diff --git a/java/src/com/android/intentresolver/icons/BaseLoadIconTask.java b/java/src/com/android/intentresolver/icons/BaseLoadIconTask.java new file mode 100644 index 00000000..2eceb89c --- /dev/null +++ b/java/src/com/android/intentresolver/icons/BaseLoadIconTask.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.icons; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; + +import com.android.intentresolver.R; +import com.android.intentresolver.TargetPresentationGetter; + +import java.util.function.Consumer; + +abstract class BaseLoadIconTask extends AsyncTask<Void, Void, Drawable> { + protected final Context mContext; + protected final TargetPresentationGetter.Factory mPresentationFactory; + private final Consumer<Drawable> mCallback; + + BaseLoadIconTask( + Context context, + TargetPresentationGetter.Factory presentationFactory, + Consumer<Drawable> callback) { + mContext = context; + mPresentationFactory = presentationFactory; + mCallback = callback; + } + + protected final Drawable loadIconPlaceholder() { + return mContext.getDrawable(R.drawable.resolver_icon_placeholder); + } + + @Override + protected final void onPostExecute(Drawable d) { + mCallback.accept(d); + } +} diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt new file mode 100644 index 00000000..0414dea7 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 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.icons + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.AsyncTask +import android.os.UserHandle +import android.util.SparseArray +import androidx.annotation.GuardedBy +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.android.intentresolver.TargetPresentationGetter +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.SelectableTargetInfo +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor + +/** An actual [TargetDataLoader] implementation. */ +// TODO: replace async tasks with coroutines. +class DefaultTargetDataLoader( + private val context: Context, + private val lifecycle: Lifecycle, + private val isAudioCaptureDevice: Boolean, +) : TargetDataLoader() { + private val presentationFactory = + TargetPresentationGetter.Factory( + context, + context.getSystemService(ActivityManager::class.java)?.launcherLargeIconDensity + ?: error("Unable to access ActivityManager") + ) + private val nextTaskId = AtomicInteger(0) + @GuardedBy("self") private val activeTasks = SparseArray<AsyncTask<*, *, *>>() + private val executor = Dispatchers.IO.asExecutor() + + init { + lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + lifecycle.removeObserver(this) + destroy() + } + } + ) + } + + override fun loadAppTargetIcon( + info: DisplayResolveInfo, + userHandle: UserHandle, + callback: Consumer<Drawable>, + ) { + val taskId = nextTaskId.getAndIncrement() + LoadIconTask(context, info, userHandle, presentationFactory) { result -> + removeTask(taskId) + callback.accept(result) + } + .also { addTask(taskId, it) } + .executeOnExecutor(executor) + } + + override fun loadDirectShareIcon( + info: SelectableTargetInfo, + userHandle: UserHandle, + callback: Consumer<Drawable>, + ) { + val taskId = nextTaskId.getAndIncrement() + LoadDirectShareIconTask( + context.createContextAsUser(userHandle, 0), + info, + userHandle, + presentationFactory, + ) { result -> + removeTask(taskId) + callback.accept(result) + } + .also { addTask(taskId, it) } + .executeOnExecutor(executor) + } + + override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) { + val taskId = nextTaskId.getAndIncrement() + LoadLabelTask(context, info, isAudioCaptureDevice, presentationFactory) { result -> + removeTask(taskId) + callback.accept(result) + } + .also { addTask(taskId, it) } + .executeOnExecutor(executor) + } + + override fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter = + presentationFactory.makePresentationGetter(info) + + private fun addTask(id: Int, task: AsyncTask<*, *, *>) { + synchronized(activeTasks) { activeTasks.put(id, task) } + } + + private fun removeTask(id: Int) { + synchronized(activeTasks) { activeTasks.remove(id) } + } + + private fun destroy() { + synchronized(activeTasks) { + for (i in 0 until activeTasks.size()) { + activeTasks.valueAt(i).cancel(false) + } + activeTasks.clear() + } + } +} diff --git a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java new file mode 100644 index 00000000..b7bacc90 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 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.icons; + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Trace; +import android.os.UserHandle; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import com.android.intentresolver.SimpleIconFactory; +import com.android.intentresolver.TargetPresentationGetter; +import com.android.intentresolver.chooser.SelectableTargetInfo; + +import java.util.function.Consumer; + +/** + * Loads direct share targets icons. + */ +class LoadDirectShareIconTask extends BaseLoadIconTask { + private static final String TAG = "DirectShareIconTask"; + private final SelectableTargetInfo mTargetInfo; + + LoadDirectShareIconTask( + Context context, + SelectableTargetInfo targetInfo, + UserHandle userHandle, + TargetPresentationGetter.Factory presentationFactory, + Consumer<Drawable> callback) { + super(context, presentationFactory, callback); + mTargetInfo = targetInfo; + } + + @Override + protected Drawable doInBackground(Void... voids) { + Drawable drawable; + Trace.beginSection("shortcut-icon"); + try { + drawable = getChooserTargetIconDrawable( + mContext, + mTargetInfo.getChooserTargetIcon(), + mTargetInfo.getChooserTargetComponentName(), + mTargetInfo.getDirectShareShortcutInfo()); + } catch (Exception e) { + Log.e( + TAG, + "Failed to load shortcut icon for " + + mTargetInfo.getChooserTargetComponentName(), + e); + drawable = loadIconPlaceholder(); + } finally { + Trace.endSection(); + } + return drawable; + } + + @WorkerThread + private Drawable getChooserTargetIconDrawable( + Context context, + @Nullable Icon icon, + ComponentName targetComponentName, + @Nullable ShortcutInfo shortcutInfo) { + Drawable directShareIcon = null; + + // First get the target drawable and associated activity info + if (icon != null) { + directShareIcon = icon.loadDrawable(context); + } else if (shortcutInfo != null) { + LauncherApps launcherApps = context.getSystemService(LauncherApps.class); + if (launcherApps != null) { + directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0); + } + } + + if (directShareIcon == null) { + return null; + } + + ActivityInfo info = null; + try { + info = context.getPackageManager().getActivityInfo(targetComponentName, 0); + } catch (PackageManager.NameNotFoundException error) { + Log.e(TAG, "Could not find activity associated with ChooserTarget"); + } + + if (info == null) { + return null; + } + + // Now fetch app icon and raster with no badging even in work profile + Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null); + + // Raster target drawable with appIcon as a badge + SimpleIconFactory sif = SimpleIconFactory.obtain(context); + Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); + sif.recycle(); + + return new BitmapDrawable(context.getResources(), directShareBadgedIcon); + } +} diff --git a/java/src/com/android/intentresolver/icons/LoadIconTask.java b/java/src/com/android/intentresolver/icons/LoadIconTask.java new file mode 100644 index 00000000..37ce4093 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/LoadIconTask.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.icons; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Trace; +import android.os.UserHandle; +import android.util.Log; + +import com.android.intentresolver.ResolverActivity; +import com.android.intentresolver.TargetPresentationGetter; +import com.android.intentresolver.chooser.DisplayResolveInfo; + +import java.util.function.Consumer; + +class LoadIconTask extends BaseLoadIconTask { + private static final String TAG = "IconTask"; + protected final DisplayResolveInfo mDisplayResolveInfo; + private final UserHandle mUserHandle; + private final ResolveInfo mResolveInfo; + + LoadIconTask( + Context context, DisplayResolveInfo dri, + UserHandle userHandle, + TargetPresentationGetter.Factory presentationFactory, + Consumer<Drawable> callback) { + super(context, presentationFactory, callback); + mUserHandle = userHandle; + mDisplayResolveInfo = dri; + mResolveInfo = dri.getResolveInfo(); + } + + @Override + protected Drawable doInBackground(Void... params) { + Trace.beginSection("app-icon"); + try { + return loadIconForResolveInfo(mResolveInfo); + } catch (Exception e) { + ComponentName componentName = mDisplayResolveInfo.getResolvedComponentName(); + Log.e(TAG, "Failed to load app icon for " + componentName, e); + return loadIconPlaceholder(); + } finally { + Trace.endSection(); + } + } + + protected final Drawable loadIconForResolveInfo(ResolveInfo ri) { + // Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons + // should be badged. + return mPresentationFactory.makePresentationGetter(ri) + .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, mUserHandle)); + } + +} diff --git a/java/src/com/android/intentresolver/icons/LoadLabelTask.java b/java/src/com/android/intentresolver/icons/LoadLabelTask.java new file mode 100644 index 00000000..a0867b8e --- /dev/null +++ b/java/src/com/android/intentresolver/icons/LoadLabelTask.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 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.icons; + +import android.content.Context; +import android.content.PermissionChecker; +import android.content.pm.ActivityInfo; +import android.os.AsyncTask; +import android.os.Trace; + +import com.android.intentresolver.R; +import com.android.intentresolver.TargetPresentationGetter; +import com.android.intentresolver.chooser.DisplayResolveInfo; + +import java.util.function.Consumer; + +class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { + private final Context mContext; + private final DisplayResolveInfo mDisplayResolveInfo; + private final boolean mIsAudioCaptureDevice; + protected final TargetPresentationGetter.Factory mPresentationFactory; + private final Consumer<CharSequence[]> mCallback; + + LoadLabelTask(Context context, DisplayResolveInfo dri, + boolean isAudioCaptureDevice, TargetPresentationGetter.Factory presentationFactory, + Consumer<CharSequence[]> callback) { + mContext = context; + mDisplayResolveInfo = dri; + mIsAudioCaptureDevice = isAudioCaptureDevice; + mPresentationFactory = presentationFactory; + mCallback = callback; + } + + @Override + protected CharSequence[] doInBackground(Void... voids) { + try { + Trace.beginSection("app-label"); + return loadLabel(); + } finally { + Trace.endSection(); + } + } + + private CharSequence[] loadLabel() { + TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter( + mDisplayResolveInfo.getResolveInfo()); + + if (mIsAudioCaptureDevice) { + // This is an audio capture device, so check record permissions + ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; + String packageName = activityInfo.packageName; + + int uid = activityInfo.applicationInfo.uid; + boolean hasRecordPermission = + PermissionChecker.checkPermissionForPreflight( + mContext, + android.Manifest.permission.RECORD_AUDIO, -1, uid, + packageName) + == android.content.pm.PackageManager.PERMISSION_GRANTED; + + if (!hasRecordPermission) { + // Doesn't have record permission, so warn the user + return new CharSequence[]{ + pg.getLabel(), + mContext.getString(R.string.usb_device_resolve_prompt_warn) + }; + } + } + + return new CharSequence[]{ + pg.getLabel(), + pg.getSubLabel() + }; + } + + @Override + protected void onPostExecute(CharSequence[] result) { + mCallback.accept(result); + } +} diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt new file mode 100644 index 00000000..50f731f8 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.icons + +import android.content.pm.ResolveInfo +import android.graphics.drawable.Drawable +import android.os.UserHandle +import com.android.intentresolver.TargetPresentationGetter +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.SelectableTargetInfo +import java.util.function.Consumer + +/** A target data loader contract. Added to support testing. */ +abstract class TargetDataLoader { + /** Load an app target icon */ + abstract fun loadAppTargetIcon( + info: DisplayResolveInfo, + userHandle: UserHandle, + callback: Consumer<Drawable>, + ) + + /** Load a shortcut icon */ + abstract fun loadDirectShareIcon( + info: SelectableTargetInfo, + userHandle: UserHandle, + callback: Consumer<Drawable>, + ) + + /** Load target label */ + abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) + + /** Create a presentation getter to be used with a [DisplayResolveInfo] */ + // TODO: get rid of DisplayResolveInfo's dependency on the presentation getter and remove this + // method. + abstract fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter +} diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt index 9504f377..4612b430 100644 --- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -27,10 +27,10 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.ChooserListAdapter.LoadDirectShareIconTask import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.chooser.SelectableTargetInfo import com.android.intentresolver.chooser.TargetInfo +import com.android.intentresolver.icons.TargetDataLoader import com.android.internal.R import org.junit.Before import org.junit.Test @@ -40,47 +40,43 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class ChooserListAdapterTest { - private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry - .getInstrumentation().getTargetContext().getUser() + private val userHandle: UserHandle = + InstrumentationRegistry.getInstrumentation().targetContext.user - private val packageManager = mock<PackageManager> { - whenever( - resolveActivity(any(), any<ResolveInfoFlags>()) - ).thenReturn(mock()) - } - private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val packageManager = + mock<PackageManager> { + whenever(resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(mock()) + } + private val context = InstrumentationRegistry.getInstrumentation().context private val resolverListController = mock<ResolverListController>() private val chooserActivityLogger = mock<ChooserActivityLogger>() + private val mTargetDataLoader = mock<TargetDataLoader>() - private fun createChooserListAdapter( - taskProvider: (TargetInfo?) -> LoadDirectShareIconTask - ) = object : ChooserListAdapter( + private val testSubject by lazy { + ChooserListAdapter( context, emptyList(), emptyArray(), emptyList(), false, resolverListController, - null, + userHandle, Intent(), mock(), packageManager, chooserActivityLogger, mock(), 0, - null - ) { - override fun createLoadDirectShareIconTask( - info: SelectableTargetInfo - ): LoadDirectShareIconTask = taskProvider(info) - } + null, + mTargetDataLoader + ) + } @Before fun setup() { // ChooserListAdapter reads DeviceConfig and needs a permission for that. - InstrumentationRegistry - .getInstrumentation() - .getUiAutomation() + InstrumentationRegistry.getInstrumentation() + .uiAutomation .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") } @@ -90,41 +86,56 @@ class ChooserListAdapterTest { val viewHolder = ResolverListAdapter.ViewHolder(view) view.tag = viewHolder val targetInfo = createSelectableTargetInfo() - val iconTask = mock<LoadDirectShareIconTask>() - val testSubject = createChooserListAdapter { iconTask } testSubject.onBindView(view, targetInfo, 0) - verify(iconTask, times(1)).loadIcon() + verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) } @Test - fun testOnlyOneTaskPerTarget() { + fun onBindView_DirectShareTargetIconAndLabelLoadedOnlyOnce() { val view = createView() val viewHolderOne = ResolverListAdapter.ViewHolder(view) view.tag = viewHolderOne val targetInfo = createSelectableTargetInfo() - val iconTaskOne = mock<LoadDirectShareIconTask>() - val testTaskProvider = mock<() -> LoadDirectShareIconTask> { - whenever(invoke()).thenReturn(iconTaskOne) - } - val testSubject = createChooserListAdapter { testTaskProvider.invoke() } testSubject.onBindView(view, targetInfo, 0) val viewHolderTwo = ResolverListAdapter.ViewHolder(view) view.tag = viewHolderTwo - whenever(testTaskProvider()).thenReturn(mock()) testSubject.onBindView(view, targetInfo, 0) - verify(iconTaskOne, times(1)).loadIcon() - verify(testTaskProvider, times(1)).invoke() + verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) + } + + @Test + fun onBindView_AppTargetIconAndLabelLoadedOnlyOnce() { + val view = createView() + val viewHolderOne = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderOne + val targetInfo = + DisplayResolveInfo.newDisplayResolveInfo( + Intent(), + ResolverDataProvider.createResolveInfo(2, 0, userHandle), + null, + "extended info", + Intent(), + /* resolveInfoPresentationGetter= */ null + ) + testSubject.onBindView(view, targetInfo, 0) + + val viewHolderTwo = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderTwo + + testSubject.onBindView(view, targetInfo, 0) + + verify(mTargetDataLoader, times(1)).loadAppTargetIcon(any(), any(), any()) } private fun createSelectableTargetInfo(): TargetInfo = SelectableTargetInfo.newSelectableTargetInfo( /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolveInfo(2, 0, userHandle), "label", "extended info", Intent(), @@ -133,7 +144,10 @@ class ChooserListAdapterTest { /* backupResolveInfo = */ mock(), /* resolvedIntent = */ Intent(), /* chooserTarget = */ createChooserTarget( - "Target", 0.5f, ComponentName("pkg", "Class"), "id-1" + "Target", + 0.5f, + ComponentName("pkg", "Class"), + "id-1" ), /* modifiedScore = */ 1f, /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1), diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index fa934f87..6ac6b6d3 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -39,6 +39,7 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -72,7 +73,8 @@ public class ChooserWrapperActivity UserHandle userHandle, Intent targetIntent, ChooserRequestParameters chooserRequest, - int maxTargetsPerRow) { + int maxTargetsPerRow, + TargetDataLoader targetDataLoader) { PackageManager packageManager = sOverrides.packageManager == null ? context.getPackageManager() : sOverrides.packageManager; @@ -90,7 +92,8 @@ public class ChooserWrapperActivity getChooserActivityLogger(), chooserRequest, maxTargetsPerRow, - userHandle); + userHandle, + targetDataLoader); } @Override diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index 31c0a498..7233fd3d 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -109,7 +109,7 @@ public class ResolverActivityTest { setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); assertThat(activity.getAdapter().getCount(), is(2)); @@ -246,7 +246,7 @@ public class ResolverActivityTest { ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); Intent sendIntent = createSendImageIntent(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the last used slot @@ -280,7 +280,7 @@ public class ResolverActivityTest { setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot @@ -321,7 +321,7 @@ public class ResolverActivityTest { .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot @@ -782,7 +782,7 @@ public class ResolverActivityTest { .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the last used slot @@ -848,7 +848,7 @@ public class ResolverActivityTest { .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); assertThat(activity.getAdapter().hasFilteredItem(), is(false)); diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java index 645e8c72..401ede26 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -22,19 +22,26 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.annotation.Nullable; -import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.test.espresso.idling.CountingIdlingResource; + import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.TargetDataLoader; import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; /* @@ -42,7 +49,9 @@ import java.util.function.Function; */ public class ResolverWrapperActivity extends ResolverActivity { static final OverrideData sOverrides = new OverrideData(); - private UsageStatsManager mUsm; + + private final CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); public ResolverWrapperActivity() { super(/* isIntentPicker= */ true); @@ -55,11 +64,20 @@ public class ResolverWrapperActivity extends ResolverActivity { return 1234; } + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + @Override - public ResolverListAdapter createResolverListAdapter(Context context, - List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed, UserHandle userHandle) { - return new ResolverWrapperAdapter( + public ResolverListAdapter createResolverListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle, + TargetDataLoader targetDataLoader) { + return new ResolverListAdapter( context, payloadIntents, initialIntents, @@ -69,7 +87,8 @@ public class ResolverWrapperActivity extends ResolverActivity { userHandle, payloadIntents.get(0), // TODO: extract upstream this, - userHandle); + userHandle, + new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource)); } @Override @@ -88,8 +107,8 @@ public class ResolverWrapperActivity extends ResolverActivity { return super.createWorkProfileAvailabilityManager(); } - ResolverWrapperAdapter getAdapter() { - return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter(); + ResolverListAdapter getAdapter() { + return mMultiProfilePagerAdapter.getActiveListAdapter(); } ResolverListAdapter getPersonalListAdapter() { @@ -226,4 +245,50 @@ public class ResolverWrapperActivity extends ResolverActivity { .thenAnswer(invocation -> hasCrossProfileIntents); } } + + private static class TargetDataLoaderWrapper extends TargetDataLoader { + private final TargetDataLoader mTargetDataLoader; + private final CountingIdlingResource mLabelIdlingResource; + + private TargetDataLoaderWrapper( + TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) { + mTargetDataLoader = targetDataLoader; + mLabelIdlingResource = labelIdlingResource; + } + + @Override + public void loadAppTargetIcon( + @NonNull DisplayResolveInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); + } + + @Override + public void loadDirectShareIcon( + @NonNull SelectableTargetInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback); + } + + @Override + public void loadLabel( + @NonNull DisplayResolveInfo info, + @NonNull Consumer<CharSequence[]> callback) { + mLabelIdlingResource.increment(); + mTargetDataLoader.loadLabel( + info, + (result) -> { + mLabelIdlingResource.decrement(); + callback.accept(result); + }); + } + + @NonNull + @Override + public TargetPresentationGetter createPresentationGetter(@NonNull ResolveInfo info) { + return mTargetDataLoader.createPresentationGetter(info); + } + } } diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java deleted file mode 100644 index fd310fd8..00000000 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.os.UserHandle; - -import androidx.test.espresso.idling.CountingIdlingResource; - -import com.android.intentresolver.chooser.DisplayResolveInfo; - -import java.util.List; - -public class ResolverWrapperAdapter extends ResolverListAdapter { - - private CountingIdlingResource mLabelIdlingResource = - new CountingIdlingResource("LoadLabelTask"); - - public ResolverWrapperAdapter( - Context context, - List<Intent> payloadIntents, - Intent[] initialIntents, - List<ResolveInfo> rList, - boolean filterLastUsed, - ResolverListController resolverListController, - UserHandle userHandle, - Intent targetIntent, - ResolverListCommunicator resolverListCommunicator, - UserHandle initialIntentsUserHandle) { - super( - context, - payloadIntents, - initialIntents, - rList, - filterLastUsed, - resolverListController, - userHandle, - targetIntent, - resolverListCommunicator, - false, - initialIntentsUserHandle); - } - - public CountingIdlingResource getLabelIdlingResource() { - return mLabelIdlingResource; - } - - @Override - protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { - return new LoadLabelWrapperTask(info); - } - - class LoadLabelWrapperTask extends LoadLabelTask { - - protected LoadLabelWrapperTask(DisplayResolveInfo dri) { - super(dri); - } - - @Override - protected void onPreExecute() { - mLabelIdlingResource.increment(); - } - - @Override - protected void onPostExecute(CharSequence[] result) { - super.onPostExecute(result); - mLabelIdlingResource.decrement(); - } - } -} |