Extract ChooserActionFactory.

This was a sizable chunk of code dedicated to one logical set of
responsibilities, so it's nice to separate.

Test: `atest IntentResolverUnitTests`
Bug: 202167050
Change-Id: I3b033e975afeee66e33da38d0dc0eeba768d0ed4
Merged-In: I3b033e975afeee66e33da38d0dc0eeba768d0ed4
(cherry picked from commit 6f3ea1e9310afb2a56c1491148802e1b6154a094)
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
new file mode 100644
index 0000000..1fe5589
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -0,0 +1,477 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+    /** Delegate interface to launch activities when the actions are selected. */
+    public interface ActionActivityStarter {
+        /**
+         * Request an activity launch for the provided target. Implementations may choose to exit
+         * the current activity when the target is launched.
+         */
+        void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+        /**
+         * Request an activity launch for the provided target, optionally employing the specified
+         * shared element transition. Implementations may choose to exit the current activity when
+         * the target is launched.
+         */
+        default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+                TargetInfo info, View sharedElement, String sharedElementName) {
+            safelyStartActivityAsPersonalProfileUser(info);
+        }
+    }
+
+    private static final String TAG = "ChooserActions";
+
+    private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+    private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
+    private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
+
+    private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+    private final Context mContext;
+    private final String mCopyButtonLabel;
+    private final Drawable mCopyButtonDrawable;
+    private final Runnable mOnCopyButtonClicked;
+    private final TargetInfo mEditSharingTarget;
+    private final Runnable mOnEditButtonClicked;
+    private final TargetInfo mNearbySharingTarget;
+    private final Runnable mOnNearbyButtonClicked;
+    private final ImmutableList<ChooserAction> mCustomActions;
+    private final PendingIntent mReselectionIntent;
+    private final Consumer<Boolean> mExcludeSharedTextAction;
+    private final Consumer</* @Nullable */ Integer> mFinishCallback;
+
+    /**
+     * @param context
+     * @param chooserRequest data about the invocation of the current Sharesheet session.
+     * @param featureFlagRepository feature flags that may control the eligibility of some actions.
+     * @param integratedDeviceComponents info about other components that are available on this
+     * device to implement the supported action types.
+     * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+     * setting is updated. The argument is whether the shared text is to be excluded.
+     * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+     * View in the Sharesheet UI, if any, or null.
+     * @param activityStarter a delegate to launch activities when actions are selected.
+     * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+     * completed).
+     */
+    public ChooserActionFactory(
+            Context context,
+            ChooserRequestParameters chooserRequest,
+            FeatureFlagRepository featureFlagRepository,
+            ChooserIntegratedDeviceComponents integratedDeviceComponents,
+            ChooserActivityLogger logger,
+            Consumer<Boolean> onUpdateSharedTextIsExcluded,
+            Callable</* @Nullable */ View> firstVisibleImageQuery,
+            ActionActivityStarter activityStarter,
+            Consumer</* @Nullable */ Integer> finishCallback) {
+        this(
+                context,
+                context.getString(com.android.internal.R.string.copy),
+                context.getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
+                makeOnCopyRunnable(
+                        context,
+                        chooserRequest.getTargetIntent(),
+                        chooserRequest.getReferrerPackageName(),
+                        finishCallback,
+                        logger),
+                getEditSharingTarget(
+                        context,
+                        chooserRequest.getTargetIntent(),
+                        integratedDeviceComponents),
+                makeOnEditRunnable(
+                        getEditSharingTarget(
+                                context,
+                                chooserRequest.getTargetIntent(),
+                                integratedDeviceComponents),
+                        firstVisibleImageQuery,
+                        activityStarter,
+                        logger),
+                getNearbySharingTarget(
+                        context,
+                        chooserRequest.getTargetIntent(),
+                        integratedDeviceComponents),
+                makeOnNearbyShareRunnable(
+                        getNearbySharingTarget(
+                                context,
+                                chooserRequest.getTargetIntent(),
+                                integratedDeviceComponents),
+                        activityStarter,
+                        finishCallback,
+                        logger),
+                chooserRequest.getChooserActions(),
+                (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)
+                        ? chooserRequest.getModifyShareAction() : null),
+                onUpdateSharedTextIsExcluded,
+                finishCallback);
+    }
+
+    @VisibleForTesting
+    ChooserActionFactory(
+            Context context,
+            String copyButtonLabel,
+            Drawable copyButtonDrawable,
+            Runnable onCopyButtonClicked,
+            TargetInfo editSharingTarget,
+            Runnable onEditButtonClicked,
+            TargetInfo nearbySharingTarget,
+            Runnable onNearbyButtonClicked,
+            List<ChooserAction> customActions,
+            @Nullable PendingIntent reselectionIntent,
+            Consumer<Boolean> onUpdateSharedTextIsExcluded,
+            Consumer</* @Nullable */ Integer> finishCallback) {
+        mContext = context;
+        mCopyButtonLabel = copyButtonLabel;
+        mCopyButtonDrawable = copyButtonDrawable;
+        mOnCopyButtonClicked = onCopyButtonClicked;
+        mEditSharingTarget = editSharingTarget;
+        mOnEditButtonClicked = onEditButtonClicked;
+        mNearbySharingTarget = nearbySharingTarget;
+        mOnNearbyButtonClicked = onNearbyButtonClicked;
+        mCustomActions = ImmutableList.copyOf(customActions);
+        mReselectionIntent = reselectionIntent;
+        mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+        mFinishCallback = finishCallback;
+    }
+
+    /** Create an action that copies the share content to the clipboard. */
+    @Override
+    public ActionRow.Action createCopyButton() {
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_copy_button,
+                mCopyButtonLabel,
+                mCopyButtonDrawable,
+                mOnCopyButtonClicked);
+    }
+
+    /** Create an action that opens the share content in a system-default editor. */
+    @Override
+    @Nullable
+    public ActionRow.Action createEditButton() {
+        if (mEditSharingTarget == null) {
+            return null;
+        }
+
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_edit_button,
+                mEditSharingTarget.getDisplayLabel(),
+                mEditSharingTarget.getDisplayIconHolder().getDisplayIcon(),
+                mOnEditButtonClicked);
+    }
+
+    /** Create a "Share to Nearby" action. */
+    @Override
+    @Nullable
+    public ActionRow.Action createNearbyButton() {
+        if (mNearbySharingTarget == null) {
+            return null;
+        }
+
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_nearby_button,
+                mNearbySharingTarget.getDisplayLabel(),
+                mNearbySharingTarget.getDisplayIconHolder().getDisplayIcon(),
+                mOnNearbyButtonClicked);
+    }
+
+    /** Create custom actions */
+    @Override
+    public List<ActionRow.Action> createCustomActions() {
+        return mCustomActions.stream()
+                .map(target -> createCustomAction(mContext, target, mFinishCallback))
+                .filter(action -> action != null)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Provides a share modification action, if any.
+     */
+    @Override
+    @Nullable
+    public Runnable getModifyShareAction() {
+        return (mReselectionIntent == null) ? null : createReselectionRunnable(mReselectionIntent);
+    }
+
+    private Runnable createReselectionRunnable(PendingIntent pendingIntent) {
+        return () -> {
+            try {
+                pendingIntent.send();
+            } catch (PendingIntent.CanceledException e) {
+                Log.d(TAG, "Payload reselection action has been cancelled");
+            }
+            // TODO: add reporting
+            mFinishCallback.accept(Activity.RESULT_OK);
+        };
+    }
+
+    /**
+     * <p>
+     * Creates an exclude-text action that can be called when the user changes shared text
+     * status in the Media + Text preview.
+     * </p>
+     * <p>
+     * <code>true</code> argument value indicates that the text should be excluded.
+     * </p>
+     */
+    @Override
+    public Consumer<Boolean> getExcludeSharedTextAction() {
+        return mExcludeSharedTextAction;
+    }
+
+    private static Runnable makeOnCopyRunnable(
+            Context context,
+            Intent targetIntent,
+            String referrerPackageName,
+            Consumer<Integer> finishCallback,
+            ChooserActivityLogger logger) {
+        return () -> {
+            if (targetIntent == null) {
+                finishCallback.accept(null);
+                return;
+            }
+
+            final String action = targetIntent.getAction();
+
+            ClipData clipData = null;
+            if (Intent.ACTION_SEND.equals(action)) {
+                String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
+                Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+
+                if (extraText != null) {
+                    clipData = ClipData.newPlainText(null, extraText);
+                } else if (extraStream != null) {
+                    clipData = ClipData.newUri(context.getContentResolver(), null, extraStream);
+                } else {
+                    Log.w(TAG, "No data available to copy to clipboard");
+                    return;
+                }
+            } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+                final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
+                        Intent.EXTRA_STREAM);
+                clipData = ClipData.newUri(context.getContentResolver(), null, streams.get(0));
+                for (int i = 1; i < streams.size(); i++) {
+                    clipData.addItem(
+                            context.getContentResolver(),
+                            new ClipData.Item(streams.get(i)));
+                }
+            } else {
+                // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
+                // so warn about unexpected action
+                Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
+                return;
+            }
+
+            ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+                    Context.CLIPBOARD_SERVICE);
+            clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+            logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+            finishCallback.accept(Activity.RESULT_OK);
+        };
+    }
+
+    private static TargetInfo getEditSharingTarget(
+            Context context,
+            Intent originalIntent,
+            ChooserIntegratedDeviceComponents integratedComponents) {
+        final ComponentName editorComponent = integratedComponents.getEditSharingComponent();
+
+        final Intent resolveIntent = new Intent(originalIntent);
+        // Retain only URI permission grant flags if present. Other flags may prevent the scene
+        // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+        // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+        resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
+        resolveIntent.setComponent(editorComponent);
+        resolveIntent.setAction(Intent.ACTION_EDIT);
+        String originalAction = originalIntent.getAction();
+        if (Intent.ACTION_SEND.equals(originalAction)) {
+            if (resolveIntent.getData() == null) {
+                Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+                if (uri != null) {
+                    String mimeType = context.getContentResolver().getType(uri);
+                    resolveIntent.setDataAndType(uri, mimeType);
+                }
+            }
+        } else {
+            Log.e(TAG, originalAction + " is not supported.");
+            return null;
+        }
+        final ResolveInfo ri = context.getPackageManager().resolveActivity(
+                resolveIntent, PackageManager.GET_META_DATA);
+        if (ri == null || ri.activityInfo == null) {
+            Log.e(TAG, "Device-specified editor (" + editorComponent + ") not available");
+            return null;
+        }
+
+        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+                originalIntent,
+                ri,
+                context.getString(com.android.internal.R.string.screenshot_edit),
+                "",
+                resolveIntent,
+                null);
+        dri.getDisplayIconHolder().setDisplayIcon(
+                context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+        return dri;
+    }
+
+    private static Runnable makeOnEditRunnable(
+            TargetInfo editSharingTarget,
+            Callable</* @Nullable */ View> firstVisibleImageQuery,
+            ActionActivityStarter activityStarter,
+            ChooserActivityLogger logger) {
+        return () -> {
+            // Log share completion via edit.
+            logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT);
+
+            View firstImageView = null;
+            try {
+                firstImageView = firstVisibleImageQuery.call();
+            } catch (Exception e) { /* ignore */ }
+            // Action bar is user-independent; always start as primary.
+            if (firstImageView == null) {
+                activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+            } else {
+                activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+                        editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+            }
+        };
+    }
+
+    private static TargetInfo getNearbySharingTarget(
+            Context context,
+            Intent originalIntent,
+            ChooserIntegratedDeviceComponents integratedComponents) {
+        final ComponentName cn = integratedComponents.getNearbySharingComponent();
+        if (cn == null) return null;
+
+        final Intent resolveIntent = new Intent(originalIntent);
+        resolveIntent.setComponent(cn);
+        final ResolveInfo ri = context.getPackageManager().resolveActivity(
+                resolveIntent, PackageManager.GET_META_DATA);
+        if (ri == null || ri.activityInfo == null) {
+            Log.e(TAG, "Device-specified nearby sharing component (" + cn
+                    + ") not available");
+            return null;
+        }
+
+        // Allow the nearby sharing component to provide a more appropriate icon and label
+        // for the chip.
+        CharSequence name = null;
+        Drawable icon = null;
+        final Bundle metaData = ri.activityInfo.metaData;
+        if (metaData != null) {
+            try {
+                final Resources pkgRes = context.getPackageManager().getResourcesForActivity(cn);
+                final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY);
+                name = pkgRes.getString(nameResId);
+                final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY);
+                icon = pkgRes.getDrawable(resId);
+            } catch (NameNotFoundException | Resources.NotFoundException ex) { /* ignore */ }
+        }
+        if (TextUtils.isEmpty(name)) {
+            name = ri.loadLabel(context.getPackageManager());
+        }
+        if (icon == null) {
+            icon = ri.loadIcon(context.getPackageManager());
+        }
+
+        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+                originalIntent, ri, name, "", resolveIntent, null);
+        dri.getDisplayIconHolder().setDisplayIcon(icon);
+        return dri;
+    }
+
+    private static Runnable makeOnNearbyShareRunnable(
+            TargetInfo nearbyShareTarget,
+            ActionActivityStarter activityStarter,
+            Consumer<Integer> finishCallback,
+            ChooserActivityLogger logger) {
+        return () -> {
+            logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_NEARBY);
+            // Action bar is user-independent; always start as primary.
+            activityStarter.safelyStartActivityAsPersonalProfileUser(nearbyShareTarget);
+        };
+    }
+
+    @Nullable
+    private static ActionRow.Action createCustomAction(
+            Context context, ChooserAction action, Consumer<Integer> finishCallback) {
+        Drawable icon = action.getIcon().loadDrawable(context);
+        if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+            return null;
+        }
+        return new ActionRow.Action(
+                action.getLabel(),
+                icon,
+                () -> {
+                    try {
+                        action.getAction().send();
+                    } catch (PendingIntent.CanceledException e) {
+                        Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+                    }
+                    // TODO: add reporting
+                    finishCallback.accept(Activity.RESULT_OK);
+                }
+        );
+    }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 3439077..a2f2bbd 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -32,13 +32,10 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
-import android.app.PendingIntent;
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.app.prediction.AppTargetId;
-import android.content.ClipData;
-import android.content.ClipboardManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -49,31 +46,24 @@
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Insets;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.PatternMatcher;
 import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.StorageManager;
 import android.provider.DeviceConfig;
