From 21ab7fb0b67fe3b5c16fc26b5cf6d016bfc0e248 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 6 Mar 2023 12:01:27 -0800 Subject: 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 --- .../intentresolver/ChooserActionFactory.java | 1 + .../android/intentresolver/ChooserActivity.java | 111 ++-- .../intentresolver/ChooserActivityLogger.java | 7 +- .../intentresolver/ChooserContentPreviewUi.java | 699 --------------------- .../com/android/intentresolver/HttpUriMatcher.kt | 29 - .../android/intentresolver/ResolverActivity.java | 2 +- .../contentpreview/ChooserContentPreviewUi.java | 310 +++++++++ .../contentpreview/ContentPreviewType.java | 35 ++ .../contentpreview/ContentPreviewUi.java | 130 ++++ .../contentpreview/FileContentPreviewUi.java | 236 +++++++ .../contentpreview/ImageContentPreviewUi.java | 179 ++++++ .../intentresolver/contentpreview/IsHttpUri.kt | 27 + .../contentpreview/NoContextPreviewUi.kt | 33 + .../contentpreview/TextContentPreviewUi.java | 138 ++++ 14 files changed, 1145 insertions(+), 792 deletions(-) delete mode 100644 java/src/com/android/intentresolver/ChooserContentPreviewUi.java delete mode 100644 java/src/com/android/intentresolver/HttpUriMatcher.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java create mode 100644 java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java create mode 100644 java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java create mode 100644 java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java create mode 100644 java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java create mode 100644 java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 14d59720..947155f3 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -39,6 +39,7 @@ import android.view.View; 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 3a7c892e..ae5be26d 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.NoCrossProfileEmptyStateProvider.DevicePolicyB 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 @@ public class ChooserActivity extends ResolverActivity implements private ChooserRefinementManager mRefinementManager; private FeatureFlagRepository mFeatureFlagRepository; - private ChooserActionFactory mChooserActionFactory; private ChooserContentPreviewUi mChooserContentPreviewUi; private boolean mShouldDisplayLandscape; @@ -231,9 +231,6 @@ public class ChooserActivity extends ResolverActivity implements private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5); - @Nullable - private ImageLoader mPreviewImageLoader; - private int mScrollStatus = SCROLL_STATUS_IDLE; @VisibleForTesting @@ -273,38 +270,6 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements finish(); }); - mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository); + mChooserContentPreviewUi = new ChooserContentPreviewUi( + mChooserRequest.getTargetIntent(), + getContentResolver(), + this::isImageType, + createPreviewImageLoader(), + createChooserActionFactory(), + mEnterTransitionAnimationDelegate, + mFeatureFlagRepository); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); @@ -339,8 +311,6 @@ public class ChooserActivity extends ResolverActivity implements mChooserRequest.getTargetIntentFilter()), mChooserRequest.getTargetIntentFilter()); - mPreviewImageLoader = createPreviewImageLoader(); - super.onCreate( savedInstanceState, mChooserRequest.getTargetIntent(), @@ -392,8 +362,7 @@ public class ChooserActivity extends ResolverActivity implements (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 @@ public class ChooserActivity extends ResolverActivity implements || mChooserMultiProfilePagerAdapter .getCurrentRootAdapter().getSystemRowCount() != 0) { getChooserActivityLogger().logActionShareWithPreview( - ChooserContentPreviewUi.findPreferredContentPreview( - getTargetIntent(), getContentResolver(), this::isImageType)); + mChooserContentPreviewUi.getPreferredContentPreview()); } return postRebuildListInternal(rebuildCompleted); } @@ -717,22 +685,11 @@ public class ChooserActivity extends ResolverActivity implements * @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 @@ public class ChooserActivity extends ResolverActivity implements @Override public View buildContentPreview(ViewGroup parent) { - return createContentPreviewView(parent, mPreviewImageLoader); + return createContentPreviewView(parent); } @Override @@ -1350,6 +1307,40 @@ public class ChooserActivity extends ResolverActivity implements 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 @@ public class ChooserActivity extends ResolverActivity implements // 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 f7ab595b..1f606f26 100644 --- a/java/src/com/android/intentresolver/ChooserActivityLogger.java +++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java @@ -24,6 +24,7 @@ import android.provider.MediaStore; 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 @@ public class ChooserActivityLogger { */ 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 60ea0122..00000000 --- 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 createCustomActions(); - - /** - * Provides a share modification action, if any. - */ - @Nullable - Runnable getModifyShareAction(); - - /** - *

