Encapsulate target icon and label loading in an isolated component.
Patchset #1
Make ResolverListAdapter's and ChooserListAdapter icon and label async
task classes static, move them into a separate package.
After the modification anonymous AsyncTask from
ResolverListAdapter#loadFilteredItemIconTaskAsync became the identical
to the ResolverListAdapter$LoadIconTask and thus removed.
Patchset #2
Incapsulate target icons and labels loading within a new component,
IconLoader; use it instead of the direct async task usage.
Use coroutines Dispatchers.IO pool for icon and label loading.
Bug: 280653893
Test: manual testing
Change-Id: Ie995a9ee9e88baeacc62821390ee90ef6b7e31e3
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 014aa2a..a2dff97 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.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
- UserHandle userHandle) {
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
ChooserListAdapter chooserListAdapter = createChooserListAdapter(
context,
payloadIntents,
@@ -1194,7 +1204,8 @@
userHandle,
getTargetIntent(),
mChooserRequest,
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ targetDataLoader);
return new ChooserGridAdapter(
context,
@@ -1252,7 +1263,8 @@
UserHandle userHandle,
Intent targetIntent,
ChooserRequestParameters chooserRequest,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ TargetDataLoader targetDataLoader) {
UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
&& userHandle.equals(getPersonalProfileUserHandle())
? getCloneProfileUserHandle() : userHandle;
@@ -1270,7 +1282,8 @@
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 c20af20..b1fa16b 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -27,14 +27,10 @@
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.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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
};
}
- /**
- * 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 ac3b9a6..5787153 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -60,7 +60,6 @@
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.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 @@
setSafeForwardingMode(true);
- onCreate(savedInstanceState, intent, null, 0, null, null, true);
+ onCreate(savedInstanceState, intent, null, 0, null, null, true, createIconLoader());
}
/**
@@ -343,13 +344,26 @@
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 @@
// 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 @@
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 @@
// @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 @@
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 @@
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 @@
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 @@
rList,
(filterLastUsed && UserHandle.myUserId()
== getPersonalProfileUserHandle().getIdentifier()),
- /* userHandle */ getPersonalProfileUserHandle());
+ /* userHandle */ getPersonalProfileUserHandle(),
+ targetDataLoader);
UserHandle workProfileUserHandle = getWorkProfileUserHandle();
ResolverListAdapter workAdapter = createResolverListAdapter(
/* context */ this,
@@ -1181,7 +1210,8 @@
rList,
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
- /* userHandle */ workProfileUserHandle);
+ /* userHandle */ workProfileUserHandle,
+ targetDataLoader);
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
@@ -1698,7 +1728,7 @@
* 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 @@
}
if (shouldUseMiniResolver()) {
- configureMiniResolverContent();
+ configureMiniResolverContent(targetDataLoader);
Trace.endSection();
return false;
}
@@ -1738,7 +1768,7 @@
* 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 @@
// 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 a5fdd32..282a672 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 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 @@
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 @@
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 @@
if (otherProfileInfo != null) {
mOtherProfile = makeOtherProfileDisplayResolveInfo(
- mContext,
otherProfileInfo,
mPm,
mTargetIntent,
mResolverListCommunicator,
- mIconDpi);
+ mTargetDataLoader);
} else {
mOtherProfile = null;
try {
@@ -483,7 +472,7 @@
ri.loadLabel(mPm),
null,
ii,
- mPresentationFactory.makePresentationGetter(ri)));
+ mTargetDataLoader.createPresentationGetter(ri)));
}
}
@@ -536,7 +525,7 @@
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 @@
}
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 @@
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 @@
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 @@
* 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 @@
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 @@
}
}
}
-
- 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 0000000..2eceb89
--- /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 0000000..0414dea
--- /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 0000000..b7bacc9
--- /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 0000000..37ce409
--- /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 0000000..a0867b8
--- /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 0000000..50f731f
--- /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 9504f37..4612b43 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.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 @@
@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 @@
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 @@
/* 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 fa934f8..6ac6b6d 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.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 @@
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 @@
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 31c0a49..7233fd3 100644
--- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
@@ -109,7 +109,7 @@
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 @@
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 @@
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 @@
.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 @@
.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 @@
.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 645e8c7..401ede2 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.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 @@
*/
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 @@
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 @@
userHandle,
payloadIntents.get(0), // TODO: extract upstream
this,
- userHandle);
+ userHandle,
+ new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource));
}
@Override
@@ -88,8 +107,8 @@
return super.createWorkProfileAvailabilityManager();
}
- ResolverWrapperAdapter getAdapter() {
- return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter();
+ ResolverListAdapter getAdapter() {
+ return mMultiProfilePagerAdapter.getActiveListAdapter();
}
ResolverListAdapter getPersonalListAdapter() {
@@ -226,4 +245,50 @@
.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 fd310fd..0000000
--- 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();
- }
- }
-}