-import android.provider.Settings;
-import android.service.chooser.ChooserAction;
 import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -100,7 +90,6 @@
 import com.android.intentresolver.chooser.TargetInfo;
 import com.android.intentresolver.flags.FeatureFlagRepository;
 import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
-import com.android.intentresolver.flags.Flags;
 import com.android.intentresolver.grid.ChooserGridAdapter;
 import com.android.intentresolver.grid.DirectShareViewHolder;
 import com.android.intentresolver.model.AbstractResolverComparator;
@@ -108,7 +97,6 @@
 import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
 import com.android.intentresolver.shortcuts.AppPredictorFactory;
 import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.intentresolver.widget.ActionRow;
 import com.android.intentresolver.widget.ResolverDrawerLayout;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -116,8 +104,6 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.FrameworkStatsLog;
 
-import com.google.common.collect.ImmutableList;
-
 import java.io.File;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -210,6 +196,8 @@
             | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
             | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
 
+    private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+
     /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
      * only assignment there, and expect it to be ready by the time we ever use it --
      * someday if we move all the usage to a component with a narrower lifecycle (something that
@@ -220,6 +208,7 @@
     private ChooserRequestParameters mChooserRequest;
 
     private FeatureFlagRepository mFeatureFlagRepository;
+    private ChooserActionFactory mChooserActionFactory;
     private ChooserContentPreviewUi mChooserContentPreviewUi;
 
     private boolean mShouldDisplayLandscape;
@@ -274,11 +263,14 @@
         getChooserActivityLogger().logSharesheetTriggered();
 
         mFeatureFlagRepository = createFeatureFlagRepository();
+        mIntegratedDeviceComponents = getIntegratedDeviceComponents();
+
         try {
             mChooserRequest = new ChooserRequestParameters(
                     getIntent(),
+                    getReferrerPackageName(),
                     getReferrer(),
-                    getNearbySharingComponent(),
+                    mIntegratedDeviceComponents,
                     mFeatureFlagRepository);
         } catch (IllegalArgumentException e) {
             Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
@@ -286,6 +278,39 @@
             super_onCreate(null);
             return;
         }
+
+        mChooserActionFactory = new ChooserActionFactory(
+                this,
+                mChooserRequest,
+                mFeatureFlagRepository,
+                mIntegratedDeviceComponents,
+                getChooserActivityLogger(),
+                (isExcluded) -> mExcludeSharedText = isExcluded,
+                this::getFirstVisibleImgPreviewView,
+                new ChooserActionFactory.ActionActivityStarter() {
+                    @Override
+                    public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+                        safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
+                        finish();
+                    }
+
+                    @Override
+                    public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+                            TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+                        ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+                                ChooserActivity.this, sharedElement, sharedElementName);
+                        safelyStartActivityAsUser(
+                                targetInfo, getPersonalProfileUserHandle(), options.toBundle());
+                        startFinishAnimation();
+                    }
+                },
+                (status) -> {
+                    if (status != null) {
+                        setResult(status);
+                    }
+                    finish();
+                });
+
         mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository);
 
         setAdditionalTargets(mChooserRequest.getAdditionalTargets());
