Split ChooserContentPreviewUi into multiple components

A prep step for the new preview UI: extract each preview logic from
ChooserContentPreviewUi into individual components leaving the former as
a facade for the latter.
More specifically:
 * move ChooserContnetPreviewUi into a new package;
 * make #displayTextContentPreview() to be the core of the
   TextContentPreviewUI (with related methods);
 * make #displayFileContentPreview() to be the core of the
   FileContentPreviewUi (with related methods);
 * make #displayImageContentPreview() to the core of the
   ImageContentPreviewUi (with the related methods);
 * for all aforementioned new components, pass component specific
   dependencies as constructor arguments, capture the common component
   contract in the base abstract class, ContentPreviewUi, along with the
   static utility methods.

Bug: 271613784
Test: Manual testing of
 * Text sharing with and without preview;
 * File sharing with and without preivew, single file and multiple
   fiels;
 * Image sharing, single and multiple files, image + text sharing;
 * Custom actions with text, image and files sharing;
 * Reselection action with text, image and files sharing.
Test: screenshot transition animation
Test: atest IntentResolverUnitTests
Change-Id: I392de610b3d3e044e23c83d29fd11061fbc7192d
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 14d5972..947155f 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -39,6 +39,7 @@
 
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
 import com.android.intentresolver.flags.FeatureFlagRepository;
 import com.android.intentresolver.flags.Flags;
 import com.android.intentresolver.widget.ActionRow;
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 3a7c892..ae5be26 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -83,6 +83,7 @@
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
 import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
 import com.android.intentresolver.flags.FeatureFlagRepository;
 import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
 import com.android.intentresolver.flags.Flags;
@@ -205,7 +206,6 @@
     private ChooserRefinementManager mRefinementManager;
 
     private FeatureFlagRepository mFeatureFlagRepository;
-    private ChooserActionFactory mChooserActionFactory;
     private ChooserContentPreviewUi mChooserContentPreviewUi;
 
     private boolean mShouldDisplayLandscape;
@@ -231,9 +231,6 @@
 
     private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
 
-    @Nullable
-    private ImageLoader mPreviewImageLoader;
-
     private int mScrollStatus = SCROLL_STATUS_IDLE;
 
     @VisibleForTesting
@@ -273,38 +270,6 @@
             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();
-                });
-
         mRefinementManager = new ChooserRefinementManager(
                 this,
                 mChooserRequest.getRefinementIntentSender(),
@@ -319,7 +284,14 @@
                     finish();
                 });
 
-        mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository);
+        mChooserContentPreviewUi = new ChooserContentPreviewUi(
+                mChooserRequest.getTargetIntent(),
+                getContentResolver(),
+                this::isImageType,
+                createPreviewImageLoader(),
+                createChooserActionFactory(),
+                mEnterTransitionAnimationDelegate,
+                mFeatureFlagRepository);
 
         setAdditionalTargets(mChooserRequest.getAdditionalTargets());
 
@@ -339,8 +311,6 @@
                         mChooserRequest.getTargetIntentFilter()),
                 mChooserRequest.getTargetIntentFilter());
 
-        mPreviewImageLoader = createPreviewImageLoader();
-
         super.onCreate(
                 savedInstanceState,
                 mChooserRequest.getTargetIntent(),
@@ -392,8 +362,7 @@
                 (mChooserRequest.getInitialIntents() == null)
                         ? 0 : mChooserRequest.getInitialIntents().length,
                 isWorkProfile(),
-                ChooserContentPreviewUi.findPreferredContentPreview(
-                        getTargetIntent(), getContentResolver(), this::isImageType),
+                mChooserContentPreviewUi.getPreferredContentPreview(),
                 mChooserRequest.getTargetAction(),
                 mChooserRequest.getChooserActions().size(),
                 mChooserRequest.getModifyShareAction() != null
@@ -594,8 +563,7 @@
                 || mChooserMultiProfilePagerAdapter
                         .getCurrentRootAdapter().getSystemRowCount() != 0) {
             getChooserActivityLogger().logActionShareWithPreview(
-                    ChooserContentPreviewUi.findPreferredContentPreview(
-                            getTargetIntent(), getContentResolver(), this::isImageType));
+                    mChooserContentPreviewUi.getPreferredContentPreview());
         }
         return postRebuildListInternal(rebuildCompleted);
     }
@@ -717,22 +685,11 @@
      * @param parent reference to the parent container where the view should be attached to
      * @return content preview view
      */
-    protected ViewGroup createContentPreviewView(ViewGroup parent, ImageLoader imageLoader) {
-        Intent targetIntent = getTargetIntent();
-        int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
-                targetIntent, getContentResolver(), this::isImageType);
-
+    protected ViewGroup createContentPreviewView(ViewGroup parent) {
         ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
-                previewType,
-                targetIntent,
                 getResources(),
                 getLayoutInflater(),
-                mChooserActionFactory,
-                parent,
-                imageLoader,
-                mEnterTransitionAnimationDelegate,
-                getContentResolver(),
-                this::isImageType);
+                parent);
 
         if (layout != null) {
             adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
@@ -1223,7 +1180,7 @@
 
                     @Override
                     public View buildContentPreview(ViewGroup parent) {
-                        return createContentPreviewView(parent, mPreviewImageLoader);
+                        return createContentPreviewView(parent);
                     }
 
                     @Override
@@ -1350,6 +1307,40 @@
         return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize);
     }
 
+    private ChooserActionFactory createChooserActionFactory() {
+        return 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();
+                });
+    }
+
     private void handleScroll(View view, int x, int y, int oldx, int oldy) {
         if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
             mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy);
@@ -1712,10 +1703,10 @@
             // We don't show it in landscape as otherwise there is no room for scrolling.
             // If the sticky content preview will be shown at some point with orientation change,
             // then always preload it to avoid subsequent resizing of the share sheet.
-            ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+            ViewGroup contentPreviewContainer =
+                    findViewById(com.android.internal.R.id.content_preview_container);
             if (contentPreviewContainer.getChildCount() == 0) {
-                ViewGroup contentPreviewView =
-                        createContentPreviewView(contentPreviewContainer, mPreviewImageLoader);
+                ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
                 contentPreviewContainer.addView(contentPreviewView);
             }
         }
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
index f7ab595..1f606f2 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -24,6 +24,7 @@
 import android.util.HashedStringCache;
 import android.util.Log;
 
