summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java12
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java269
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java58
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileInfo.kt26
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java120
-rw-r--r--java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java7
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt399
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java88
-rw-r--r--java/src/com/android/intentresolver/measurements/Tracer.kt11
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()
+ }
+}