@@ -368,6 +393,11 @@
         mEnterTransitionAnimationDelegate.postponeTransition();
     }
 
+    @VisibleForTesting
+    protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+        return ChooserIntegratedDeviceComponents.get(this);
+    }
+
     @Override
     protected int appliedThemeResId() {
         return R.style.Theme_DeviceDefault_Chooser;
@@ -607,51 +637,6 @@
         updateProfileViewButton();
     }
 
-    private void onCopyButtonClicked() {
-        Intent targetIntent = getTargetIntent();
-        if (targetIntent == null) {
-            finish();
-        } else {
-            final String action = targetIntent.getAction();
-
-            ClipData clipData = null;
-            if (Intent.ACTION_SEND.equals(action)) {
-                String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
-                Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-
-                if (extraText != null) {
-                    clipData = ClipData.newPlainText(null, extraText);
-                } else if (extraStream != null) {
-                    clipData = ClipData.newUri(getContentResolver(), null, extraStream);
-                } else {
-                    Log.w(TAG, "No data available to copy to clipboard");
-                    return;
-                }
-            } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
-                final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
-                        Intent.EXTRA_STREAM);
-                clipData = ClipData.newUri(getContentResolver(), null, streams.get(0));
-                for (int i = 1; i < streams.size(); i++) {
-                    clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i)));
-                }
-            } else {
-                // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
-                // so warn about unexpected action
-                Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
-                return;
-            }
-
-            ClipboardManager clipboardManager = (ClipboardManager) getSystemService(
-                    Context.CLIPBOARD_SERVICE);
-            clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName());
-
-            getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
-
-            setResult(RESULT_OK);
-            finish();
-        }
-    }
-
     @Override
     protected void onResume() {
         super.onResume();
@@ -728,64 +713,12 @@
         int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
                 targetIntent, getContentResolver(), this::isImageType);
 