- * Creates an exclude-text action that can be called when the user changes shared text - * status in the Media + Text preview. - *

- *

- * true argument value indicates that the text should be excluded. - *

- */ - Consumer 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 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 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 createActions( - List systemActions, List customActions) { - ArrayList 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 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 createTextPreviewActions(ActionFactory actionFactory) { - ArrayList 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 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 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 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 createImagePreviewActions( - ActionFactory buttonFactory) { - ArrayList 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 actions, - ViewGroup parent, - ImageLoader imageLoader, - ContentResolver contentResolver, - @LayoutRes int actionRowLayout) { - ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( - R.layout.chooser_grid_preview_file, parent, false); - - List 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 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 extractContentUris(Intent targetIntent) { - List 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 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 createFilePreviewActions(ActionFactory actionFactory) { - List 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/HttpUriMatcher.kt b/java/src/com/android/intentresolver/HttpUriMatcher.kt deleted file mode 100644 index 0f59df2b..00000000 --- a/java/src/com/android/intentresolver/HttpUriMatcher.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -@file:JvmName("HttpUriMatcher") -package com.android.intentresolver - -import com.android.internal.annotations.VisibleForTesting -import java.net.URI - -@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) -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 diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 7a0c0f1a..d224299e 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -1581,7 +1581,7 @@ public class ResolverActivity extends FragmentActivity implements * @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 00000000..205be444 --- /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 createCustomActions(); + + /** + * Provides a share modification action, if any. + */ + @Nullable + Runnable getModifyShareAction(); + + /** + *

+ * Creates an exclude-text action that can be called when the user changes shared text + * status in the Media + Text preview. + *

+ *

+ * true argument value indicates that the text should be excluded. + *

+ */ + Consumer 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 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 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 extractContentUris(Intent targetIntent) { + List 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 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 00000000..ebab147d --- /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 00000000..39856e66 --- /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 createActions( + List systemActions, + List customActions, + FeatureFlagRepository featureFlagRepository) { + ArrayList 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 00000000..7cd71475 --- /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 mUris; + private final ChooserContentPreviewUi.ActionFactory mActionFactory; + private final ImageLoader mImageLoader; + private final ContentInterface mContentResolver; + private final FeatureFlagRepository mFeatureFlagRepository; + + FileContentPreviewUi(List 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 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 createFilePreviewActions() { + List 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 00000000..db26ab1b --- /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 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 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 createImagePreviewActions() { + ArrayList 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 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/contentpreview/IsHttpUri.kt b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt new file mode 100644 index 00000000..80232537 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt @@ -0,0 +1,27 @@ +/* + * 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. + */ + +@file:JvmName("HttpUriMatcher") +package com.android.intentresolver.contentpreview + +import java.net.URI + +internal fun String.isHttpUri() = + kotlin.runCatching { + URI(this).scheme.takeIf { scheme -> + "http".compareTo(scheme, true) == 0 || "https".compareTo(scheme, true) == 0 + } + }.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 00000000..90016932 --- /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 00000000..7901e4cb --- /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 createTextPreviewActions() { + ArrayList actions = new ArrayList<>(2); + actions.add(mActionFactory.createCopyButton()); + ActionRow.Action nearbyAction = mActionFactory.createNearbyButton(); + if (nearbyAction != null) { + actions.add(nearbyAction); + } + return actions; + } +} -- cgit v1.2.3-59-g8ed1b