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();
-        }
-    }
-}