-        ChooserContentPreviewUi.ActionFactory actionFactory =
-                new ChooserContentPreviewUi.ActionFactory() {
-                    @Override
-                    public ActionRow.Action createCopyButton() {
-                        return ChooserActivity.this.createCopyAction();
-                    }
-
-                    @Nullable
-                    @Override
-                    public ActionRow.Action createEditButton() {
-                        return ChooserActivity.this.createEditAction(targetIntent);
-                    }
-
-                    @Nullable
-                    @Override
-                    public ActionRow.Action createNearbyButton() {
-                        return ChooserActivity.this.createNearbyAction(targetIntent);
-                    }
-
-                    @Override
-                    public List<ActionRow.Action> createCustomActions() {
-                        ImmutableList<ChooserAction> customActions =
-                                mChooserRequest.getChooserActions();
-                        List<ActionRow.Action> actions = new ArrayList<>(customActions.size());
-                        for (ChooserAction customAction : customActions) {
-                            ActionRow.Action action = createCustomAction(customAction);
-                            if (action != null) {
-                                actions.add(action);
-                            }
-                        }
-                        return actions;
-                    }
-
-                    @Nullable
-                    @Override
-                    public Runnable getModifyShareAction() {
-                        if (!mFeatureFlagRepository
-                                .isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) {
-                            return null;
-                        }
-                        PendingIntent reselectionAction = mChooserRequest.getModifyShareAction();
-                        return reselectionAction == null
-                                ? null
-                                : createReselectionRunnable(reselectionAction);
-                    }
-
-                    @Override
-                    public Consumer<Boolean> getExcludeSharedTextAction() {
-                        return (isExcluded) -> mExcludeSharedText = isExcluded;
-                    }
-                };
-
         ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
                 previewType,
                 targetIntent,
                 getResources(),
                 getLayoutInflater(),