+import com.android.intentresolver.contentpreview.ContentPreviewType;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.InstanceIdSequence;
@@ -432,11 +433,11 @@
      */
     private static int typeFromPreviewInt(int previewType) {
         switch(previewType) {
-            case ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE:
+            case ContentPreviewType.CONTENT_PREVIEW_IMAGE:
                 return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE;
-            case ChooserContentPreviewUi.CONTENT_PREVIEW_FILE:
+            case ContentPreviewType.CONTENT_PREVIEW_FILE:
                 return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE;
-            case ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT:
+            case ContentPreviewType.CONTENT_PREVIEW_TEXT:
             default:
                 return FrameworkStatsLog
                         .SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN;
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
deleted file mode 100644
index 60ea012..0000000
--- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
+++ /dev/null
@@ -1,699 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import static android.content.ContentProvider.getUserIdFromUri;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.annotation.IntDef;
-import android.content.ClipData;
-import android.content.ContentResolver;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.provider.DocumentsContract;
-import android.provider.Downloads;
-import android.provider.OpenableColumns;
-import android.text.TextUtils;
-import android.text.util.Linkify;
-import android.transition.TransitionManager;
-import android.util.Log;
-import android.util.PluralsMessageFormatter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.LayoutRes;
-import androidx.annotation.Nullable;
-
-import com.android.intentresolver.flags.FeatureFlagRepository;
-import com.android.intentresolver.flags.Flags;
-import com.android.intentresolver.widget.ActionRow;
-import com.android.intentresolver.widget.ImagePreviewView;
-import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
-import com.android.intentresolver.widget.RoundedRectImageView;
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-/**
- * Collection of helpers for building the content preview UI displayed in {@link ChooserActivity}.
- *
- * TODO: this "namespace" was pulled out of {@link ChooserActivity} as a bucket of static methods
- * to show that they're one-shot procedures with no dependencies back to {@link ChooserActivity}
- * state other than the delegates that are explicitly provided. There may be more appropriate
- * abstractions (e.g., maybe this can be a "widget" added directly to the view hierarchy to show the
- * appropriate preview), or it may at least be safe (and more convenient) to adopt a more "object
- * oriented" design where the static specifiers are removed and some of the dependencies are cached
- * as ivars when this "class" is initialized.
- */
-public final class ChooserContentPreviewUi {
-    private static final int IMAGE_FADE_IN_MILLIS = 150;
-
-    /**
-     * Delegate to build the default system action buttons to display in the preview layout, if/when
-     * they're determined to be appropriate for the particular preview we display.
-     * TODO: clarify why action buttons are part of preview logic.
-     */
-    public interface ActionFactory {
-        /** Create an action that copies the share content to the clipboard. */
-        ActionRow.Action createCopyButton();
-
-        /** Create an action that opens the share content in a system-default editor. */
-        @Nullable
-        ActionRow.Action createEditButton();
-
-        /** Create an "Share to Nearby" action. */
-        @Nullable
-        ActionRow.Action createNearbyButton();
-
-        /** Create custom actions */
-        List<ActionRow.Action> createCustomActions();
-
-        /**
-         * Provides a share modification action, if any.
-         */
-        @Nullable
-        Runnable getModifyShareAction();
-
-        /**
-         * <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>
-         */
-        Consumer<Boolean> getExcludeSharedTextAction();
-    }
-
-    /**
-     * Testing shim to specify whether a given mime type is considered to be an "image."
-     *
-     * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
-     * then migrate {@link ChooserActivity#isImageType(String)} into this class.
-     */
-    public interface ImageMimeTypeClassifier {
-        /** @return whether the specified {@code mimeType} is classified as an "image" type. */
-        boolean isImageType(String mimeType);
-    }
-
-    @Retention(SOURCE)
-    @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
-    private @interface ContentPreviewType {
-    }
-
-    // Starting at 1 since 0 is considered "undefined" for some of the database transformations
-    // of tron logs.
-    @VisibleForTesting
-    public static final int CONTENT_PREVIEW_IMAGE = 1;
-    @VisibleForTesting
-    public static final int CONTENT_PREVIEW_FILE = 2;
-    @VisibleForTesting
-    public static final int CONTENT_PREVIEW_TEXT = 3;
-
-    private static final String TAG = "ChooserPreview";
-
-    private static final String PLURALS_COUNT  = "count";
-    private static final String PLURALS_FILE_NAME = "file_name";
-
-    private final FeatureFlagRepository mFeatureFlagRepository;
-
-    /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
-    @ContentPreviewType
-    public static int findPreferredContentPreview(
-            Intent targetIntent,
-            ContentResolver resolver,
-            ImageMimeTypeClassifier imageClassifier) {
-        /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
-         * that broadly covers all data being shared, such as {@literal *}/* when sending an image
-         * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
-         * FILE, TEXT.  */
-        String action = targetIntent.getAction();
-        if (Intent.ACTION_SEND.equals(action)) {
-            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            return findPreferredContentPreview(uri, resolver, imageClassifier);
-        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
-            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-            if (uris == null || uris.isEmpty()) {
-                return CONTENT_PREVIEW_TEXT;
-            }
-
-            for (Uri uri : uris) {
-                // Defaulting to file preview when there are mixed image/file types is
-                // preferable, as it shows the user the correct number of items being shared
-                int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
-                if (uriPreviewType == CONTENT_PREVIEW_FILE) {
-                    return CONTENT_PREVIEW_FILE;
-                }
-            }
-
-            return CONTENT_PREVIEW_IMAGE;
-        }
-
-        return CONTENT_PREVIEW_TEXT;
-    }
-
-    public ChooserContentPreviewUi(
-            FeatureFlagRepository featureFlagRepository) {
-        mFeatureFlagRepository = featureFlagRepository;
-    }
-
-    /**
-     * Display a content preview of the specified {@code previewType} to preview the content of the
-     * specified {@code intent}.
-     */
-    public ViewGroup displayContentPreview(
-            @ContentPreviewType int previewType,
-            Intent targetIntent,
-            Resources resources,
-            LayoutInflater layoutInflater,
-            ActionFactory actionFactory,
-            ViewGroup parent,
-            ImageLoader previewImageLoader,
-            TransitionElementStatusCallback transitionElementStatusCallback,
-            ContentResolver contentResolver,
-            ImageMimeTypeClassifier imageClassifier) {
-        ViewGroup layout = null;
-
-        if (previewType != CONTENT_PREVIEW_IMAGE) {
-            transitionElementStatusCallback.onAllTransitionElementsReady();
-        }
-        int actionRowLayout = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)
-                ? R.layout.scrollable_chooser_action_row
-                : R.layout.chooser_action_row;
-        List<ActionRow.Action> customActions = actionFactory.createCustomActions();
-        switch (previewType) {
-            case CONTENT_PREVIEW_TEXT:
-                layout = displayTextContentPreview(
-                        targetIntent,
-                        layoutInflater,
-                        createActions(
-                                createTextPreviewActions(actionFactory),
-                                customActions),
-                        parent,
-                        previewImageLoader,
-                        actionRowLayout);
-                break;
-            case CONTENT_PREVIEW_IMAGE:
-                layout = displayImageContentPreview(
-                        targetIntent,
-                        layoutInflater,
-                        createActions(
-                                createImagePreviewActions(actionFactory),
-                                customActions),
-                        parent,
-                        previewImageLoader,
-                        transitionElementStatusCallback,
-                        contentResolver,
-                        imageClassifier,
-                        actionRowLayout,
-                        actionFactory);
-                break;
-            case CONTENT_PREVIEW_FILE:
-                layout = displayFileContentPreview(
-                        targetIntent,
-                        resources,
-                        layoutInflater,
-                        createActions(
-                                createFilePreviewActions(actionFactory),
-                                customActions),
-                        parent,
-                        previewImageLoader,
-                        contentResolver,
-                        actionRowLayout);
-                break;
-            default:
-                Log.e(TAG, "Unexpected content preview type: " + previewType);
-        }
-        Runnable modifyShareAction = actionFactory.getModifyShareAction();
-        if (modifyShareAction != null && layout != null
-                && mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) {
-            View modifyShareView = layout.findViewById(R.id.reselection_action);
-            if (modifyShareView != null) {
-                modifyShareView.setVisibility(View.VISIBLE);
-                modifyShareView.setOnClickListener(view -> modifyShareAction.run());
-            }
-        }
-
-        return layout;
-    }
-
-    private List<ActionRow.Action> createActions(
-            List<ActionRow.Action> systemActions, List<ActionRow.Action> customActions) {
-        ArrayList<ActionRow.Action> actions =
-                new ArrayList<>(systemActions.size() + customActions.size());
-        actions.addAll(systemActions);
-        if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)) {
-            actions.addAll(customActions);
-        }
-        return actions;
-    }
-
-    private static Cursor queryResolver(ContentResolver resolver, Uri uri) {
-        return resolver.query(uri, null, null, null, null);
-    }
-
-    @ContentPreviewType
-    private static int findPreferredContentPreview(
-            Uri uri, ContentResolver resolver, ImageMimeTypeClassifier imageClassifier) {
-        if (uri == null) {
-            return CONTENT_PREVIEW_TEXT;
-        }
-
-        String mimeType = resolver.getType(uri);
-        return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
-    }
-
-    private static ViewGroup displayTextContentPreview(
-            Intent targetIntent,
-            LayoutInflater layoutInflater,
-            List<ActionRow.Action> actions,
-            ViewGroup parent,
-            ImageLoader previewImageLoader,
-            @LayoutRes int actionRowLayout) {
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_text, parent, false);
-
-        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
-        if (actionRow != null) {
-            actionRow.setActions(actions);
-        }
-
-        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
-        if (sharingText == null) {
-            contentPreviewLayout
-                    .findViewById(com.android.internal.R.id.content_preview_text_layout)
-                    .setVisibility(View.GONE);
-        } else {
-            TextView textView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_text);
-            textView.setText(sharingText);
-        }
-
-        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
-        if (TextUtils.isEmpty(previewTitle)) {
-            contentPreviewLayout
-                    .findViewById(com.android.internal.R.id.content_preview_title_layout)
-                    .setVisibility(View.GONE);
-        } else {
-            TextView previewTitleView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_title);
-            previewTitleView.setText(previewTitle);
-
-            ClipData previewData = targetIntent.getClipData();
-            Uri previewThumbnail = null;
-            if (previewData != null) {
-                if (previewData.getItemCount() > 0) {
-                    ClipData.Item previewDataItem = previewData.getItemAt(0);
-                    previewThumbnail = previewDataItem.getUri();
-                }
-            }
-
-            ImageView previewThumbnailView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_thumbnail);
-            if (!validForContentPreview(previewThumbnail)) {
-                previewThumbnailView.setVisibility(View.GONE);
-            } else {
-                previewImageLoader.loadImage(
-                        previewThumbnail,
-                        (bitmap) -> updateViewWithImage(
-                                contentPreviewLayout.findViewById(
-                                    com.android.internal.R.id.content_preview_thumbnail),
-                                bitmap));
-            }
-        }
-
-        return contentPreviewLayout;
-    }
-
-    private static List<ActionRow.Action> createTextPreviewActions(ActionFactory actionFactory) {
-        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
-        actions.add(actionFactory.createCopyButton());
-        ActionRow.Action nearbyAction = actionFactory.createNearbyButton();
-        if (nearbyAction != null) {
-            actions.add(nearbyAction);
-        }
-        return actions;
-    }
-
-    private ViewGroup displayImageContentPreview(
-            Intent targetIntent,
-            LayoutInflater layoutInflater,
-            List<ActionRow.Action> actions,
-            ViewGroup parent,
-            ImageLoader imageLoader,
-            TransitionElementStatusCallback transitionElementStatusCallback,
-            ContentResolver contentResolver,
-            ImageMimeTypeClassifier imageClassifier,
-            @LayoutRes int actionRowLayout,
-            ActionFactory actionFactory) {
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_image, parent, false);
-        ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
-
-        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
-        if (actionRow != null) {
-            actionRow.setActions(actions);
-        }
-
-        String action = targetIntent.getAction();
-        // TODO: why don't we use image classifier for single-element ACTION_SEND?
-        final List<Uri> imageUris = Intent.ACTION_SEND.equals(action)
-                ? extractContentUris(targetIntent)
-                : extractContentUris(targetIntent)
-                        .stream()
-                        .filter(uri ->
-                                imageClassifier.isImageType(contentResolver.getType(uri))
-                        )
-                        .collect(Collectors.toList());
-
-        if (imageUris.size() == 0) {
-            Log.i(TAG, "Attempted to display image preview area with zero"
-                    + " available images detected in EXTRA_STREAM list");
-            ((View) imagePreview).setVisibility(View.GONE);
-            transitionElementStatusCallback.onAllTransitionElementsReady();
-            return contentPreviewLayout;
-        }
-
-        setTextInImagePreviewVisibility(
-                contentPreviewLayout,
-                targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
-                actionFactory);
-        imagePreview.setTransitionElementStatusCallback(transitionElementStatusCallback);
-        imagePreview.setImages(imageUris, imageLoader);
-        imageLoader.prePopulate(imageUris);
-
-        return contentPreviewLayout;
-    }
-
-    private void setTextInImagePreviewVisibility(
-            ViewGroup contentPreview, CharSequence text, ActionFactory actionFactory) {
-        int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW)
-                && !TextUtils.isEmpty(text)
-                        ? View.VISIBLE
-                        : View.GONE;
-
-        final TextView textView = contentPreview
-                .requireViewById(com.android.internal.R.id.content_preview_text);
-        CheckBox actionView = contentPreview
-                .requireViewById(R.id.include_text_action);
-        textView.setVisibility(visibility);
-        boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(text.toString());
-        textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
-        textView.setText(text);
-
-        if (visibility == View.VISIBLE) {
-            final int[] actionLabels = isLink
-                    ? new int[] { R.string.include_link, R.string.exclude_link }
-                    : new int[] { R.string.include_text, R.string.exclude_text };
-            final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction();
-            actionView.setChecked(true);
-            actionView.setText(actionLabels[1]);
-            shareTextAction.accept(false);
-            actionView.setOnCheckedChangeListener((view, isChecked) -> {
-                view.setText(actionLabels[isChecked ? 1 : 0]);
-                TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent());
-                textView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
-                shareTextAction.accept(!isChecked);
-            });
-        }
-        actionView.setVisibility(visibility);
-    }
-
-    private static List<ActionRow.Action> createImagePreviewActions(
-            ActionFactory buttonFactory) {
-        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
-        //TODO: add copy action;
-        ActionRow.Action action = buttonFactory.createNearbyButton();
-        if (action != null) {
-            actions.add(action);
-        }
-        action = buttonFactory.createEditButton();
-        if (action != null) {
-            actions.add(action);
-        }
-        return actions;
-    }
-
-    private ImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
-        ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub);
-        if (stub != null) {
-            int layoutId =
-                    mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)
-                            ? R.layout.scrollable_image_preview_view
-                            : R.layout.chooser_image_preview_view;
-            stub.setLayoutResource(layoutId);
-            stub.inflate();
-        }
-        return previewLayout.findViewById(
-                com.android.internal.R.id.content_preview_image_area);
-    }
-
-    private static ViewGroup displayFileContentPreview(
-            Intent targetIntent,
-            Resources resources,
-            LayoutInflater layoutInflater,
-            List<ActionRow.Action> actions,
-            ViewGroup parent,
-            ImageLoader imageLoader,
-            ContentResolver contentResolver,
-            @LayoutRes int actionRowLayout) {
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_file, parent, false);
-
-        List<Uri> uris = extractContentUris(targetIntent);
-        final int uriCount = uris.size();
-
-        if (uriCount == 0) {
-            contentPreviewLayout.setVisibility(View.GONE);
-            Log.i(TAG,
-                    "Appears to be no uris available in EXTRA_STREAM, removing "
-                            + "preview area");
-            return contentPreviewLayout;
-        }
-
-        if (uriCount == 1) {
-            loadFileUriIntoView(uris.get(0), contentPreviewLayout, imageLoader, contentResolver);
-        } else {
-            FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
-            int remUriCount = uriCount - 1;
-            Map<String, Object> arguments = new HashMap<>();
-            arguments.put(PLURALS_COUNT, remUriCount);
-            arguments.put(PLURALS_FILE_NAME, fileInfo.name);
-            String fileName =
-                    PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
-
-            TextView fileNameView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_filename);
-            fileNameView.setText(fileName);
-
-            View thumbnailView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_file_thumbnail);
-            thumbnailView.setVisibility(View.GONE);
-
-            ImageView fileIconView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_file_icon);
-            fileIconView.setVisibility(View.VISIBLE);
-            fileIconView.setImageResource(R.drawable.ic_file_copy);
-        }
-
-        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
-        if (actionRow != null) {
-            actionRow.setActions(actions);
-        }
-
-        return contentPreviewLayout;
-    }
-
-    private static List<Uri> extractContentUris(Intent targetIntent) {
-        List<Uri> uris = new ArrayList<>();
-        if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
-            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            if (validForContentPreview(uri)) {
-                uris.add(uri);
-            }
-        } else {
-            List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-            if (receivedUris != null) {
-                for (Uri uri : receivedUris) {
-                    if (validForContentPreview(uri)) {
-                        uris.add(uri);
-                    }
-                }
-            }
-        }
-        return uris;
-    }
-
-    /**
-     * Indicate if the incoming content URI should be allowed.
-     *
-     * @param uri the uri to test
-     * @return true if the URI is allowed for content preview
-     */
-    private static boolean validForContentPreview(Uri uri) throws SecurityException {
-        if (uri == null) {
-            return false;
-        }
-        int userId = getUserIdFromUri(uri, UserHandle.USER_CURRENT);
-        if (userId != UserHandle.USER_CURRENT && userId != UserHandle.myUserId()) {
-            Log.e(TAG, "dropped invalid content URI belonging to user " + userId);
-            return false;
-        }
-        return true;
-    }
-
-
-    private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
-        List<ActionRow.Action> actions = new ArrayList<>(1);
-        //TODO(b/120417119):
-        // add action buttonFactory.createCopyButton()
-        ActionRow.Action action = actionFactory.createNearbyButton();
-        if (action != null) {
-            actions.add(action);
-        }
-        return actions;
-    }
-
-    private static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
-        final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
-        if (stub != null) {
-            stub.setLayoutResource(actionRowLayout);
-            stub.inflate();
-        }
-        return parent.findViewById(com.android.internal.R.id.chooser_action_row);
-    }
-
-    private static void logContentPreviewWarning(Uri uri) {
-        // The ContentResolver already logs the exception. Log something more informative.
-        Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
-                + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
-                + "and set your Intent's clipData and flags in accordance with that method's "
-                + "documentation");
-    }
-
-    private static void loadFileUriIntoView(
-            final Uri uri,
-            final View parent,
-            final ImageLoader imageLoader,
-            final ContentResolver contentResolver) {
-        FileInfo fileInfo = extractFileInfo(uri, contentResolver);
-
-        TextView fileNameView = parent.findViewById(
-                com.android.internal.R.id.content_preview_filename);
-        fileNameView.setText(fileInfo.name);
-
-        if (fileInfo.hasThumbnail) {
-            imageLoader.loadImage(
-                    uri,
-                    (bitmap) -> updateViewWithImage(
-                            parent.findViewById(
-                                    com.android.internal.R.id.content_preview_file_thumbnail),
-                            bitmap));
-        } else {
-            View thumbnailView = parent.findViewById(
-                    com.android.internal.R.id.content_preview_file_thumbnail);
-            thumbnailView.setVisibility(View.GONE);
-
-            ImageView fileIconView = parent.findViewById(
-                    com.android.internal.R.id.content_preview_file_icon);
-            fileIconView.setVisibility(View.VISIBLE);
-            fileIconView.setImageResource(R.drawable.chooser_file_generic);
-        }
-    }
-
-    private static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
-        if (image == null) {
-            imageView.setVisibility(View.GONE);
-            return;
-        }
-        imageView.setVisibility(View.VISIBLE);
-        imageView.setAlpha(0.0f);
-        imageView.setImageBitmap(image);
-
-        ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
-        fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-        fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
-        fadeAnim.start();
-    }
-
-    private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
-        String fileName = null;
-        boolean hasThumbnail = false;
-
-        try (Cursor cursor = queryResolver(resolver, uri)) {
-            if (cursor != null && cursor.getCount() > 0) {
-                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
-                int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
-                int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
-
-                cursor.moveToFirst();
-                if (nameIndex != -1) {
-                    fileName = cursor.getString(nameIndex);
-                } else if (titleIndex != -1) {
-                    fileName = cursor.getString(titleIndex);
-                }
-
-                if (flagsIndex != -1) {
-                    hasThumbnail = (cursor.getInt(flagsIndex)
-                            & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
-                }
-            }
-        } catch (SecurityException | NullPointerException e) {
-            logContentPreviewWarning(uri);
-        }
-
-        if (TextUtils.isEmpty(fileName)) {
-            fileName = uri.getPath();
-            int index = fileName.lastIndexOf('/');
-            if (index != -1) {
-                fileName = fileName.substring(index + 1);
-            }
-        }
-
-        return new FileInfo(fileName, hasThumbnail);
-    }
-
-    private static class FileInfo {
-        public final String name;
-        public final boolean hasThumbnail;
-
-        FileInfo(String name, boolean hasThumbnail) {
-            this.name = name;
-            this.hasThumbnail = hasThumbnail;
-        }
-    }
-}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 7a0c0f1..d224299 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -1581,7 +1581,7 @@
      * @param cti TargetInfo to be launched.
      * @param user User to launch this activity as.
      */
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
     public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
         safelyStartActivityAsUser(cti, user, null);
     }
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
new file mode 100644
index 0000000..205be44
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ContentInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Collection of helpers for building the content preview UI displayed in
+ * {@link com.android.intentresolver.ChooserActivity}.
+ *
+ * A content preview façade.
+ */
+public final class ChooserContentPreviewUi {
+    /**
+     * Delegate to build the default system action buttons to display in the preview layout, if/when
+     * they're determined to be appropriate for the particular preview we display.
+     * TODO: clarify why action buttons are part of preview logic.
+     */
+    public interface ActionFactory {
+        /** Create an action that copies the share content to the clipboard. */
+        ActionRow.Action createCopyButton();
+
+        /** Create an action that opens the share content in a system-default editor. */
+        @Nullable
+        ActionRow.Action createEditButton();
+
+        /** Create an "Share to Nearby" action. */
+        @Nullable
+        ActionRow.Action createNearbyButton();
+
+        /** Create custom actions */
+        List<ActionRow.Action> createCustomActions();
+
+        /**
+         * Provides a share modification action, if any.
+         */
+        @Nullable
+        Runnable getModifyShareAction();
+
+        /**
+         * <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>
+         */
+        Consumer<Boolean> getExcludeSharedTextAction();
+    }
+
+    /**
+     * Testing shim to specify whether a given mime type is considered to be an "image."
+     *
+     * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
+     * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this
+     * class.
+     */
+    public interface ImageMimeTypeClassifier {
+        /** @return whether the specified {@code mimeType} is classified as an "image" type. */
+        boolean isImageType(String mimeType);
+    }
+
+    private final ContentPreviewUi mContentPreviewUi;
+
+    public ChooserContentPreviewUi(
+            Intent targetIntent,
+            ContentInterface contentResolver,
+            ImageMimeTypeClassifier imageClassifier,
+            ImageLoader imageLoader,
+            ActionFactory actionFactory,
+            TransitionElementStatusCallback transitionElementStatusCallback,
+            FeatureFlagRepository featureFlagRepository) {
+
+        mContentPreviewUi = createContentPreview(
+                targetIntent,
+                contentResolver,
+                imageClassifier,
+                imageLoader,
+                actionFactory,
+                transitionElementStatusCallback,
+                featureFlagRepository);
+        if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) {
+            transitionElementStatusCallback.onAllTransitionElementsReady();
+        }
+    }
+
+    private ContentPreviewUi createContentPreview(
+            Intent targetIntent,
+            ContentInterface contentResolver,
+            ImageMimeTypeClassifier imageClassifier,
+            ImageLoader imageLoader,
+            ActionFactory actionFactory,
+            TransitionElementStatusCallback transitionElementStatusCallback,
+            FeatureFlagRepository featureFlagRepository) {
+        int type = findPreferredContentPreview(targetIntent, contentResolver, imageClassifier);
+        switch (type) {
+            case CONTENT_PREVIEW_TEXT:
+                return createTextPreview(
+                        targetIntent, actionFactory, imageLoader, featureFlagRepository);
+
+            case CONTENT_PREVIEW_FILE:
+                return new FileContentPreviewUi(
+                        extractContentUris(targetIntent),
+                        actionFactory,
+                        imageLoader,
+                        contentResolver,
+                        featureFlagRepository);
+
+            case CONTENT_PREVIEW_IMAGE:
+                return createImagePreview(
+                        targetIntent,
+                        actionFactory,
+                        contentResolver,
+                        imageClassifier,
+                        imageLoader,
+                        transitionElementStatusCallback,
+                        featureFlagRepository);
+        }
+
+        return new NoContextPreviewUi(type);
+    }
+
+    public int getPreferredContentPreview() {
+        return mContentPreviewUi.getType();
+    }
+
+    /**
+     * Display a content preview of the specified {@code previewType} to preview the content of the
+     * specified {@code intent}.
+     */
+    public ViewGroup displayContentPreview(
+            Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+
+        return mContentPreviewUi.display(resources, layoutInflater, parent);
+    }
+
+    /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
+    @ContentPreviewType
+    private static int findPreferredContentPreview(
+            Intent targetIntent,
+            ContentInterface resolver,
+            ImageMimeTypeClassifier imageClassifier) {
+        /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
+         * that broadly covers all data being shared, such as {@literal *}/* when sending an image
+         * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
+         * FILE, TEXT.  */
+        final String action = targetIntent.getAction();
+        final String type = targetIntent.getType();
+        final boolean isSend = Intent.ACTION_SEND.equals(action);
+        final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action);
+
+        if (!(isSend || isSendMultiple)
+                || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) {
+            return CONTENT_PREVIEW_TEXT;
+        }
+
+        if (isSend) {
+            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+            return findPreferredContentPreview(uri, resolver, imageClassifier);
+        }
+
+        List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+        if (uris == null || uris.isEmpty()) {
+            return CONTENT_PREVIEW_TEXT;
+        }
+
+        for (Uri uri : uris) {
+            // Defaulting to file preview when there are mixed image/file types is
+            // preferable, as it shows the user the correct number of items being shared
+            int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
+            if (uriPreviewType == CONTENT_PREVIEW_FILE) {
+                return CONTENT_PREVIEW_FILE;
+            }
+        }
+
+        return CONTENT_PREVIEW_IMAGE;
+    }
+
+    @ContentPreviewType
+    private static int findPreferredContentPreview(
+            Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier) {
+        if (uri == null) {
+            return CONTENT_PREVIEW_TEXT;
+        }
+
+        String mimeType = null;
+        try {
+            mimeType = resolver.getType(uri);
+        } catch (RemoteException ignored) {
+        }
+        return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
+    }
+
+    private static TextContentPreviewUi createTextPreview(
+            Intent targetIntent,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            ImageLoader imageLoader,
+            FeatureFlagRepository featureFlagRepository) {
+        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+        ClipData previewData = targetIntent.getClipData();
+        Uri previewThumbnail = null;
+        if (previewData != null) {
+            if (previewData.getItemCount() > 0) {
+                ClipData.Item previewDataItem = previewData.getItemAt(0);
+                previewThumbnail = previewDataItem.getUri();
+            }
+        }
+        return new TextContentPreviewUi(
+                sharingText,
+                previewTitle,
+                previewThumbnail,
+                actionFactory,
+                imageLoader,
+                featureFlagRepository);
+    }
+
+    static ImageContentPreviewUi createImagePreview(
+            Intent targetIntent,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            ContentInterface contentResolver,
+            ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier,
+            ImageLoader imageLoader,
+            ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
+            FeatureFlagRepository featureFlagRepository) {
+        CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+        String action = targetIntent.getAction();
+        // TODO: why don't we use image classifier for single-element ACTION_SEND?
+        final List<Uri> imageUris = Intent.ACTION_SEND.equals(action)
+                ? extractContentUris(targetIntent)
+                : extractContentUris(targetIntent)
+                        .stream()
+                        .filter(uri -> {
+                            String type = null;
+                            try {
+                                type = contentResolver.getType(uri);
+                            } catch (RemoteException ignored) {
+                            }
+                            return imageClassifier.isImageType(type);
+                        })
+                        .collect(Collectors.toList());
+        return new ImageContentPreviewUi(
+                imageUris,
+                text,
+                actionFactory,
+                imageLoader,
+                transitionElementStatusCallback,
+                featureFlagRepository);
+    }
+
+    private static List<Uri> extractContentUris(Intent targetIntent) {
+        List<Uri> uris = new ArrayList<>();
+        if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
+            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+            if (ContentPreviewUi.validForContentPreview(uri)) {
+                uris.add(uri);
+            }
+        } else {
+            List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+            if (receivedUris != null) {
+                for (Uri uri : receivedUris) {
+                    if (ContentPreviewUi.validForContentPreview(uri)) {
+                        uris.add(uri);
+                    }
+                }
+            }
+        }
+        return uris;
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
new file mode 100644
index 0000000..ebab147
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
@@ -0,0 +1,35 @@
+/*
+ * 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.contentpreview;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+@Retention(SOURCE)
+@IntDef({ContentPreviewType.CONTENT_PREVIEW_FILE,
+        ContentPreviewType.CONTENT_PREVIEW_IMAGE,
+        ContentPreviewType.CONTENT_PREVIEW_TEXT})
+public @interface ContentPreviewType {
+    // Starting at 1 since 0 is considered "undefined" for some of the database transformations
+    // of tron logs.
+    int CONTENT_PREVIEW_IMAGE = 1;
+    int CONTENT_PREVIEW_FILE = 2;
+    int CONTENT_PREVIEW_TEXT = 3;
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
new file mode 100644
index 0000000..39856e6
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -0,0 +1,130 @@
+/*
+ * 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.contentpreview;
+
+import static android.content.ContentProvider.getUserIdFromUri;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.LayoutRes;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.RoundedRectImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class ContentPreviewUi {
+    private static final int IMAGE_FADE_IN_MILLIS = 150;
+    static final String TAG = "ChooserPreview";
+
+    @ContentPreviewType
+    public abstract int getType();
+
+    public abstract ViewGroup display(
+            Resources resources, LayoutInflater layoutInflater, ViewGroup parent);
+
+    protected static int getActionRowLayout(FeatureFlagRepository featureFlagRepository) {
+        return featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)
+                ? R.layout.scrollable_chooser_action_row
+                : R.layout.chooser_action_row;
+    }
+
+    protected static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
+        final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
+        if (stub != null) {
+            stub.setLayoutResource(actionRowLayout);
+            stub.inflate();
+        }
+        return parent.findViewById(com.android.internal.R.id.chooser_action_row);
+    }
+
+    protected static List<ActionRow.Action> createActions(
+            List<ActionRow.Action> systemActions,
+            List<ActionRow.Action> customActions,
+            FeatureFlagRepository featureFlagRepository) {
+        ArrayList<ActionRow.Action> actions =
+                new ArrayList<>(systemActions.size() + customActions.size());
+        actions.addAll(systemActions);
+        if (featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)) {
+            actions.addAll(customActions);
+        }
+        return actions;
+    }
+
+    /**
+     * Indicate if the incoming content URI should be allowed.
+     *
+     * @param uri the uri to test
+     * @return true if the URI is allowed for content preview
+     */
+    protected static boolean validForContentPreview(Uri uri) throws SecurityException {
+        if (uri == null) {
+            return false;
+        }
+        int userId = getUserIdFromUri(uri, UserHandle.USER_CURRENT);
+        if (userId != UserHandle.USER_CURRENT && userId != UserHandle.myUserId()) {
+            Log.e(ContentPreviewUi.TAG, "dropped invalid content URI belonging to user " + userId);
+            return false;
+        }
+        return true;
+    }
+
+    protected static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
+        if (image == null) {
+            imageView.setVisibility(View.GONE);
+            return;
+        }
+        imageView.setVisibility(View.VISIBLE);
+        imageView.setAlpha(0.0f);
+        imageView.setImageBitmap(image);
+
+        ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
+        fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+        fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+        fadeAnim.start();
+    }
+
+    protected static void displayPayloadReselectionAction(
+            ViewGroup layout,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            FeatureFlagRepository featureFlagRepository) {
+        Runnable modifyShareAction = actionFactory.getModifyShareAction();
+        if (modifyShareAction != null && layout != null
+                && featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) {
+            View modifyShareView = layout.findViewById(R.id.reselection_action);
+            if (modifyShareView != null) {
+                modifyShareView.setVisibility(View.VISIBLE);
+                modifyShareView.setOnClickListener(view -> modifyShareAction.run());
+            }
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
new file mode 100644
index 0000000..7cd7147
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -0,0 +1,236 @@
+/*
+ * 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.contentpreview;
+
+import android.content.ContentInterface;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PluralsMessageFormatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class FileContentPreviewUi extends ContentPreviewUi {
+    private static final String PLURALS_COUNT  = "count";
+    private static final String PLURALS_FILE_NAME = "file_name";
+
+    private final List<Uri> mUris;
+    private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+    private final ImageLoader mImageLoader;
+    private final ContentInterface mContentResolver;
+    private final FeatureFlagRepository mFeatureFlagRepository;
+
+    FileContentPreviewUi(List<Uri> uris,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            ImageLoader imageLoader,
+            ContentInterface contentResolver,
+            FeatureFlagRepository featureFlagRepository) {
+        mUris = uris;
+        mActionFactory = actionFactory;
+        mImageLoader = imageLoader;
+        mContentResolver = contentResolver;
+        mFeatureFlagRepository = featureFlagRepository;
+    }
+
+    @Override
+    public int getType() {
+        return ContentPreviewType.CONTENT_PREVIEW_FILE;
+    }
+
+    @Override
+    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+        ViewGroup layout = displayInternal(resources, layoutInflater, parent);
+        displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+        return layout;
+    }
+
+    private ViewGroup displayInternal(
+            Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+        @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_file, parent, false);
+
+        final int uriCount = mUris.size();
+
+        if (uriCount == 0) {
+            contentPreviewLayout.setVisibility(View.GONE);
+            Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM,"
+                    + " removing preview area");
+            return contentPreviewLayout;
+        }
+
+        if (uriCount == 1) {
+            loadFileUriIntoView(mUris.get(0), contentPreviewLayout, mImageLoader, mContentResolver);
+        } else {
+            FileInfo fileInfo = extractFileInfo(mUris.get(0), mContentResolver);
+            int remUriCount = uriCount - 1;
+            Map<String, Object> arguments = new HashMap<>();
+            arguments.put(PLURALS_COUNT, remUriCount);
+            arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+            String fileName =
+                    PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
+
+            TextView fileNameView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_filename);
+            fileNameView.setText(fileName);
+
+            View thumbnailView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_file_thumbnail);
+            thumbnailView.setVisibility(View.GONE);
+
+            ImageView fileIconView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_file_icon);
+            fileIconView.setVisibility(View.VISIBLE);
+            fileIconView.setImageResource(R.drawable.ic_file_copy);
+        }
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(
+                    createActions(
+                            createFilePreviewActions(),
+                            mActionFactory.createCustomActions(),
+                            mFeatureFlagRepository));
+        }
+
+        return contentPreviewLayout;
+    }
+
+    private List<ActionRow.Action> createFilePreviewActions() {
+        List<ActionRow.Action> actions = new ArrayList<>(1);
+        //TODO(b/120417119):
+        // add action buttonFactory.createCopyButton()
+        ActionRow.Action action = mActionFactory.createNearbyButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        return actions;
+    }
+
+    private static void loadFileUriIntoView(
+            final Uri uri,
+            final View parent,
+            final ImageLoader imageLoader,
+            final ContentInterface contentResolver) {
+        FileInfo fileInfo = extractFileInfo(uri, contentResolver);
+
+        TextView fileNameView = parent.findViewById(
+                com.android.internal.R.id.content_preview_filename);
+        fileNameView.setText(fileInfo.name);
+
+        if (fileInfo.hasThumbnail) {
+            imageLoader.loadImage(
+                    uri,
+                    (bitmap) -> updateViewWithImage(
+                            parent.findViewById(
+                                    com.android.internal.R.id.content_preview_file_thumbnail),
+                            bitmap));
+        } else {
+            View thumbnailView = parent.findViewById(
+                    com.android.internal.R.id.content_preview_file_thumbnail);
+            thumbnailView.setVisibility(View.GONE);
+
+            ImageView fileIconView = parent.findViewById(
+                    com.android.internal.R.id.content_preview_file_icon);
+            fileIconView.setVisibility(View.VISIBLE);
+            fileIconView.setImageResource(R.drawable.chooser_file_generic);
+        }
+    }
+
+    private static FileInfo extractFileInfo(Uri uri, ContentInterface resolver) {
+        String fileName = null;
+        boolean hasThumbnail = false;
+
+        try (Cursor cursor = queryResolver(resolver, uri)) {
+            if (cursor != null && cursor.getCount() > 0) {
+                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
+                int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
+
+                cursor.moveToFirst();
+                if (nameIndex != -1) {
+                    fileName = cursor.getString(nameIndex);
+                } else if (titleIndex != -1) {
+                    fileName = cursor.getString(titleIndex);
+                }
+
+                if (flagsIndex != -1) {
+                    hasThumbnail = (cursor.getInt(flagsIndex)
+                            & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
+                }
+            }
+        } catch (SecurityException | NullPointerException e) {
+            // The ContentResolver already logs the exception. Log something more informative.
+            Log.w(
+                    TAG,
+                    "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
+                    + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+                    + "and set your Intent's clipData and flags in accordance with that method's "
+                    + "documentation");
+        }
+
+        if (TextUtils.isEmpty(fileName)) {
+            fileName = uri.getPath();
+            fileName = fileName == null ? "" : fileName;
+            int index = fileName.lastIndexOf('/');
+            if (index != -1) {
+                fileName = fileName.substring(index + 1);
+            }
+        }
+
+        return new FileInfo(fileName, hasThumbnail);
+    }
+
+    private static Cursor queryResolver(ContentInterface resolver, Uri uri) {
+        try {
+            return resolver.query(uri, null, null, null);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    private static class FileInfo {
+        public final String name;
+        public final boolean hasThumbnail;
+
+        FileInfo(String name, boolean hasThumbnail) {
+            this.name = name;
+            this.hasThumbnail = hasThumbnail;
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
new file mode 100644
index 0000000..db26ab1
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
@@ -0,0 +1,179 @@
+/*
+ * 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.contentpreview;
+
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
+
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.text.util.Linkify;
+import android.transition.TransitionManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+class ImageContentPreviewUi extends ContentPreviewUi {
+    private final List<Uri> mImageUris;
+    @Nullable
+    private final CharSequence mText;
+    private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+    private final ImageLoader mImageLoader;
+    private final ImagePreviewView.TransitionElementStatusCallback mTransitionElementStatusCallback;
+    private final FeatureFlagRepository mFeatureFlagRepository;
+
+    ImageContentPreviewUi(
+            List<Uri> imageUris,
+            @Nullable CharSequence text,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            ImageLoader imageLoader,
+            ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
+            FeatureFlagRepository featureFlagRepository) {
+        mImageUris = imageUris;
+        mText = text;
+        mActionFactory = actionFactory;
+        mImageLoader = imageLoader;
+        mTransitionElementStatusCallback = transitionElementStatusCallback;
+        mFeatureFlagRepository = featureFlagRepository;
+
+        mImageLoader.prePopulate(mImageUris);
+    }
+
+    @Override
+    public int getType() {
+        return CONTENT_PREVIEW_IMAGE;
+    }
+
+    @Override
+    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+        ViewGroup layout = displayInternal(layoutInflater, parent);
+        displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+        return layout;
+    }
+
+    private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+        @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_image, parent, false);
+        ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(
+                    createActions(
+                            createImagePreviewActions(),
+                            mActionFactory.createCustomActions(),
+                            mFeatureFlagRepository));
+        }
+
+        if (mImageUris.size() == 0) {
+            Log.i(
+                    TAG,
+                    "Attempted to display image preview area with zero"
+                        + " available images detected in EXTRA_STREAM list");
+            ((View) imagePreview).setVisibility(View.GONE);
+            mTransitionElementStatusCallback.onAllTransitionElementsReady();
+            return contentPreviewLayout;
+        }
+
+        setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory);
+        imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
+        imagePreview.setImages(mImageUris, mImageLoader);
+
+        return contentPreviewLayout;
+    }
+
+    private List<ActionRow.Action> createImagePreviewActions() {
+        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+        //TODO: add copy action;
+        ActionRow.Action action = mActionFactory.createNearbyButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        action = mActionFactory.createEditButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        return actions;
+    }
+
+    private ImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
+        ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub);
+        if (stub != null) {
+            int layoutId =
+                    mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)
+                            ? R.layout.scrollable_image_preview_view
+                            : R.layout.chooser_image_preview_view;
+            stub.setLayoutResource(layoutId);
+            stub.inflate();
+        }
+        return previewLayout.findViewById(
+                com.android.internal.R.id.content_preview_image_area);
+    }
+
+    private void setTextInImagePreviewVisibility(
+            ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) {
+        int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW)
+                && !TextUtils.isEmpty(mText)
+                ? View.VISIBLE
+                : View.GONE;
+
+        final TextView textView = contentPreview
+                .requireViewById(com.android.internal.R.id.content_preview_text);
+        CheckBox actionView = contentPreview
+                .requireViewById(R.id.include_text_action);
+        textView.setVisibility(visibility);
+        boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString());
+        textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
+        textView.setText(mText);
+
+        if (visibility == View.VISIBLE) {
+            final int[] actionLabels = isLink
+                    ? new int[] { R.string.include_link, R.string.exclude_link }
+                    : new int[] { R.string.include_text, R.string.exclude_text };
+            final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction();
+            actionView.setChecked(true);
+            actionView.setText(actionLabels[1]);
+            shareTextAction.accept(false);
+            actionView.setOnCheckedChangeListener((view, isChecked) -> {
+                view.setText(actionLabels[isChecked ? 1 : 0]);
+                TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent());
+                textView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+                shareTextAction.accept(!isChecked);
+            });
+        }
+        actionView.setVisibility(visibility);
+    }
+}
diff --git a/java/src/com/android/intentresolver/HttpUriMatcher.kt b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
similarity index 81%
rename from java/src/com/android/intentresolver/HttpUriMatcher.kt
rename to java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
index 0f59df2..8023253 100644
--- a/java/src/com/android/intentresolver/HttpUriMatcher.kt
+++ b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
@@ -15,15 +15,13 @@
  */
 
 @file:JvmName("HttpUriMatcher")
-package com.android.intentresolver
+package com.android.intentresolver.contentpreview
 
-import com.android.internal.annotations.VisibleForTesting
 import java.net.URI
 
-@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-fun String.isHttpUri() =
+internal fun String.isHttpUri() =
     kotlin.runCatching {
         URI(this).scheme.takeIf { scheme ->
             "http".compareTo(scheme, true) == 0 || "https".compareTo(scheme, true) == 0
         }
-    }.getOrNull() != null
\ No newline at end of file
+    }.getOrNull() != null
diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
new file mode 100644
index 0000000..9001693
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.contentpreview
+
+import android.content.res.Resources
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.ViewGroup
+
+internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {
+    override fun getType(): Int = type
+
+    override fun display(
+        resources: Resources?, layoutInflater: LayoutInflater?, parent: ViewGroup?
+    ): ViewGroup? {
+        Log.e(TAG, "Unexpected content preview type: $type")
+        return null
+    }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
new file mode 100644
index 0000000..7901e4c
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
@@ -0,0 +1,138 @@
+/*
+ * 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.contentpreview;
+
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class TextContentPreviewUi extends ContentPreviewUi {
+    @Nullable
+    private final CharSequence mSharingText;
+    @Nullable
+    private final CharSequence mPreviewTitle;
+    @Nullable
+    private final Uri mPreviewThumbnail;
+    private final ImageLoader mImageLoader;
+    private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+    private final FeatureFlagRepository mFeatureFlagRepository;
+
+    TextContentPreviewUi(
+            @Nullable CharSequence sharingText,
+            @Nullable CharSequence previewTitle,
+            @Nullable Uri previewThumbnail,
+            ChooserContentPreviewUi.ActionFactory actionFactory,
+            ImageLoader imageLoader,
+            FeatureFlagRepository featureFlagRepository) {
+        mSharingText = sharingText;
+        mPreviewTitle = previewTitle;
+        mPreviewThumbnail = previewThumbnail;
+        mImageLoader = imageLoader;
+        mActionFactory = actionFactory;
+        mFeatureFlagRepository = featureFlagRepository;
+    }
+
+    @Override
+    public int getType() {
+        return ContentPreviewType.CONTENT_PREVIEW_TEXT;
+    }
+
+    @Override
+    public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+        ViewGroup layout = displayInternal(layoutInflater, parent);
+        displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+        return layout;
+    }
+
+    private ViewGroup displayInternal(
+            LayoutInflater layoutInflater,
+            ViewGroup parent) {
+        @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_text, parent, false);
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(
+                    createActions(
+                            createTextPreviewActions(),
+                            mActionFactory.createCustomActions(),
+                            mFeatureFlagRepository));
+        }
+
+        if (mSharingText == null) {
+            contentPreviewLayout
+                    .findViewById(com.android.internal.R.id.content_preview_text_layout)
+                    .setVisibility(View.GONE);
+        } else {
+            TextView textView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_text);
+            textView.setText(mSharingText);
+        }
+
+        if (TextUtils.isEmpty(mPreviewTitle)) {
+            contentPreviewLayout
+                    .findViewById(com.android.internal.R.id.content_preview_title_layout)
+                    .setVisibility(View.GONE);
+        } else {
+            TextView previewTitleView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_title);
+            previewTitleView.setText(mPreviewTitle);
+
+            ImageView previewThumbnailView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_thumbnail);
+            if (!validForContentPreview(mPreviewThumbnail)) {
+                previewThumbnailView.setVisibility(View.GONE);
+            } else {
+                mImageLoader.loadImage(
+                        mPreviewThumbnail,
+                        (bitmap) -> updateViewWithImage(
+                                contentPreviewLayout.findViewById(
+                                        com.android.internal.R.id.content_preview_thumbnail),
+                                bitmap));
+            }
+        }
+
+        return contentPreviewLayout;
+    }
+
+    private List<ActionRow.Action> createTextPreviewActions() {
+        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+        actions.add(mActionFactory.createCopyButton());
+        ActionRow.Action nearbyAction = mActionFactory.createNearbyButton();
+        if (nearbyAction != null) {
+            actions.add(nearbyAction);
+        }
+        return actions;
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
index 7d1b248..aa42c24 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
@@ -36,6 +36,7 @@
 import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent;
 import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent;
 import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent;
+import com.android.intentresolver.contentpreview.ContentPreviewType;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
@@ -117,7 +118,7 @@
         final int appProvidedDirectTargets = 123;
         final int appProvidedAppTargets = 456;
         final boolean workProfile = true;
-        final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_FILE;
+        final int previewType = ContentPreviewType.CONTENT_PREVIEW_FILE;
         final String intentAction = Intent.ACTION_SENDTO;
         final int numCustomActions = 3;
         final boolean modifyShareProvided = true;
@@ -233,7 +234,7 @@
 
     @Test
     public void testLogActionShareWithPreview() {
-        final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT;
+        final int previewType = ContentPreviewType.CONTENT_PREVIEW_TEXT;
 
         mChooserLogger.logActionShareWithPreview(previewType);
 
diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
index 5a159d2..b904771 100644
--- a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
+++ b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.flags.ReleasedFlag
 import com.android.systemui.flags.UnreleasedFlag
 
-internal class TestFeatureFlagRepository(
+class TestFeatureFlagRepository(
     private val overrides: Map<BooleanFlag, Boolean>
 ) : FeatureFlagRepository {
     override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag)
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
new file mode 100644
index 0000000..d870a8c
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.contentpreview
+
+import android.content.ClipDescription
+import android.content.ContentInterface
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import com.android.intentresolver.ImageLoader
+import com.android.intentresolver.TestFeatureFlagRepository
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
+import com.android.intentresolver.flags.Flags
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.android.intentresolver.widget.ImagePreviewView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.function.Consumer
+
+private const val PROVIDER_NAME = "org.pkg.app"
+class ChooserContentPreviewUiTest {
+    private val contentResolver = mock<ContentInterface>()
+    private val imageClassifier = ChooserContentPreviewUi.ImageMimeTypeClassifier { mimeType ->
+        mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*")
+    }
+    private val imageLoader = object : ImageLoader {
+        override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+            callback.accept(null)
+        }
+        override fun prePopulate(uris: List<Uri>) = Unit
+        override suspend fun invoke(uri: Uri): Bitmap? = null
+    }
+    private val actionFactory = object : ActionFactory {
+        override fun createCopyButton() = ActionRow.Action(label = "Copy", icon = null) {}
+        override fun createEditButton(): ActionRow.Action? = null
+        override fun createNearbyButton(): ActionRow.Action? = null
+        override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+        override fun getModifyShareAction(): Runnable? = null
+        override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+    }
+    private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>()
+    private val featureFlagRepository = TestFeatureFlagRepository(
+        mapOf(
+            Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW to true
+        )
+    )
+
+    @Test
+    fun test_ChooserContentPreview_non_send_intent_action_to_text_preview() {
+        val targetIntent = Intent(Intent.ACTION_VIEW)
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
+        verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+    }
+
+    @Test
+    fun test_ChooserContentPreview_text_mime_type_to_text_preview() {
+        val targetIntent = Intent(Intent.ACTION_SEND).apply {
+            type = "text/plain"
+            putExtra(Intent.EXTRA_TEXT, "Text Extra")
+        }
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
+        verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+    }
+
+    @Test
+    fun test_ChooserContentPreview_single_image_uri_to_image_preview() {
+        val uri = Uri.parse("content://$PROVIDER_NAME/test.png")
+        val targetIntent = Intent(Intent.ACTION_SEND).apply {
+            putExtra(Intent.EXTRA_STREAM, uri)
+        }
+        whenever(contentResolver.getType(uri)).thenReturn("image/png")
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+        verify(transitionCallback, never()).onAllTransitionElementsReady()
+    }
+
+    @Test
+    fun test_ChooserContentPreview_single_non_image_uri_to_file_preview() {
+        val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf")
+        val targetIntent = Intent(Intent.ACTION_SEND).apply {
+            putExtra(Intent.EXTRA_STREAM, uri)
+        }
+        whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
+        verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+    }
+
+    @Test
+    fun test_ChooserContentPreview_multiple_image_uri_to_image_preview() {
+        val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png")
+        val uri2 = Uri.parse("content://$PROVIDER_NAME/test.jpg")
+        val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+            putExtra(
+                Intent.EXTRA_STREAM,
+                ArrayList<Uri>().apply {
+                    add(uri1)
+                    add(uri2)
+                }
+            )
+        }
+        whenever(contentResolver.getType(uri1)).thenReturn("image/png")
+        whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg")
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+        verify(transitionCallback, never()).onAllTransitionElementsReady()
+    }
+
+    @Test
+    fun test_ChooserContentPreview_some_non_image_uri_to_file_preview() {
+        val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png")
+        val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf")
+        val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+            putExtra(
+                Intent.EXTRA_STREAM,
+                ArrayList<Uri>().apply {
+                    add(uri1)
+                    add(uri2)
+                }
+            )
+        }
+        whenever(contentResolver.getType(uri1)).thenReturn("image/png")
+        whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
+        val testSubject = ChooserContentPreviewUi(
+            targetIntent,
+            contentResolver,
+            imageClassifier,
+            imageLoader,
+            actionFactory,
+            transitionCallback,
+            featureFlagRepository
+        )
+        assertThat(testSubject.preferredContentPreview)
+            .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
+        verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+    }
+}