summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2023-03-13 16:39:23 -0700
committer Andrey Epin <ayepin@google.com> 2023-03-14 16:07:49 -0700
commit4649ef1769d53727d59423f184cb3ee068ce40db (patch)
tree07420a85e880c372e0341f38deb89df9d0be0c20 /java/src
parente7f8555d956c8bf5f338a182d8023b2740edc389 (diff)
Add unified preview UI
ChooserContentPreviewUi applies various heuristic to determine if each shared URI has a preview and, if any, displays a scrollable preview list. Each preview item in the list is badge accroding ot its type: no badge for images, a video-file badge for videos and a generic-file badge for all others. All URIs without a previwe are groupped under a single item at list end (+N files). Collateral changes: * FileContentPreviewUi$FileInfo is moved into the package level; * ChooserContentPreviewUi$ImageMimeTypeClassifier internface is moved into the package level, renamted to MimeTypeClassifier and defines a new method, isVideoType(); * ScrollableImagePreviewView is modified to support badges and the "+N" item; * A new class, UnfifiedContentPreviewUi is clonned from ImageContentPreviewUi class, and is reponsible for drawing the new unified preview ui, ImageContentPreviewUi is used only with the legacy image content preview. Bug: 271613784 Test: manual testing Change-Id: Ia25f5a1565226ac679cc8ecefd58acb95cb60142
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java2
-rw-r--r--java/src/com/android/intentresolver/ImagePreviewImageLoader.kt10
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java306
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java101
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileInfo.kt48
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java19
-rw-r--r--java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java38
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java202
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt2
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt1
-rw-r--r--java/src/com/android/intentresolver/widget/RoundedRectImageView.java13
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt125
12 files changed, 612 insertions, 255 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index ae5be26d..37a17e79 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -714,7 +714,7 @@ public class ChooserActivity extends ResolverActivity implements
}
@VisibleForTesting
- protected boolean isImageType(String mimeType) {
+ protected boolean isImageType(@Nullable String mimeType) {
return mimeType != null && mimeType.startsWith("image/");
}
diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
index 7b6651a2..9650403e 100644
--- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
@@ -19,6 +19,7 @@ package com.android.intentresolver
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
+import android.util.Log
import android.util.Size
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
@@ -32,6 +33,8 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.function.Consumer
+private const val TAG = "ImagePreviewImageLoader"
+
@VisibleForTesting
class ImagePreviewImageLoader @JvmOverloads constructor(
private val context: Context,
@@ -79,9 +82,12 @@ class ImagePreviewImageLoader @JvmOverloads constructor(
}
private fun CompletableDeferred<Bitmap?>.loadBitmap(uri: Uri) {
- val bitmap = runCatching {
+ val bitmap = try {
context.contentResolver.loadThumbnail(uri, thumbnailSize, null)
- }.getOrNull()
+ } catch (t: Throwable) {
+ Log.d(TAG, "failed to load $uri preview", t)
+ null
+ }
complete(bitmap)
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index 205be444..526b15ae 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -16,17 +16,23 @@
package com.android.intentresolver.contentpreview;
-import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
+import static android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL;
+
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
-import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ContentInterface;
import android.content.Intent;
import android.content.res.Resources;
+import android.database.Cursor;
+import android.media.MediaMetadata;
import android.net.Uri;
import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -34,14 +40,14 @@ import androidx.annotation.Nullable;
import com.android.intentresolver.ImageLoader;
import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.widget.ActionRow;
-import com.android.intentresolver.widget.ImagePreviewView;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
/**
* Collection of helpers for building the content preview UI displayed in
@@ -88,24 +94,12 @@ public final class ChooserContentPreviewUi {
Consumer<Boolean> getExcludeSharedTextAction();
}
- /**
- * Testing shim to specify whether a given mime type is considered to be an "image."
- *
- * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
- * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this
- * class.
- */
- public interface ImageMimeTypeClassifier {
- /** @return whether the specified {@code mimeType} is classified as an "image" type. */
- boolean isImageType(String mimeType);
- }
-
private final ContentPreviewUi mContentPreviewUi;
public ChooserContentPreviewUi(
Intent targetIntent,
ContentInterface contentResolver,
- ImageMimeTypeClassifier imageClassifier,
+ MimeTypeClassifier imageClassifier,
ImageLoader imageLoader,
ActionFactory actionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
@@ -127,37 +121,78 @@ public final class ChooserContentPreviewUi {
private ContentPreviewUi createContentPreview(
Intent targetIntent,
ContentInterface contentResolver,
- ImageMimeTypeClassifier imageClassifier,
+ MimeTypeClassifier typeClassifier,
ImageLoader imageLoader,
ActionFactory actionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
FeatureFlagRepository featureFlagRepository) {
- int type = findPreferredContentPreview(targetIntent, contentResolver, imageClassifier);
- switch (type) {
- case CONTENT_PREVIEW_TEXT:
- return createTextPreview(
- targetIntent, actionFactory, imageLoader, featureFlagRepository);
- case CONTENT_PREVIEW_FILE:
- return new FileContentPreviewUi(
- extractContentUris(targetIntent),
- actionFactory,
- imageLoader,
- contentResolver,
- featureFlagRepository);
+ /* 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);
- case CONTENT_PREVIEW_IMAGE:
- return createImagePreview(
- targetIntent,
+ if (!(isSend || isSendMultiple)
+ || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) {
+ return createTextPreview(
+ targetIntent, actionFactory, imageLoader, featureFlagRepository);
+ }
+ List<Uri> uris = extractContentUris(targetIntent);
+ if (uris.isEmpty()) {
+ return createTextPreview(
+ targetIntent, actionFactory, imageLoader, featureFlagRepository);
+ }
+ ArrayList<FileInfo> files = new ArrayList<>(uris.size());
+ int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files);
+ if (previewCount == 0) {
+ return new FileContentPreviewUi(
+ files,
+ actionFactory,
+ imageLoader,
+ featureFlagRepository);
+ }
+ if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) {
+ return new UnifiedContentPreviewUi(
+ files,
+ targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
+ actionFactory,
+ imageLoader,
+ typeClassifier,
+ transitionElementStatusCallback,
+ featureFlagRepository);
+ }
+ if (previewCount < uris.size()) {
+ return new FileContentPreviewUi(
+ files,
+ actionFactory,
+ imageLoader,
+ featureFlagRepository);
+ }
+ // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up
+ // here. To preserve the legacy behavior, before using it, check that all uris are images.
+ for (FileInfo fileInfo: files) {
+ if (!typeClassifier.isImageType(fileInfo.getMimeType())) {
+ return new FileContentPreviewUi(
+ files,
actionFactory,
- contentResolver,
- imageClassifier,
imageLoader,
- transitionElementStatusCallback,
featureFlagRepository);
+ }
}
-
- return new NoContextPreviewUi(type);
+ return new ImageContentPreviewUi(
+ files.stream()
+ .map(FileInfo::getPreviewUri)
+ .filter(Objects::nonNull)
+ .toList(),
+ targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
+ actionFactory,
+ imageLoader,
+ transitionElementStatusCallback,
+ featureFlagRepository);
}
public int getPreferredContentPreview() {
@@ -174,61 +209,98 @@ public final class ChooserContentPreviewUi {
return mContentPreviewUi.display(resources, layoutInflater, parent);
}
- /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
- @ContentPreviewType
- private static int findPreferredContentPreview(
- Intent targetIntent,
- ContentInterface resolver,
- ImageMimeTypeClassifier imageClassifier) {
- /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
- * that broadly covers all data being shared, such as {@literal *}/* when sending an image
- * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
- * FILE, TEXT. */
- final String action = targetIntent.getAction();
- final String type = targetIntent.getType();
- final boolean isSend = Intent.ACTION_SEND.equals(action);
- final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action);
-
- if (!(isSend || isSendMultiple)
- || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) {
- return CONTENT_PREVIEW_TEXT;
+ 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;
+ }
- if (isSend) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- return findPreferredContentPreview(uri, resolver, imageClassifier);
+ 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();
}
-
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- if (uris == null || uris.isEmpty()) {
- return CONTENT_PREVIEW_TEXT;
+ readFileMetadata(resolver, uri, builder);
+ if (builder.getPreviewUri() == null) {
+ readOtherFileTypes(resolver, uri, typeClassifier, builder);
}
+ return builder.build();
+ }
- for (Uri uri : uris) {
- // Defaulting to file preview when there are mixed image/file types is
- // preferable, as it shows the user the correct number of items being shared
- int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
- if (uriPreviewType == CONTENT_PREVIEW_FILE) {
- return CONTENT_PREVIEW_FILE;
+ 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);
+ }
- return CONTENT_PREVIEW_IMAGE;
- }
-
- @ContentPreviewType
- private static int findPreferredContentPreview(
- Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier) {
- if (uri == null) {
- return CONTENT_PREVIEW_TEXT;
+ 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);
+ }
+ }
- String mimeType = null;
- try {
- mimeType = resolver.getType(uri);
- } catch (RemoteException ignored) {
+ 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;
+ }
+ }
}
- return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
}
private static TextContentPreviewUi createTextPreview(
@@ -255,39 +327,6 @@ public final class ChooserContentPreviewUi {
featureFlagRepository);
}
- static ImageContentPreviewUi createImagePreview(
- Intent targetIntent,
- ChooserContentPreviewUi.ActionFactory actionFactory,
- ContentInterface contentResolver,
- ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier,
- ImageLoader imageLoader,
- ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
- FeatureFlagRepository featureFlagRepository) {
- CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
- String action = targetIntent.getAction();
- // TODO: why don't we use image classifier for single-element ACTION_SEND?
- final List<Uri> imageUris = Intent.ACTION_SEND.equals(action)
- ? extractContentUris(targetIntent)
- : extractContentUris(targetIntent)
- .stream()
- .filter(uri -> {
- String type = null;
- try {
- type = contentResolver.getType(uri);
- } catch (RemoteException ignored) {
- }
- return imageClassifier.isImageType(type);
- })
- .collect(Collectors.toList());
- return new ImageContentPreviewUi(
- imageUris,
- text,
- actionFactory,
- imageLoader,
- transitionElementStatusCallback,
- featureFlagRepository);
- }
-
private static List<Uri> extractContentUris(Intent targetIntent) {
List<Uri> uris = new ArrayList<>();
if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
@@ -307,4 +346,41 @@ public final class ChooserContentPreviewUi {
}
return uris;
}
+
+ @Nullable
+ private static String getType(ContentInterface resolver, Uri uri) {
+ try {
+ return resolver.getType(uri);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ private static Cursor query(ContentInterface resolver, Uri uri) {
+ try {
+ return resolver.query(uri, null, null, null);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ private static String[] getStreamTypes(ContentInterface resolver, Uri uri) {
+ try {
+ return resolver.getStreamTypes(uri, "*/*");
+ } catch (RemoteException e) {
+ 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;
+ }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
index 7cd71475..2c5def8b 100644
--- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -16,15 +16,7 @@
package com.android.intentresolver.contentpreview;
-import android.content.ContentInterface;
import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.DocumentsContract;
-import android.provider.Downloads;
-import android.provider.OpenableColumns;
-import android.text.TextUtils;
import android.util.Log;
import android.util.PluralsMessageFormatter;
import android.view.LayoutInflater;
@@ -49,21 +41,19 @@ class FileContentPreviewUi extends ContentPreviewUi {
private static final String PLURALS_COUNT = "count";
private static final String PLURALS_FILE_NAME = "file_name";
- private final List<Uri> mUris;
+ private final List<FileInfo> mFiles;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
- private final ContentInterface mContentResolver;
private final FeatureFlagRepository mFeatureFlagRepository;
- FileContentPreviewUi(List<Uri> uris,
+ FileContentPreviewUi(
+ List<FileInfo> files,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
- ContentInterface contentResolver,
FeatureFlagRepository featureFlagRepository) {
- mUris = uris;
+ mFiles = files;
mActionFactory = actionFactory;
mImageLoader = imageLoader;
- mContentResolver = contentResolver;
mFeatureFlagRepository = featureFlagRepository;
}
@@ -85,7 +75,7 @@ class FileContentPreviewUi extends ContentPreviewUi {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
- final int uriCount = mUris.size();
+ final int uriCount = mFiles.size();
if (uriCount == 0) {
contentPreviewLayout.setVisibility(View.GONE);
@@ -95,13 +85,13 @@ class FileContentPreviewUi extends ContentPreviewUi {
}
if (uriCount == 1) {
- loadFileUriIntoView(mUris.get(0), contentPreviewLayout, mImageLoader, mContentResolver);
+ loadFileUriIntoView(mFiles.get(0), contentPreviewLayout, mImageLoader);
} else {
- FileInfo fileInfo = extractFileInfo(mUris.get(0), mContentResolver);
+ FileInfo fileInfo = mFiles.get(0);
int remUriCount = uriCount - 1;
Map<String, Object> arguments = new HashMap<>();
arguments.put(PLURALS_COUNT, remUriCount);
- arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+ arguments.put(PLURALS_FILE_NAME, fileInfo.getName());
String fileName =
PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
@@ -143,19 +133,16 @@ class FileContentPreviewUi extends ContentPreviewUi {
}
private static void loadFileUriIntoView(
- final Uri uri,
+ final FileInfo fileInfo,
final View parent,
- final ImageLoader imageLoader,
- final ContentInterface contentResolver) {
- FileInfo fileInfo = extractFileInfo(uri, contentResolver);
-
+ final ImageLoader imageLoader) {
TextView fileNameView = parent.findViewById(
com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileInfo.name);
+ fileNameView.setText(fileInfo.getName());
- if (fileInfo.hasThumbnail) {
+ if (fileInfo.getPreviewUri() != null) {
imageLoader.loadImage(
- uri,
+ fileInfo.getPreviewUri(),
(bitmap) -> updateViewWithImage(
parent.findViewById(
com.android.internal.R.id.content_preview_file_thumbnail),
@@ -171,66 +158,4 @@ class FileContentPreviewUi extends ContentPreviewUi {
fileIconView.setImageResource(R.drawable.chooser_file_generic);
}
}
-
- private static FileInfo extractFileInfo(Uri uri, ContentInterface resolver) {
- String fileName = null;
- boolean hasThumbnail = false;
-
- try (Cursor cursor = queryResolver(resolver, uri)) {
- if (cursor != null && cursor.getCount() > 0) {
- int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
- int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
- int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
-
- cursor.moveToFirst();
- if (nameIndex != -1) {
- fileName = cursor.getString(nameIndex);
- } else if (titleIndex != -1) {
- fileName = cursor.getString(titleIndex);
- }
-
- if (flagsIndex != -1) {
- hasThumbnail = (cursor.getInt(flagsIndex)
- & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
- }
- }
- } catch (SecurityException | NullPointerException e) {
- // The ContentResolver already logs the exception. Log something more informative.
- Log.w(
- TAG,
- "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
- + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
- + "and set your Intent's clipData and flags in accordance with that method's "
- + "documentation");
- }
-
- if (TextUtils.isEmpty(fileName)) {
- fileName = uri.getPath();
- fileName = fileName == null ? "" : fileName;
- int index = fileName.lastIndexOf('/');
- if (index != -1) {
- fileName = fileName.substring(index + 1);
- }
- }
-
- return new FileInfo(fileName, hasThumbnail);
- }
-
- private static Cursor queryResolver(ContentInterface resolver, Uri uri) {
- try {
- return resolver.query(uri, null, null, null);
- } catch (RemoteException e) {
- return null;
- }
- }
-
- private static class FileInfo {
- public final String name;
- public final boolean hasThumbnail;
-
- FileInfo(String name, boolean hasThumbnail) {
- this.name = name;
- this.hasThumbnail = hasThumbnail;
- }
- }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt
new file mode 100644
index 00000000..527bfc8e
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.net.Uri
+
+internal class FileInfo private constructor(
+ val uri: Uri,
+ val name: String?,
+ val previewUri: Uri?,
+ val mimeType: String?
+) {
+ 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
+ }
+
+ fun withMimeType(mimeType: String?): Builder = apply {
+ this.mimeType = mimeType
+ }
+
+ fun build(): FileInfo = FileInfo(uri, name, previewUri, mimeType)
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
index db26ab1b..5f3bdf40 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
@@ -39,7 +39,8 @@ import com.android.intentresolver.R;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.widget.ActionRow;
-import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.widget.ChooserImagePreviewView;
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
import java.util.ArrayList;
import java.util.List;
@@ -51,7 +52,7 @@ class ImageContentPreviewUi extends ContentPreviewUi {
private final CharSequence mText;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
- private final ImagePreviewView.TransitionElementStatusCallback mTransitionElementStatusCallback;
+ private final TransitionElementStatusCallback mTransitionElementStatusCallback;
private final FeatureFlagRepository mFeatureFlagRepository;
ImageContentPreviewUi(
@@ -59,7 +60,7 @@ class ImageContentPreviewUi extends ContentPreviewUi {
@Nullable CharSequence text,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
- ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
+ TransitionElementStatusCallback transitionElementStatusCallback,
FeatureFlagRepository featureFlagRepository) {
mImageUris = imageUris;
mText = text;
@@ -87,7 +88,7 @@ class ImageContentPreviewUi extends ContentPreviewUi {
@LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_image, parent, false);
- ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
+ ChooserImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
if (actionRow != null) {
@@ -103,7 +104,7 @@ class ImageContentPreviewUi extends ContentPreviewUi {
TAG,
"Attempted to display image preview area with zero"
+ " available images detected in EXTRA_STREAM list");
- ((View) imagePreview).setVisibility(View.GONE);
+ imagePreview.setVisibility(View.GONE);
mTransitionElementStatusCallback.onAllTransitionElementsReady();
return contentPreviewLayout;
}
@@ -129,14 +130,10 @@ class ImageContentPreviewUi extends ContentPreviewUi {
return actions;
}
- private ImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
+ private ChooserImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub);
if (stub != null) {
- int layoutId =
- mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)
- ? R.layout.scrollable_image_preview_view
- : R.layout.chooser_image_preview_view;
- stub.setLayoutResource(layoutId);
+ stub.setLayoutResource(R.layout.chooser_image_preview_view);
stub.inflate();
}
return previewLayout.findViewById(
diff --git a/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java
new file mode 100644
index 00000000..5172dd29
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import android.content.ClipDescription;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Testing shim to specify whether a given mime type is considered to be an "image."
+ *
+ * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
+ * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this
+ * class.
+ */
+public interface MimeTypeClassifier {
+ /** @return whether the specified {@code mimeType} is classified as an "image" type. */
+ boolean isImageType(@Nullable String mimeType);
+
+ /** @return whether the specified {@code mimeType} is classified as an "video" type */
+ default boolean isVideoType(@Nullable String mimeType) {
+ return ClipDescription.compareMimeTypes(mimeType, "video/*");
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
new file mode 100644
index 00000000..ee24d18f
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
+
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.text.util.Linkify;
+import android.transition.TransitionManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
+import com.android.intentresolver.widget.ScrollableImagePreviewView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+class UnifiedContentPreviewUi extends ContentPreviewUi {
+ private final List<FileInfo> mFiles;
+ @Nullable
+ private final CharSequence mText;
+ private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+ private final ImageLoader mImageLoader;
+ private final MimeTypeClassifier mTypeClassifier;
+ private final TransitionElementStatusCallback mTransitionElementStatusCallback;
+ private final FeatureFlagRepository mFeatureFlagRepository;
+
+ UnifiedContentPreviewUi(
+ List<FileInfo> files,
+ @Nullable CharSequence text,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ImageLoader imageLoader,
+ MimeTypeClassifier typeClassifier,
+ TransitionElementStatusCallback transitionElementStatusCallback,
+ FeatureFlagRepository featureFlagRepository) {
+ mFiles = files;
+ mText = text;
+ mActionFactory = actionFactory;
+ mImageLoader = imageLoader;
+ mTypeClassifier = typeClassifier;
+ mTransitionElementStatusCallback = transitionElementStatusCallback;
+ mFeatureFlagRepository = featureFlagRepository;
+
+ mImageLoader.prePopulate(mFiles.stream()
+ .map(FileInfo::getPreviewUri)
+ .filter(Objects::nonNull)
+ .toList());
+ }
+
+ @Override
+ public int getType() {
+ return CONTENT_PREVIEW_IMAGE;
+ }
+
+ @Override
+ public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent);
+ displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+ return layout;
+ }
+
+ private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_image, parent, false);
+ ScrollableImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(
+ createActions(
+ createImagePreviewActions(),
+ mActionFactory.createCustomActions(),
+ mFeatureFlagRepository));
+ }
+
+ if (mFiles.size() == 0) {
+ Log.i(
+ TAG,
+ "Attempted to display image preview area with zero"
+ + " available images detected in EXTRA_STREAM list");
+ imagePreview.setVisibility(View.GONE);
+ mTransitionElementStatusCallback.onAllTransitionElementsReady();
+ return contentPreviewLayout;
+ }
+
+ setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory);
+ imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
+ List<ScrollableImagePreviewView.Preview> previews = mFiles.stream()
+ .filter(fileInfo -> fileInfo.getPreviewUri() != null)
+ .map(fileInfo ->
+ new ScrollableImagePreviewView.Preview(
+ getPreviewType(fileInfo.getMimeType()),
+ fileInfo.getPreviewUri()))
+ .toList();
+ imagePreview.setPreviews(
+ previews,
+ mFiles.size() - previews.size(),
+ mImageLoader);
+
+ return contentPreviewLayout;
+ }
+
+ private List<ActionRow.Action> createImagePreviewActions() {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ //TODO: add copy action;
+ ActionRow.Action action = mActionFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ action = mActionFactory.createEditButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
+ private ScrollableImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
+ ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub);
+ if (stub != null) {
+ stub.setLayoutResource(R.layout.scrollable_image_preview_view);
+ stub.inflate();
+ }
+ return previewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_area);
+ }
+
+ private void setTextInImagePreviewVisibility(
+ ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) {
+ int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW)
+ && !TextUtils.isEmpty(mText)
+ ? View.VISIBLE
+ : View.GONE;
+
+ final TextView textView = contentPreview
+ .requireViewById(com.android.internal.R.id.content_preview_text);
+ CheckBox actionView = contentPreview
+ .requireViewById(R.id.include_text_action);
+ textView.setVisibility(visibility);
+ boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString());
+ textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
+ textView.setText(mText);
+
+ if (visibility == View.VISIBLE) {
+ final int[] actionLabels = isLink
+ ? new int[] { R.string.include_link, R.string.exclude_link }
+ : new int[] { R.string.include_text, R.string.exclude_text };
+ final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction();
+ actionView.setChecked(true);
+ actionView.setText(actionLabels[1]);
+ shareTextAction.accept(false);
+ actionView.setOnCheckedChangeListener((view, isChecked) -> {
+ view.setText(actionLabels[isChecked ? 1 : 0]);
+ TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent());
+ textView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+ shareTextAction.accept(!isChecked);
+ });
+ }
+ actionView.setVisibility(visibility);
+ }
+
+ private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) {
+ if (mTypeClassifier.isImageType(mimeType)) {
+ return ScrollableImagePreviewView.PreviewType.Image;
+ }
+ if (mTypeClassifier.isVideoType(mimeType)) {
+ return ScrollableImagePreviewView.PreviewType.Video;
+ }
+ return ScrollableImagePreviewView.PreviewType.File;
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt
index ca94a95d..6273296d 100644
--- a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt
@@ -74,7 +74,7 @@ class ChooserImagePreviewView : RelativeLayout, ImagePreviewView {
transitionStatusElementCallback = callback
}
- override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
loadImageJob?.cancel()
loadImageJob = coroutineScope.launch {
when (uris.size) {
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
index a166ef27..8813adca 100644
--- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -23,7 +23,6 @@ internal typealias ImageLoader = suspend (Uri) -> Bitmap?
interface ImagePreviewView {
fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?)
- fun setImages(uris: List<Uri>, imageLoader: ImageLoader)
/**
* [ImagePreviewView] progressively prepares views for shared element transition and reports
diff --git a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
index 8538041b..8ca6ed14 100644
--- a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
+++ b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
@@ -17,6 +17,7 @@
package com.android.intentresolver.widget;
import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -52,7 +53,17 @@ public class RoundedRectImageView extends ImageView {
public RoundedRectImageView(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs,
+ R.styleable.RoundedRectImageView,
+ defStyleAttr,
+ 0);
+ mRadius = a.getDimensionPixelSize(R.styleable.RoundedRectImageView_radius, -1);
+ if (mRadius < 0) {
+ mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
+ }
+ a.recycle();
mOverlayPaint.setColor(0x99000000);
mOverlayPaint.setStyle(Paint.Style.FILL);
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 467c404a..c02a10a2 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -20,11 +20,13 @@ import android.content.Context
import android.graphics.Rect
import android.net.Uri
import android.util.AttributeSet
+import android.util.PluralsMessageFormatter
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
+import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.intentresolver.R
@@ -33,11 +35,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
+import kotlin.math.sign
private const val TRANSITION_NAME = "screenshot_preview_image"
+private const val PLURALS_COUNT = "count"
class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
constructor(context: Context) : this(context, null)
@@ -66,41 +69,64 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
previewAdapter.transitionStatusElementCallback = callback
}
- override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
- previewAdapter.setImages(uris, imageLoader)
+ fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader) =
+ previewAdapter.setPreviews(previews, otherItemCount, imageLoader)
+
+ class Preview(val type: PreviewType, val uri: Uri)
+ enum class PreviewType {
+ Image, Video, File
}
private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
- private val uris = ArrayList<Uri>()
+ private val previews = ArrayList<Preview>()
private var imageLoader: ImageLoader? = null
+ private var firstImagePos = -1
var transitionStatusElementCallback: TransitionElementStatusCallback? = null
+ private var otherItemCount = 0
- fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
- this.uris.clear()
- this.uris.addAll(uris)
+ fun setPreviews(
+ previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader
+ ) {
+ this.previews.clear()
+ this.previews.addAll(previews)
this.imageLoader = imageLoader
+ firstImagePos = previews.indexOfFirst { it.type == PreviewType.Image }
+ this.otherItemCount = maxOf(0, otherItemCount)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
- return ViewHolder(
- LayoutInflater.from(context)
- .inflate(R.layout.image_preview_image_item, parent, false)
- )
+ val view = LayoutInflater.from(context).inflate(itemType, parent, false);
+ return if (itemType == R.layout.image_preview_other_item) {
+ OtherItemViewHolder(view)
+ } else {
+ PreviewViewHolder(view)
+ }
}
- override fun getItemCount(): Int = uris.size
+ override fun getItemCount(): Int = previews.size + otherItemCount.sign
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == previews.size) {
+ R.layout.image_preview_other_item
+ } else {
+ R.layout.image_preview_image_item
+ }
+ }
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
- vh.bind(
- uris[position],
- imageLoader ?: error("ImageLoader is missing"),
- if (position == 0 && transitionStatusElementCallback != null) {
- this::onTransitionElementReady
- } else {
- null
- }
- )
+ when (vh) {
+ is OtherItemViewHolder -> vh.bind(otherItemCount)
+ is PreviewViewHolder -> vh.bind(
+ previews[position],
+ imageLoader ?: error("ImageLoader is missing"),
+ if (position == firstImagePos && transitionStatusElementCallback != null) {
+ this::onTransitionElementReady
+ } else {
+ null
+ }
+ )
+ }
}
override fun onViewRecycled(vh: ViewHolder) {
@@ -121,12 +147,18 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
- private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private sealed class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ abstract fun unbind()
+ }
+
+ private class PreviewViewHolder(view: View) : ViewHolder(view) {
private val image = view.requireViewById<ImageView>(R.id.image)
+ private val badgeFrame = view.requireViewById<View>(R.id.badge_frame)
+ private val badge = view.requireViewById<ImageView>(R.id.badge)
private var scope: CoroutineScope? = null
fun bind(
- uri: Uri,
+ preview: Preview,
imageLoader: ImageLoader,
previewReadyCallback: ((String) -> Unit)?
) {
@@ -136,26 +168,35 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
} else {
null
}
+ badgeFrame.visibility = when (preview.type) {
+ PreviewType.Image -> View.GONE
+ PreviewType.Video -> {
+ badge.setImageResource(R.drawable.ic_file_video)
+ View.VISIBLE
+ }
+ else -> {
+ badge.setImageResource(R.drawable.chooser_file_generic)
+ View.VISIBLE
+ }
+ }
resetScope().launch {
- loadImage(uri, imageLoader, previewReadyCallback)
+ loadImage(preview.uri, imageLoader)
+ if (preview.type == PreviewType.Image) {
+ previewReadyCallback?.let { callback ->
+ image.waitForPreDraw()
+ callback(TRANSITION_NAME)
+ }
+ }
}
}
- private suspend fun loadImage(
- uri: Uri,
- imageLoader: ImageLoader,
- previewReadyCallback: ((String) -> Unit)?
- ) {
+ private suspend fun loadImage(uri: Uri, imageLoader: ImageLoader) {
val bitmap = runCatching {
// it's expected for all loading/caching optimizations to be implemented by the
// loader
imageLoader(uri)
}.getOrNull()
image.setImageBitmap(bitmap)
- previewReadyCallback?.let { callback ->
- image.waitForPreDraw()
- callback(TRANSITION_NAME)
- }
}
private fun resetScope(): CoroutineScope =
@@ -164,13 +205,27 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
scope = it
}
- fun unbind() {
+ override fun unbind() {
scope?.cancel()
scope = null
}
}
- private class SpacingDecoration(private val margin: Int) : RecyclerView.ItemDecoration() {
+ private class OtherItemViewHolder(view: View) : ViewHolder(view) {
+ private val label = view.requireViewById<TextView>(R.id.label)
+
+ fun bind(count: Int) {
+ label.text = PluralsMessageFormatter.format(
+ itemView.context.resources,
+ mapOf(PLURALS_COUNT to count),
+ R.string.other_files
+ )
+ }
+
+ override fun unbind() = Unit
+ }
+
+ private class SpacingDecoration(private val margin: Int) : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
outRect.set(margin, 0, margin, 0)
}