-                actionFactory,
+                mChooserActionFactory,
                 parent,
                 imageLoader,
                 mEnterTransitionAnimationDelegate,
@@ -799,211 +732,6 @@
         return layout;
     }
 
-    @VisibleForTesting
-    protected ComponentName getNearbySharingComponent() {
-        String nearbyComponent = Settings.Secure.getString(
-                getContentResolver(),
-                Settings.Secure.NEARBY_SHARING_COMPONENT);
-        if (TextUtils.isEmpty(nearbyComponent)) {
-            nearbyComponent = getString(R.string.config_defaultNearbySharingComponent);
-        }
-        if (TextUtils.isEmpty(nearbyComponent)) {
-            return null;
-        }
-        return ComponentName.unflattenFromString(nearbyComponent);
-    }
-
-    @VisibleForTesting
-    protected @Nullable ComponentName getEditSharingComponent() {
-        String editorPackage = getApplicationContext().getString(R.string.config_systemImageEditor);
-        if (editorPackage == null || TextUtils.isEmpty(editorPackage)) {
-            return null;
-        }
-        return ComponentName.unflattenFromString(editorPackage);
-    }
-
-    @VisibleForTesting
-    protected TargetInfo getEditSharingTarget(Intent originalIntent) {
-        final ComponentName cn = getEditSharingComponent();
-
-        final Intent resolveIntent = new Intent(originalIntent);
-        // Retain only URI permission grant flags if present. Other flags may prevent the scene
-        // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
-        // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
-        resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
-        resolveIntent.setComponent(cn);
-        resolveIntent.setAction(Intent.ACTION_EDIT);
-        String originalAction = originalIntent.getAction();
-        if (Intent.ACTION_SEND.equals(originalAction)) {
-            if (resolveIntent.getData() == null) {
-                Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-                if (uri != null) {
-                    String mimeType = getContentResolver().getType(uri);
-                    resolveIntent.setDataAndType(uri, mimeType);
-                }
-            }
-        } else {
-            Log.e(TAG, originalAction + " is not supported.");
-            return null;
-        }
-        final ResolveInfo ri = getPackageManager().resolveActivity(
-                resolveIntent, PackageManager.GET_META_DATA);
-        if (ri == null || ri.activityInfo == null) {
-            Log.e(TAG, "Device-specified image edit component (" + cn
-                    + ") not available");
-            return null;
-        }
-
-        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
-                originalIntent,
-                ri,
-                getString(com.android.internal.R.string.screenshot_edit),
-                "",
-                resolveIntent,
-                null);
-        dri.getDisplayIconHolder().setDisplayIcon(
-                getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
-        return dri;
-    }
-
-    @VisibleForTesting
-    protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
-        final ComponentName cn = getNearbySharingComponent();
-        if (cn == null) return null;
-
-        final Intent resolveIntent = new Intent(originalIntent);
-        resolveIntent.setComponent(cn);
-        final ResolveInfo ri = getPackageManager().resolveActivity(
-                resolveIntent, PackageManager.GET_META_DATA);
-        if (ri == null || ri.activityInfo == null) {
-            Log.e(TAG, "Device-specified nearby sharing component (" + cn
-                    + ") not available");
-            return null;
-        }
-
-        // Allow the nearby sharing component to provide a more appropriate icon and label
-        // for the chip.
-        CharSequence name = null;
-        Drawable icon = null;
-        final Bundle metaData = ri.activityInfo.metaData;
-        if (metaData != null) {
-            try {
-                final Resources pkgRes = getPackageManager().getResourcesForActivity(cn);
-                final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY);
-                name = pkgRes.getString(nameResId);
-                final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY);
-                icon = pkgRes.getDrawable(resId);
-            } catch (Resources.NotFoundException ex) {
-            } catch (NameNotFoundException ex) {
-            }
-        }
-        if (TextUtils.isEmpty(name)) {
-            name = ri.loadLabel(getPackageManager());
-        }
-        if (icon == null) {
-            icon = ri.loadIcon(getPackageManager());
-        }
-
-        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
-                originalIntent, ri, name, "", resolveIntent, null);
-        dri.getDisplayIconHolder().setDisplayIcon(icon);
-        return dri;
-    }
-
-    private ActionRow.Action createCopyAction() {
-        return new ActionRow.Action(
-                com.android.internal.R.id.chooser_copy_button,
-                getString(com.android.internal.R.string.copy),
-                getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
-                this::onCopyButtonClicked);
-    }
-
-    @Nullable
-    private ActionRow.Action createNearbyAction(Intent originalIntent) {
-        final TargetInfo ti = getNearbySharingTarget(originalIntent);
-        if (ti == null) {
-            return null;
-        }
-
-        return new ActionRow.Action(
-                com.android.internal.R.id.chooser_nearby_button,
-                ti.getDisplayLabel(),
-                ti.getDisplayIconHolder().getDisplayIcon(),
-                () -> {
-                    getChooserActivityLogger().logActionSelected(
-                            ChooserActivityLogger.SELECTION_TYPE_NEARBY);
-                    // Action bar is user-independent, always start as primary
-                    safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
-                    finish();
-                });
-    }
-
-    @Nullable
-    private ActionRow.Action createEditAction(Intent originalIntent) {
-        final TargetInfo ti = getEditSharingTarget(originalIntent);
-        if (ti == null) {
-            return null;
-        }
-
-        return new ActionRow.Action(
-                com.android.internal.R.id.chooser_edit_button,
-                ti.getDisplayLabel(),
-                ti.getDisplayIconHolder().getDisplayIcon(),
-                () -> {
-                    // Log share completion via edit
-                    getChooserActivityLogger().logActionSelected(
-                            ChooserActivityLogger.SELECTION_TYPE_EDIT);
-                    View firstImgView = getFirstVisibleImgPreviewView();
-                    // Action bar is user-independent, always start as primary
-                    if (firstImgView == null) {
-                        safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
-                        finish();
-                    } else {
-                        ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
-                                this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT);
-                        safelyStartActivityAsUser(
-                                ti, getPersonalProfileUserHandle(), options.toBundle());
-                        startFinishAnimation();
-                    }
-                }
-        );
-    }
-
-    @Nullable
-    private ActionRow.Action createCustomAction(ChooserAction action) {
-        Drawable icon = action.getIcon().loadDrawable(this);
-        if (icon == null && TextUtils.isEmpty(action.getLabel())) {
-            return null;
-        }
-        return new ActionRow.Action(
-                action.getLabel(),
-                icon,
-                () -> {
-                    try {
-                        action.getAction().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
-                    }
-                    // TODO: add reporting
-                    setResult(RESULT_OK);
-                    finish();
-                }
-        );
-    }
-
-    private Runnable createReselectionRunnable(PendingIntent pendingIntent) {
-        return () -> {
-            try {
-                pendingIntent.send();
-            } catch (PendingIntent.CanceledException e) {
-                Log.d(TAG, "Payload reselection action has been cancelled");
-            }
-            // TODO: add reporting
-            setResult(RESULT_OK);
-            finish();
-        };
-    }
-
     @Nullable
     private View getFirstVisibleImgPreviewView() {
         View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large);
