diff options
Diffstat (limited to 'java/src')
10 files changed, 637 insertions, 359 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 82cfd8ef..84e14d72 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -86,6 +86,7 @@ import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.contentpreview.PreviewDataProvider; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; @@ -272,8 +273,9 @@ public class ChooserActivity extends ResolverActivity implements }); mChooserContentPreviewUi = new ChooserContentPreviewUi( + getLifecycle(), + createPreviewDataProvider(), mChooserRequest.getTargetIntent(), - getContentResolver(), this::isImageType, createPreviewImageLoader(), createChooserActionFactory(), @@ -536,6 +538,14 @@ public class ChooserActivity extends ResolverActivity implements mMaxTargetsPerRow); } + private PreviewDataProvider createPreviewDataProvider() { + // TODO: move this into a ViewModel so it could survive orientation change + return new PreviewDataProvider( + mChooserRequest.getTargetIntent(), + getContentResolver(), + this::isImageType); + } + private int findSelectedProfile() { int selectedProfile = getSelectedProfileExtra(); if (selectedProfile == -1) { diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index e9476909..55131de2 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -16,55 +16,36 @@ package com.android.intentresolver.contentpreview; -import static android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL; - +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.util.UriFilters.isOwnedByCurrentUser; +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.database.Cursor; -import android.media.MediaMetadata; import android.net.Uri; -import android.provider.DocumentsContract; -import android.provider.Downloads; -import android.provider.OpenableColumns; import android.text.TextUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; -import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * 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 { - /** - * A set of metadata columns we read for a content URI (see [readFileMetadata] method). - */ - @VisibleForTesting - static final String[] METADATA_COLUMNS = new String[] { - DocumentsContract.Document.COLUMN_FLAGS, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, - OpenableColumns.DISPLAY_NAME, - Downloads.Impl.COLUMN_TITLE - }; + private final Lifecycle mLifecycle; /** * Delegate to build the default system action buttons to display in the preview layout, if/when @@ -100,20 +81,22 @@ public final class ChooserContentPreviewUi { Consumer<Boolean> getExcludeSharedTextAction(); } - private final ContentPreviewUi mContentPreviewUi; + @VisibleForTesting + final ContentPreviewUi mContentPreviewUi; public ChooserContentPreviewUi( + Lifecycle lifecycle, + PreviewDataProvider previewData, Intent targetIntent, - ContentInterface contentResolver, MimeTypeClassifier imageClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator) { - + mLifecycle = lifecycle; mContentPreviewUi = createContentPreview( + previewData, targetIntent, - contentResolver, imageClassifier, imageLoader, actionFactory, @@ -125,63 +108,62 @@ public final class ChooserContentPreviewUi { } private ContentPreviewUi createContentPreview( + PreviewDataProvider previewData, Intent targetIntent, - ContentInterface contentResolver, MimeTypeClassifier typeClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator) { - /* 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/*"))) { + int previewType = previewData.getPreviewType(); + if (previewType == CONTENT_PREVIEW_TEXT) { return createTextPreview( targetIntent, actionFactory, imageLoader, headlineGenerator); } - List<Uri> uris = extractContentUris(targetIntent); - if (uris.isEmpty()) { - return createTextPreview( - targetIntent, + if (previewType == CONTENT_PREVIEW_FILE) { + FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi( + previewData.getUriCount(), actionFactory, - imageLoader, headlineGenerator); + if (previewData.getUriCount() > 0) { + previewData.getFirstFileName( + mLifecycle, fileContentPreviewUi::setFirstFileName); + } + return fileContentPreviewUi; } - ArrayList<FileInfo> files = new ArrayList<>(uris.size()); - int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); + boolean isSingleImageShare = previewData.getUriCount() == 1 + && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType()); CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); if (!TextUtils.isEmpty(text)) { - return new FilesPlusTextContentPreviewUi(files, - targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), - actionFactory, - imageLoader, - typeClassifier, - headlineGenerator); - } - if (previewCount == 0) { - return new FileContentPreviewUi( - files, - actionFactory, - headlineGenerator); + FilesPlusTextContentPreviewUi previewUi = + new FilesPlusTextContentPreviewUi( + isSingleImageShare, + previewData.getUriCount(), + targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), + actionFactory, + imageLoader, + typeClassifier, + headlineGenerator); + if (previewData.getUriCount() > 0) { + previewData.getFileMetadataForImagePreview( + mLifecycle, previewUi::updatePreviewMetadata); + } + return previewUi; } - return new UnifiedContentPreviewUi( - files, + + UnifiedContentPreviewUi unifiedContentPreviewUi = new UnifiedContentPreviewUi( + isSingleImageShare, actionFactory, imageLoader, typeClassifier, transitionElementStatusCallback, headlineGenerator); + previewData.getFileMetadataForImagePreview(mLifecycle, unifiedContentPreviewUi::setFiles); + return unifiedContentPreviewUi; } public int getPreferredContentPreview() { @@ -198,100 +180,6 @@ public final class ChooserContentPreviewUi { return mContentPreviewUi.display(resources, layoutInflater, parent); } - private static int readFileInfo( - ContentInterface contentResolver, - MimeTypeClassifier typeClassifier, - List<Uri> uris, - List<FileInfo> fileInfos) { - int previewCount = 0; - for (Uri uri: uris) { - FileInfo fileInfo = getFileInfo(contentResolver, typeClassifier, uri); - if (fileInfo.getPreviewUri() != null) { - previewCount++; - } - fileInfos.add(fileInfo); - } - return previewCount; - } - - private static FileInfo getFileInfo( - ContentInterface resolver, MimeTypeClassifier typeClassifier, Uri uri) { - FileInfo.Builder builder = new FileInfo.Builder(uri) - .withName(getFileName(uri)); - String mimeType = getType(resolver, uri); - builder.withMimeType(mimeType); - if (typeClassifier.isImageType(mimeType)) { - return builder.withPreviewUri(uri).build(); - } - readOtherFileTypes(resolver, uri, typeClassifier, builder); - if (builder.getPreviewUri() == null) { - readFileMetadata(resolver, uri, builder); - } - return builder.build(); - } - - private static void readFileMetadata( - ContentInterface resolver, Uri uri, FileInfo.Builder builder) { - Cursor cursor = query(resolver, uri); - if (cursor == null || !cursor.moveToFirst()) { - return; - } - int flagColIdx = -1; - int displayIconUriColIdx = -1; - int nameColIndex = -1; - int titleColIndex = -1; - String[] columns = cursor.getColumnNames(); - // TODO: double-check why Cursor#getColumnInded didn't work - for (int i = 0; i < columns.length; i++) { - String columnName = columns[i]; - if (DocumentsContract.Document.COLUMN_FLAGS.equals(columnName)) { - flagColIdx = i; - } else if (MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI.equals(columnName)) { - displayIconUriColIdx = i; - } else if (OpenableColumns.DISPLAY_NAME.equals(columnName)) { - nameColIndex = i; - } else if (Downloads.Impl.COLUMN_TITLE.equals(columnName)) { - titleColIndex = i; - } - } - String fileName = ""; - if (nameColIndex >= 0) { - fileName = cursor.getString(nameColIndex); - } else if (titleColIndex >= 0) { - fileName = cursor.getString(titleColIndex); - } - if (!TextUtils.isEmpty(fileName)) { - builder.withName(fileName); - } - - Uri previewUri = null; - if (flagColIdx >= 0 && ((cursor.getInt(flagColIdx) & FLAG_SUPPORTS_THUMBNAIL) != 0)) { - previewUri = uri; - } else if (displayIconUriColIdx >= 0) { - String uriStr = cursor.getString(displayIconUriColIdx); - previewUri = uriStr == null ? null : Uri.parse(uriStr); - } - if (previewUri != null) { - builder.withPreviewUri(previewUri); - } - } - - private static void readOtherFileTypes( - ContentInterface resolver, - Uri uri, - MimeTypeClassifier typeClassifier, - FileInfo.Builder builder) { - String[] otherTypes = getStreamTypes(resolver, uri); - if (otherTypes != null && otherTypes.length > 0) { - for (String mimeType : otherTypes) { - if (typeClassifier.isImageType(mimeType)) { - builder.withPreviewUri(uri); - break; - } - } - } - } - private static TextContentPreviewUi createTextPreview( Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, @@ -315,77 +203,4 @@ public final class ChooserContentPreviewUi { imageLoader, headlineGenerator); } - - 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 (isOwnedByCurrentUser(uri)) { - uris.add(uri); - } - } else { - List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - if (receivedUris != null) { - for (Uri uri : receivedUris) { - if (isOwnedByCurrentUser(uri)) { - uris.add(uri); - } - } - } - } - return uris; - } - - @Nullable - private static String getType(ContentInterface resolver, Uri uri) { - try { - return resolver.getType(uri); - } catch (SecurityException e) { - logProviderPermissionWarning(uri, "mime type"); - } catch (Throwable t) { - Log.e(ContentPreviewUi.TAG, "Failed to read content type, uri: " + uri, t); - } - return null; - } - - @Nullable - private static Cursor query(ContentInterface resolver, Uri uri) { - try { - return resolver.query(uri, METADATA_COLUMNS, null, null); - } catch (SecurityException e) { - logProviderPermissionWarning(uri, "metadata"); - } catch (Throwable t) { - Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: " + uri, t); - } - return null; - } - - @Nullable - private static String[] getStreamTypes(ContentInterface resolver, Uri uri) { - try { - return resolver.getStreamTypes(uri, "*/*"); - } catch (SecurityException e) { - logProviderPermissionWarning(uri, "stream types"); - } catch (Throwable t) { - Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: " + uri, t); - } - return null; - } - - private static String getFileName(Uri uri) { - String fileName = uri.getPath(); - fileName = fileName == null ? "" : fileName; - int index = fileName.lastIndexOf('/'); - if (index != -1) { - fileName = fileName.substring(index + 1); - } - return fileName; - } - - private static void logProviderPermissionWarning(Uri uri, String dataName) { - // The ContentResolver already logs the exception. Log something more informative. - Log.w(ContentPreviewUi.TAG, "Could not read " + uri + " " + dataName + "." - + " If a preview is desired, call Intent#setClipData() to ensure that the" - + " sharesheet is given permission."); - } } diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index c0859e53..9699594e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -50,10 +50,10 @@ abstract class ContentPreviewUi { List<ActionRow.Action> customActions) { ArrayList<ActionRow.Action> actions = new ArrayList<>(systemActions.size() + customActions.size()); - if (customActions != null && !customActions.isEmpty()) { - actions.addAll(customActions); - } else { + if (customActions.isEmpty()) { actions.addAll(systemActions); + } else { + actions.addAll(customActions); } return actions; } diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 13f27493..16ff6c23 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -25,6 +25,8 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -35,17 +37,20 @@ 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<FileInfo> mFiles; + @Nullable + private String mFirstFileName = null; + private final int mFileCount; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final HeadlineGenerator mHeadlineGenerator; + @Nullable + private ViewGroup mContentPreview = null; FileContentPreviewUi( - List<FileInfo> files, + int fileCount, ChooserContentPreviewUi.ActionFactory actionFactory, HeadlineGenerator headlineGenerator) { - mFiles = files; + mFileCount = fileCount; mActionFactory = actionFactory; mHeadlineGenerator = headlineGenerator; } @@ -55,6 +60,13 @@ class FileContentPreviewUi extends ContentPreviewUi { return ContentPreviewType.CONTENT_PREVIEW_FILE; } + public void setFirstFileName(String fileName) { + mFirstFileName = fileName; + if (mContentPreview != null) { + showFileName(mContentPreview, fileName); + } + } + @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(resources, layoutInflater, parent); @@ -64,48 +76,50 @@ class FileContentPreviewUi extends ContentPreviewUi { private ViewGroup displayInternal( Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { - ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( + mContentPreview = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_file, parent, false); - final int uriCount = mFiles.size(); + displayHeadline(mContentPreview, mHeadlineGenerator.getFilesHeadline(mFileCount)); - displayHeadline(contentPreviewLayout, mHeadlineGenerator.getFilesHeadline(mFiles.size())); - - if (uriCount == 0) { - contentPreviewLayout.setVisibility(View.GONE); + if (mFileCount == 0) { + mContentPreview.setVisibility(View.GONE); Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM," + " removing preview area"); - return contentPreviewLayout; + return mContentPreview; } - FileInfo fileInfo = mFiles.get(0); - TextView fileNameView = contentPreviewLayout.findViewById( - R.id.content_preview_filename); - fileNameView.setText(fileInfo.getName()); + if (mFirstFileName != null) { + showFileName(mContentPreview, mFirstFileName); + } - TextView secondLine = contentPreviewLayout.findViewById( + TextView secondLine = mContentPreview.findViewById( R.id.content_preview_more_files); - if (uriCount > 1) { - int remUriCount = uriCount - 1; + if (mFileCount > 1) { + int remUriCount = mFileCount - 1; Map<String, Object> arguments = new HashMap<>(); arguments.put(PLURALS_COUNT, remUriCount); secondLine.setText( PluralsMessageFormatter.format(resources, arguments, R.string.more_files)); } else { - ImageView icon = contentPreviewLayout.findViewById(R.id.content_preview_file_icon); + ImageView icon = mContentPreview.findViewById(R.id.content_preview_file_icon); icon.setImageResource(R.drawable.single_file); secondLine.setVisibility(View.GONE); } final ActionRow actionRow = - contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + mContentPreview.findViewById(com.android.internal.R.id.chooser_action_row); List<ActionRow.Action> actions = createActions(new ArrayList<>(), mActionFactory.createCustomActions()); actionRow.setActions(actions); if (actions.isEmpty()) { - contentPreviewLayout.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); + mContentPreview.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); } - return contentPreviewLayout; + return mContentPreview; + } + + private void showFileName(ViewGroup contentPreview, String name) { + TextView fileNameView = contentPreview.requireViewById(R.id.content_preview_filename); + fileNameView.setText(name); } } diff --git a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt index 527bfc8e..fe35365b 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt +++ b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt @@ -16,33 +16,21 @@ package com.android.intentresolver.contentpreview import android.net.Uri +import androidx.annotation.VisibleForTesting -internal class FileInfo private constructor( - val uri: Uri, - val name: String?, - val previewUri: Uri?, - val mimeType: String? -) { +class FileInfo private constructor(val uri: Uri, val previewUri: Uri?, val mimeType: String?) { + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) class Builder(val uri: Uri) { - var name: String = "" - private set var previewUri: Uri? = null private set var mimeType: String? = null private set - fun withName(name: String): Builder = apply { - this.name = name - } - - fun withPreviewUri(uri: Uri?): Builder = apply { - previewUri = uri - } + @Synchronized fun withPreviewUri(uri: Uri?): Builder = apply { previewUri = uri } - fun withMimeType(mimeType: String?): Builder = apply { - this.mimeType = mimeType - } + @Synchronized + fun withMimeType(mimeType: String?): Builder = apply { this.mimeType = mimeType } - fun build(): FileInfo = FileInfo(uri, name, previewUri, mimeType) + @Synchronized fun build(): FileInfo = FileInfo(uri, previewUri, mimeType) } } diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java index 4fe54681..860423c4 100644 --- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -20,6 +20,7 @@ import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTE import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; import android.content.res.Resources; +import android.net.Uri; import android.text.util.Linkify; import android.util.PluralsMessageFormatter; import android.view.LayoutInflater; @@ -29,6 +30,8 @@ import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ScrollableImagePreviewView; @@ -45,44 +48,44 @@ import java.util.function.Consumer; * file content). */ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { - private final List<FileInfo> mFiles; private final CharSequence mText; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final MimeTypeClassifier mTypeClassifier; private final HeadlineGenerator mHeadlineGenerator; - private final boolean mAllImages; - private final boolean mAllVideos; + private final boolean mIsSingleImage; + private final int mFileCount; + private ViewGroup mContentPreviewView; + private boolean mIsMetadataUpdated = false; + @Nullable + private Uri mFirstFilePreviewUri; + private boolean mAllImages; + private boolean mAllVideos; FilesPlusTextContentPreviewUi( - List<FileInfo> files, + boolean isSingleImage, + int fileCount, CharSequence text, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, MimeTypeClassifier typeClassifier, HeadlineGenerator headlineGenerator) { - mFiles = files; + if (isSingleImage && fileCount != 1) { + throw new IllegalArgumentException( + "fileCount = " + fileCount + " and isSingleImage = true"); + } + mFileCount = fileCount; + mIsSingleImage = isSingleImage; mText = text; mActionFactory = actionFactory; mImageLoader = imageLoader; mTypeClassifier = typeClassifier; mHeadlineGenerator = headlineGenerator; - - boolean allImages = true; - boolean allVideos = true; - for (FileInfo fileInfo : mFiles) { - ScrollableImagePreviewView.PreviewType previewType = - getPreviewType(mTypeClassifier, fileInfo.getMimeType()); - allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; - allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; - } - mAllImages = allImages; - mAllVideos = allVideos; } @Override public int getType() { - return shouldShowPreview() ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; + return mIsSingleImage ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; } @Override @@ -92,49 +95,52 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { return layout; } + public void updatePreviewMetadata(List<FileInfo> files) { + boolean allImages = true; + boolean allVideos = true; + for (FileInfo fileInfo : files) { + ScrollableImagePreviewView.PreviewType previewType = + getPreviewType(mTypeClassifier, fileInfo.getMimeType()); + allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; + allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; + } + mAllImages = allImages; + mAllVideos = allVideos; + mFirstFilePreviewUri = files.isEmpty() ? null : files.get(0).getPreviewUri(); + mIsMetadataUpdated = true; + if (mContentPreviewView != null) { + updateUiWithMetadata(mContentPreviewView); + } + } + private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( + mContentPreviewView = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_files_text, parent, false); - ImageView imagePreview = - contentPreviewLayout.findViewById(R.id.image_view); final ActionRow actionRow = - contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row); List<ActionRow.Action> actions = createActions( createImagePreviewActions(), mActionFactory.createCustomActions()); actionRow.setActions(actions); if (actions.isEmpty()) { - contentPreviewLayout.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); + mContentPreviewView.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); } - if (shouldShowPreview()) { - mImageLoader.loadImage(mFiles.get(0).getPreviewUri(), bitmap -> { - if (bitmap == null) { - imagePreview.setVisibility(View.GONE); - } else { - imagePreview.setImageBitmap(bitmap); - } - }); - } else { - imagePreview.setVisibility(View.GONE); + if (mIsMetadataUpdated) { + updateUiWithMetadata(mContentPreviewView); + } else if (!mIsSingleImage) { + mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE); } - prepareTextPreview(contentPreviewLayout, mActionFactory); - updateHeadline(contentPreviewLayout); - - return contentPreviewLayout; - } - - private boolean shouldShowPreview() { - return mAllImages && mFiles.size() == 1 && mFiles.get(0).getPreviewUri() != null; + return mContentPreviewView; } private List<ActionRow.Action> createImagePreviewActions() { ArrayList<ActionRow.Action> actions = new ArrayList<>(2); //TODO: add copy action; - if (mFiles.size() == 1 && mAllImages) { + if (mIsSingleImage) { ActionRow.Action action = mActionFactory.createEditButton(); if (action != null) { actions.add(action); @@ -143,24 +149,42 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { return actions; } + private void updateUiWithMetadata(ViewGroup contentPreviewView) { + prepareTextPreview(contentPreviewView, mActionFactory); + updateHeadline(contentPreviewView); + + ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view); + if (mIsSingleImage && mFirstFilePreviewUri != null) { + mImageLoader.loadImage(mFirstFilePreviewUri, bitmap -> { + if (bitmap == null) { + imagePreview.setVisibility(View.GONE); + } else { + imagePreview.setImageBitmap(bitmap); + } + }); + } else { + imagePreview.setVisibility(View.GONE); + } + } + private void updateHeadline(ViewGroup contentPreview) { CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action); String headline; if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) { if (mAllImages) { - headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, mFiles.size()); + headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, mFileCount); } else if (mAllVideos) { - headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, mFiles.size()); + headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, mFileCount); } else { - headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, mFiles.size()); + headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, mFileCount); } } else { if (mAllImages) { - headline = mHeadlineGenerator.getImagesHeadline(mFiles.size()); + headline = mHeadlineGenerator.getImagesHeadline(mFileCount); } else if (mAllVideos) { - headline = mHeadlineGenerator.getVideosHeadline(mFiles.size()); + headline = mHeadlineGenerator.getVideosHeadline(mFileCount); } else { - headline = mHeadlineGenerator.getFilesHeadline(mFiles.size()); + headline = mHeadlineGenerator.getFilesHeadline(mFileCount); } } @@ -204,7 +228,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { } HashMap<String, Object> params = new HashMap<>(); - params.put("count", mFiles.size()); + params.put("count", mFileCount); return PluralsMessageFormatter.format( resources, diff --git a/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java index 44cbd52e..2de60c5b 100644 --- a/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java +++ b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java @@ -35,4 +35,11 @@ public interface MimeTypeClassifier { default boolean isVideoType(@Nullable String mimeType) { return (mimeType != null) && ClipDescription.compareMimeTypes(mimeType, "video/*"); } + + /** + * @return whether the specified {@code mimeType} is classified as "text" type + */ + default boolean isTextType(@Nullable String mimeType) { + return (mimeType != null) && ClipDescription.compareMimeTypes(mimeType, "text/*"); + } } diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt new file mode 100644 index 00000000..94db7a63 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -0,0 +1,399 @@ +/* + * 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.Intent +import android.database.Cursor +import android.media.MediaMetadata +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL +import android.provider.Downloads +import android.provider.OpenableColumns +import android.text.TextUtils +import android.util.Log +import androidx.annotation.OpenForTesting +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE +import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE +import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT +import com.android.intentresolver.measurements.runTracing +import com.android.intentresolver.util.ownedByCurrentUser +import java.util.concurrent.atomic.AtomicInteger +import java.util.function.Consumer +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull + +/** + * A set of metadata columns we read for a content URI (see + * [PreviewDataProvider.UriRecord.readQueryResult] method). + */ +@VisibleForTesting +val METADATA_COLUMNS = + arrayOf( + DocumentsContract.Document.COLUMN_FLAGS, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, + OpenableColumns.DISPLAY_NAME, + Downloads.Impl.COLUMN_TITLE + ) +private const val TIMEOUT_MS = 1_000L + +/** + * Asynchronously loads and stores shared URI metadata (see [Intent.EXTRA_STREAM]) such as mime + * type, file name, and a preview thumbnail URI. + */ +@OpenForTesting +open class PreviewDataProvider +@VisibleForTesting +constructor( + private val targetIntent: Intent, + private val contentResolver: ContentInterface, + private val typeClassifier: MimeTypeClassifier, + private val dispatcher: CoroutineDispatcher, +) { + constructor( + targetIntent: Intent, + contentResolver: ContentInterface, + typeClassifier: MimeTypeClassifier, + ) : this( + targetIntent, + contentResolver, + typeClassifier, + Dispatchers.IO, + ) + + private val records = targetIntent.contentUris.map { UriRecord(it) } + + /** returns number of shared URIs, see [Intent.EXTRA_STREAM] */ + @get:OpenForTesting + open val uriCount: Int + get() = records.size + + /** + * Preview type to use. The type is determined asynchronously with a timeout; the fall-back + * values is [ContentPreviewType.CONTENT_PREVIEW_FILE] + */ + @get:OpenForTesting + @get:ContentPreviewType + open val previewType: Int by lazy { + runTracing("preview-type") { + /* In [android.content.Intent#getType], the app may specify a very general mime type + * that broadly covers all data being shared, such as '*' when sending an image + * and text. We therefore should inspect each item for the preferred type, in order: + * IMAGE, FILE, TEXT. */ + if ( + !targetIntent.isSend || + typeClassifier.isTextType(targetIntent.type) || + records.isEmpty() + ) { + CONTENT_PREVIEW_TEXT + } else { + runBlocking(dispatcher) { + withTimeoutOrNull(TIMEOUT_MS) { + loadPreviewType() + } ?: CONTENT_PREVIEW_FILE + } + } + } + } + + /** + * The first shared URI's metadata. This call wait's for the data to be loaded and falls back to + * a crude value if the data is not loaded within a time limit. + */ + open val firstFileInfo: FileInfo? by lazy { + runTracing("first-uri-metadata") { + records.firstOrNull()?.let { record -> + runBlocking(dispatcher) { + val builder = FileInfo.Builder(record.uri) + withTimeoutOrNull(TIMEOUT_MS) { + builder.readFromRecord(record) + } + builder.build() + } + } + } + } + + /** + * Returns a collection of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] + * and [FileInfo.previewUri] set (a data projection tailored for the image preview UI). + */ + @OpenForTesting + open fun getFileMetadataForImagePreview( + callerLifecycle: Lifecycle, + callback: Consumer<List<FileInfo>>, + ) { + callerLifecycle.coroutineScope.launch { + val result = withContext(dispatcher) { + getFileMetadataForImagePreview() + } + callback.accept(result) + } + } + + private fun getFileMetadataForImagePreview(): List<FileInfo> = + runTracing("image-preview-metadata") { + ArrayList<FileInfo>(records.size).also { result -> + for (record in records) { + result.add( + FileInfo.Builder(record.uri) + .readFromRecord(record) + .build() + ) + } + } + } + + private fun FileInfo.Builder.readFromRecord(record: UriRecord): FileInfo.Builder { + withMimeType(record.mimeType) + val previewUri = + when { + record.isImageType || record.supportsImageType || record.supportsThumbnail -> + record.uri + else -> record.iconUri + } + withPreviewUri(previewUri) + return this + } + + /** + * Returns a title for the first shared URI which is read from URI metadata or, if the metadata + * is not provided, derived from the URI. + */ + @Throws(IndexOutOfBoundsException::class) + fun getFirstFileName(callerLifecycle: Lifecycle, callback: Consumer<String>) { + if (records.isEmpty()) { + throw IndexOutOfBoundsException("There are no shared URIs") + } + callerLifecycle.coroutineScope.launch { + val result = withContext(dispatcher) { + getFirstFileName() + } + callback.accept(result) + } + } + + @Throws(IndexOutOfBoundsException::class) + private fun getFirstFileName(): String { + if (records.isEmpty()) throw IndexOutOfBoundsException("There are no shared URIs") + + val record = records[0] + return if (TextUtils.isEmpty(record.title)) getFileName(record.uri) else record.title + } + + @ContentPreviewType + private suspend fun loadPreviewType(): Int { + // Execute [ContentResolver#getType()] calls sequentially as the method contains a timeout + // logic for the actual [ContentProvider#getType] call. Thus it is possible for one getType + // call's timeout work against other concurrent getType calls e.g. when a two concurrent + // calls on the caller side are scheduled on the same thread on the callee side. + records + .firstOrNull { it.isImageType } + ?.run { + return CONTENT_PREVIEW_IMAGE + } + + val resultDeferred = CompletableDeferred<Int>() + return coroutineScope { + val job = launch { + coroutineScope { + val nextIndex = AtomicInteger(0) + repeat(4) { + launch { + while (isActive) { + val i = nextIndex.getAndIncrement() + if (i >= records.size) break + val hasPreview = + with(records[i]) { + supportsImageType || supportsThumbnail || iconUri != null + } + if (hasPreview) { + resultDeferred.complete(CONTENT_PREVIEW_IMAGE) + break + } + } + } + } + } + resultDeferred.complete(CONTENT_PREVIEW_FILE) + } + resultDeferred.await() + .also { job.cancel() } + } + } + + /** + * Provides a lazy evaluation and caches results of [ContentInterface.getType], + * [ContentInterface.getStreamTypes], and [ContentInterface.query] methods for the given [uri]. + */ + private inner class UriRecord(val uri: Uri) { + val mimeType: String? by lazy { contentResolver.getTypeSafe(uri) } + val isImageType: Boolean + get() = typeClassifier.isImageType(mimeType) + val supportsImageType: Boolean by lazy { + contentResolver.getStreamTypesSafe(uri) + ?.firstOrNull(typeClassifier::isImageType) != null + } + val supportsThumbnail: Boolean + get() = query.supportsThumbnail + val title: String + get() = query.title + val iconUri: Uri? + get() = query.iconUri + + private val query by lazy { readQueryResult() } + + private fun readQueryResult(): QueryResult { + val cursor = contentResolver.querySafe(uri) + ?.takeIf { it.moveToFirst() } + ?: return QueryResult() + + var flagColIdx = -1 + var displayIconUriColIdx = -1 + var nameColIndex = -1 + var titleColIndex = -1 + // TODO: double-check why Cursor#getColumnInded didn't work + cursor.columnNames.forEachIndexed { i, columnName -> + when (columnName) { + DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i + OpenableColumns.DISPLAY_NAME -> nameColIndex = i + Downloads.Impl.COLUMN_TITLE -> titleColIndex = i + } + } + + val supportsThumbnail = + flagColIdx >= 0 && ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0) + + var title = "" + if (nameColIndex >= 0) { + title = cursor.getString(nameColIndex) ?: "" + } + if (TextUtils.isEmpty(title) && titleColIndex >= 0) { + title = cursor.getString(titleColIndex) ?: "" + } + + val iconUri = + if (displayIconUriColIdx >= 0) { + cursor.getString(displayIconUriColIdx)?.let(Uri::parse) + } else { + null + } + + return QueryResult(supportsThumbnail, title, iconUri) + } + } + + private class QueryResult( + val supportsThumbnail: Boolean = false, + val title: String = "", + val iconUri: Uri? = null + ) +} + +private val Intent.isSend: Boolean + get() = + action.let { action -> + Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action + } + +private val Intent.contentUris: ArrayList<Uri> + get() = + ArrayList<Uri>().also { uris -> + if (Intent.ACTION_SEND == action) { + getParcelableExtra<Uri>(Intent.EXTRA_STREAM) + ?.takeIf { it.ownedByCurrentUser } + ?.let { uris.add(it) } + } else { + getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.fold(uris) { accumulator, uri + -> + if (uri.ownedByCurrentUser) { + accumulator.add(uri) + } + accumulator + } + } + } + +private fun getFileName(uri: Uri): String { + val fileName = uri.path ?: return "" + val index = fileName.lastIndexOf('/') + return if (index < 0) { + fileName + } else { + fileName.substring(index + 1) + } +} + +private fun ContentInterface.getTypeSafe(uri: Uri): String? = + runTracing("getType") { + try { + getType(uri) + } catch (e: SecurityException) { + logProviderPermissionWarning(uri, "mime type") + null + } catch (t: Throwable) { + Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) + null + } + } + +private fun ContentInterface.getStreamTypesSafe(uri: Uri): Array<String>? = + runTracing("getStreamTypes") { + try { + getStreamTypes(uri, "*/*") + } catch (e: SecurityException) { + logProviderPermissionWarning(uri, "stream types") + null + } catch (t: Throwable) { + Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: $uri", t) + null + } + } + +private fun ContentInterface.querySafe(uri: Uri): Cursor? = + runTracing("query") { + try { + query(uri, METADATA_COLUMNS, null, null) + } catch (e: SecurityException) { + logProviderPermissionWarning(uri, "metadata") + null + } catch (t: Throwable) { + Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t) + null + } + } + +private fun logProviderPermissionWarning(uri: Uri, dataName: String) { + // The ContentResolver already logs the exception. Log something more informative. + Log.w( + ContentPreviewUi.TAG, + "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" + + " ensure that the sharesheet is given permission." + ) +} diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index c918a7f6..26f4d007 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -36,32 +36,30 @@ import java.util.List; import java.util.Objects; class UnifiedContentPreviewUi extends ContentPreviewUi { - private final List<FileInfo> mFiles; - @Nullable + private final boolean mShowEditAction; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final HeadlineGenerator mHeadlineGenerator; + @Nullable + private List<FileInfo> mFiles; + @Nullable + private ViewGroup mContentPreviewView; UnifiedContentPreviewUi( - List<FileInfo> files, + boolean isSingleImage, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator) { - mFiles = files; + mShowEditAction = isSingleImage; mActionFactory = actionFactory; mImageLoader = imageLoader; mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; mHeadlineGenerator = headlineGenerator; - - mImageLoader.prePopulate(mFiles.stream() - .map(FileInfo::getPreviewUri) - .filter(Objects::nonNull) - .toList()); } @Override @@ -76,74 +74,88 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return layout; } + public void setFiles(List<FileInfo> files) { + mImageLoader.prePopulate(files.stream() + .map(FileInfo::getPreviewUri) + .filter(Objects::nonNull) + .toList()); + mFiles = files; + if (mContentPreviewView != null) { + updatePreviewWithFiles(mContentPreviewView, files); + } + } + private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( + mContentPreviewView = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); - ScrollableImagePreviewView imagePreview = - contentPreviewLayout.findViewById(R.id.scrollable_image_preview); final ActionRow actionRow = - contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row); List<ActionRow.Action> actions = createActions( createImagePreviewActions(), mActionFactory.createCustomActions()); actionRow.setActions(actions); if (actions.isEmpty()) { - contentPreviewLayout.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); + mContentPreviewView.findViewById(R.id.actions_top_divider).setVisibility(View.GONE); } + ScrollableImagePreviewView imagePreview = + mContentPreviewView.requireViewById(R.id.scrollable_image_preview); + imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE)); + imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); + + if (mFiles != null) { + updatePreviewWithFiles(mContentPreviewView, mFiles); + } - if (mFiles.size() == 0) { + return mContentPreviewView; + } + + private void updatePreviewWithFiles(ViewGroup contentPreviewView, List<FileInfo> files) { + final int count = files.size(); + ScrollableImagePreviewView imagePreview = + contentPreviewView.requireViewById(R.id.scrollable_image_preview); + if (count == 0) { Log.i( TAG, "Attempted to display image preview area with zero" - + " available images detected in EXTRA_STREAM list"); + + " available images detected in EXTRA_STREAM list"); imagePreview.setVisibility(View.GONE); mTransitionElementStatusCallback.onAllTransitionElementsReady(); - return contentPreviewLayout; + return; } List<ScrollableImagePreviewView.Preview> previews = new ArrayList<>(); - boolean allImages = !mFiles.isEmpty(); - boolean allVideos = !mFiles.isEmpty(); - for (FileInfo fileInfo : mFiles) { + boolean allImages = true; + boolean allVideos = true; + for (FileInfo fileInfo : files) { ScrollableImagePreviewView.PreviewType previewType = getPreviewType(mTypeClassifier, fileInfo.getMimeType()); allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; if (fileInfo.getPreviewUri() != null) { - previews.add(new ScrollableImagePreviewView.Preview( - previewType, - fileInfo.getPreviewUri())); + previews.add( + new ScrollableImagePreviewView.Preview( + previewType, fileInfo.getPreviewUri())); } } - imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE)); - imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); - imagePreview.setPreviews( - previews, - mFiles.size() - previews.size(), - mImageLoader); + imagePreview.setPreviews(previews, count - previews.size(), mImageLoader); if (allImages) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + displayHeadline(contentPreviewView, mHeadlineGenerator.getImagesHeadline(count)); } else if (allVideos) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); + displayHeadline(contentPreviewView, mHeadlineGenerator.getVideosHeadline(count)); } else { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getFilesHeadline(mFiles.size())); + displayHeadline(contentPreviewView, mHeadlineGenerator.getFilesHeadline(count)); } - - return contentPreviewLayout; } private List<ActionRow.Action> createImagePreviewActions() { ArrayList<ActionRow.Action> actions = new ArrayList<>(1); //TODO: add copy action; - if (mFiles.size() == 1 && mTypeClassifier.isImageType(mFiles.get(0).getMimeType())) { + if (mShowEditAction) { ActionRow.Action action = mActionFactory.createEditButton(); if (action != null) { actions.add(action); diff --git a/java/src/com/android/intentresolver/measurements/Tracer.kt b/java/src/com/android/intentresolver/measurements/Tracer.kt index 168bda0e..f7e01879 100644 --- a/java/src/com/android/intentresolver/measurements/Tracer.kt +++ b/java/src/com/android/intentresolver/measurements/Tracer.kt @@ -16,8 +16,8 @@ package com.android.intentresolver.measurements -import android.os.Trace import android.os.SystemClock +import android.os.Trace import android.util.Log import java.util.concurrent.atomic.AtomicLong @@ -44,3 +44,12 @@ object Tracer { private fun elapsedTimeNow() = SystemClock.elapsedRealtime() } + +inline fun <R> runTracing(name: String, block: () -> R): R { + Trace.beginSection(name) + try { + return block() + } finally { + Trace.endSection() + } +} |