@@ -1315,45 +1043,6 @@
         }
     }
 
-    private IntentFilter getTargetIntentFilter() {
-        return getTargetIntentFilter(getTargetIntent());
-    }
-
-    private IntentFilter getTargetIntentFilter(final Intent intent) {
-        try {
-            String dataString = intent.getDataString();
-            if (intent.getType() == null) {
-                if (!TextUtils.isEmpty(dataString)) {
-                    return new IntentFilter(intent.getAction(), dataString);
-                }
-                Log.e(TAG, "Failed to get target intent filter: intent data and type are null");
-                return null;
-            }
-            IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType());
-            List<Uri> contentUris = new ArrayList<>();
-            if (Intent.ACTION_SEND.equals(intent.getAction())) {
-                Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
-                if (uri != null) {
-                    contentUris.add(uri);
-                }
-            } else {
-                List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-                if (uris != null) {
-                    contentUris.addAll(uris);
-                }
-            }
-            for (Uri uri : contentUris) {
-                intentFilter.addDataScheme(uri.getScheme());
-                intentFilter.addDataAuthority(uri.getAuthority(), null);
-                intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
-            }
-            return intentFilter;
-        } catch (Exception e) {
-            Log.e(TAG, "Failed to get target intent filter", e);
-            return null;
-        }
-    }
-
     private void logDirectShareTargetReceived(UserHandle forUser) {
         ProfileRecord profileRecord = getProfileRecord(forUser);
         if (profileRecord == null) {
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
new file mode 100644
index 0000000..9b124c2
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper to look up the components available on this device to handle assorted built-in actions
+ * like "Edit" that may be displayed for certain content/preview types. The components are queried
+ * when this record is instantiated, and are then immutable for a given instance.
+ *
+ * Because this describes the app's external execution environment, test methods may prefer to
+ * provide explicit values to override the default lookup logic.
+ */
+public final class ChooserIntegratedDeviceComponents {
+    @Nullable
+    private final ComponentName mEditSharingComponent;
+
+    @Nullable
+    private final ComponentName mNearbySharingComponent;
+
+    /** Look up the integrated components available on this device. */
+    public static ChooserIntegratedDeviceComponents get(Context context) {
+        return new ChooserIntegratedDeviceComponents(
+                getEditSharingComponent(context),
+                getNearbySharingComponent(context));
+    }
+
+    @VisibleForTesting
+    ChooserIntegratedDeviceComponents(
+            ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
+        mEditSharingComponent = editSharingComponent;
+        mNearbySharingComponent = nearbySharingComponent;
+    }
+
+    public ComponentName getEditSharingComponent() {
+        return mEditSharingComponent;
+    }
+
+    public ComponentName getNearbySharingComponent() {
+        return mNearbySharingComponent;
+    }
+
+    private static ComponentName getEditSharingComponent(Context context) {
+        String editorComponent = context.getApplicationContext().getString(
+                R.string.config_systemImageEditor);
+        return TextUtils.isEmpty(editorComponent)
+                ? null : ComponentName.unflattenFromString(editorComponent);
+    }
+
+    private static ComponentName getNearbySharingComponent(Context context) {
+        String nearbyComponent = Settings.Secure.getString(
+                context.getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT);
+        return TextUtils.isEmpty(nearbyComponent)
+                ? null : ComponentName.unflattenFromString(nearbyComponent);
+    }
+}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 2b67b27..83a0e2e 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -71,6 +71,8 @@
             Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 
     private final Intent mTarget;
+    private final ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+    private final String mReferrerPackageName;
     private final Pair<CharSequence, Integer> mTitleSpec;
     private final Intent mReferrerFillInIntent;
     private final ImmutableList<ComponentName> mFilteredComponentNames;
@@ -102,13 +104,18 @@
 
     public ChooserRequestParameters(
             final Intent clientIntent,
+            String referrerPackageName,
             final Uri referrer,
-            @Nullable final ComponentName nearbySharingComponent,
+            ChooserIntegratedDeviceComponents integratedDeviceComponents,
             FeatureFlagRepository featureFlags) {
         final Intent requestedTarget = parseTargetIntentExtra(
                 clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
         mTarget = intentWithModifiedLaunchFlags(requestedTarget);
 
+        mIntegratedDeviceComponents = integratedDeviceComponents;
+
+        mReferrerPackageName = referrerPackageName;
+
         mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
                 clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
 
@@ -128,7 +135,8 @@
         mRefinementIntentSender = clientIntent.getParcelableExtra(
                 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
 
-        mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent);
+        mFilteredComponentNames = getFilteredComponentNames(
+                clientIntent, mIntegratedDeviceComponents.getNearbySharingComponent());
 
         mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
 
@@ -165,6 +173,10 @@
         return getTargetIntent().getType();
     }
 
+    public String getReferrerPackageName() {
+        return mReferrerPackageName;
+    }
+
     @Nullable
     public CharSequence getTitle() {
         return mTitleSpec.first;
@@ -245,6 +257,10 @@
         return mTargetIntentFilter;
     }
 
+    public ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+        return mIntegratedDeviceComponents;
+    }
+
     private static boolean isSendAction(@Nullable String action) {
         return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
     }
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index a47014e..17084e1 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -37,7 +37,6 @@
 import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
 import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
-import com.android.intentresolver.chooser.NotSelectableTargetInfo;
 import com.android.intentresolver.chooser.TargetInfo;
 import com.android.intentresolver.flags.FeatureFlagRepository;
 import com.android.intentresolver.grid.ChooserGridAdapter;
@@ -120,15 +119,13 @@
     }
 
     @Override
-    protected ComponentName getNearbySharingComponent() {
-        // an arbitrary pre-installed activity that handles this type of intent
-        return ComponentName.unflattenFromString("com.google.android.apps.messaging/"
-                + "com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity");
-    }
-
-    @Override
-    protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
-        return NotSelectableTargetInfo.newEmptyTargetInfo();
+    protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+        return new ChooserIntegratedDeviceComponents(
+                /* editSharingComponent=*/ null,
+                // An arbitrary pre-installed activity that handles this type of intent:
+                /* nearbySharingComponent=*/ new ComponentName(
+                        "com.google.android.apps.messaging",
+                        ".ui.conversationlist.ShareIntentActivity"));
     }
 
     @Override
@@ -172,7 +169,7 @@
     }
 
     @Override
-    public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) {
+    public void safelyStartActivity(TargetInfo cti) {
         if (sOverrides.onSafelyStartCallback != null
                 && sOverrides.onSafelyStartCallback.apply(cti)) {
             return;