Commit 46c8ab5b authored by Finnur Thorarinsson's avatar Finnur Thorarinsson Committed by Commit Bot

Photo Picker: Add video support behind flag (1/2).

Part 1: Show video thumbnails in the grid, but no preview (yet).

Bug: 895776, 656015
Change-Id: I1f99224e33f98dd20e26543a0d05a2b7000ff871
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1615199
Commit-Queue: Finnur Thorarinsson <finnur@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Cr-Commit-Position: refs/heads/master@{#663136}
parent 76ddcd29
...@@ -1193,6 +1193,7 @@ chrome_java_sources = [ ...@@ -1193,6 +1193,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/photo_picker/BitmapUtils.java", "java/src/org/chromium/chrome/browser/photo_picker/BitmapUtils.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecoderService.java", "java/src/org/chromium/chrome/browser/photo_picker/DecoderService.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecoderServiceHost.java", "java/src/org/chromium/chrome/browser/photo_picker/DecoderServiceHost.java",
"java/src/org/chromium/chrome/browser/photo_picker/DecodeVideoTask.java",
"java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java", "java/src/org/chromium/chrome/browser/photo_picker/FileEnumWorkerTask.java",
"java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java", "java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialog.java",
"java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerToolbar.java", "java/src/org/chromium/chrome/browser/photo_picker/PhotoPickerToolbar.java",
......
...@@ -33,6 +33,15 @@ ...@@ -33,6 +33,15 @@
android:background="@drawable/file_picker_scrim" android:background="@drawable/file_picker_scrim"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
android:visibility="gone" /> android:visibility="gone" />
<TextView
android:id="@+id/video_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:layout_marginEnd="6dp"
android:layout_gravity="end"
style="@style/TextAppearance.WhiteBody"/>
</FrameLayout> </FrameLayout>
<ImageView <ImageView
......
...@@ -433,6 +433,7 @@ ...@@ -433,6 +433,7 @@
<dimen name="photo_picker_tile_min_size">100dp</dimen> <dimen name="photo_picker_tile_min_size">100dp</dimen>
<dimen name="photo_picker_tile_gap">4dp</dimen> <dimen name="photo_picker_tile_gap">4dp</dimen>
<dimen name="photo_picker_grainy_thumbnail_size">12dp</dimen> <dimen name="photo_picker_grainy_thumbnail_size">12dp</dimen>
<dimen name="photo_picker_video_duration_offset">16dp</dimen>
<!-- Account chooser dialog dimensions --> <!-- Account chooser dialog dimensions -->
<dimen name="account_chooser_dialog_margin">24dp</dimen> <dimen name="account_chooser_dialog_margin">24dp</dimen>
......
...@@ -288,6 +288,7 @@ public abstract class ChromeFeatureList { ...@@ -288,6 +288,7 @@ public abstract class ChromeFeatureList {
public static final String PERMISSION_DELEGATION = "PermissionDelegation"; public static final String PERMISSION_DELEGATION = "PermissionDelegation";
public static final String PER_METHOD_CAN_MAKE_PAYMENT_QUOTA = public static final String PER_METHOD_CAN_MAKE_PAYMENT_QUOTA =
"WebPaymentsPerMethodCanMakePaymentQuota"; "WebPaymentsPerMethodCanMakePaymentQuota";
public static final String PHOTO_PICKER_VIDEO_SUPPORT = "PhotoPickerVideoSupport";
public static final String WEB_PAYMENTS_REDACT_SHIPPING_ADDRESS = public static final String WEB_PAYMENTS_REDACT_SHIPPING_ADDRESS =
"WebPaymentsRedactShippingAddress"; "WebPaymentsRedactShippingAddress";
public static final String PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES = public static final String PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES =
......
...@@ -235,6 +235,12 @@ public class ProcessInitializationHandler { ...@@ -235,6 +235,12 @@ public class ProcessInitializationHandler {
public void onPhotoPickerDismissed() { public void onPhotoPickerDismissed() {
mDialog = null; mDialog = null;
} }
@Override
public boolean supportsVideos() {
return ChromeFeatureList.isEnabled(
ChromeFeatureList.PHOTO_PICKER_VIDEO_SUPPORT);
}
}); });
} }
......
...@@ -16,20 +16,22 @@ import org.chromium.base.task.AsyncTask; ...@@ -16,20 +16,22 @@ import org.chromium.base.task.AsyncTask;
* A worker task to scale bitmaps in the background. * A worker task to scale bitmaps in the background.
*/ */
class BitmapScalerTask extends AsyncTask<Bitmap> { class BitmapScalerTask extends AsyncTask<Bitmap> {
private final LruCache<String, Bitmap> mCache; private final LruCache<String, PickerCategoryView.Thumbnail> mCache;
private final String mFilePath; private final String mFilePath;
private final int mSize; private final int mSize;
private final Bitmap mBitmap; private final Bitmap mBitmap;
private final String mVideoDuration;
/** /**
* A BitmapScalerTask constructor. * A BitmapScalerTask constructor.
*/ */
public BitmapScalerTask( public BitmapScalerTask(LruCache<String, PickerCategoryView.Thumbnail> cache, Bitmap bitmap,
LruCache<String, Bitmap> cache, String filePath, int size, Bitmap bitmap) { String filePath, String videoDuration, int size) {
mCache = cache; mCache = cache;
mFilePath = filePath; mFilePath = filePath;
mSize = size; mSize = size;
mBitmap = bitmap; mBitmap = bitmap;
mVideoDuration = videoDuration;
} }
/** /**
...@@ -60,6 +62,6 @@ class BitmapScalerTask extends AsyncTask<Bitmap> { ...@@ -60,6 +62,6 @@ class BitmapScalerTask extends AsyncTask<Bitmap> {
return; return;
} }
mCache.put(mFilePath, result); mCache.put(mFilePath, new PickerCategoryView.Thumbnail(result, mVideoDuration));
} }
} }
...@@ -8,6 +8,7 @@ import android.graphics.Bitmap; ...@@ -8,6 +8,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.media.MediaMetadataRetriever;
import android.os.Build; import android.os.Build;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
...@@ -69,6 +70,26 @@ class BitmapUtils { ...@@ -69,6 +70,26 @@ class BitmapUtils {
return sizeBitmap(bitmap, size, descriptor); return sizeBitmap(bitmap, size, descriptor);
} }
/**
* Given a FileDescriptor, decodes the video and returns a bitmap of dimensions |size|x|size|.
* @param descriptor The FileDescriptor for the file to read.
* @param size The width and height of the bitmap to return.
* @return The resulting bitmap.
*/
public static Bitmap decodeVideoFromFileDescriptor(
MediaMetadataRetriever retriever, FileDescriptor descriptor, int size) {
try {
retriever.setDataSource(descriptor);
} catch (RuntimeException exception) {
return null;
}
Bitmap bitmap = retriever.getFrameAtTime();
if (bitmap == null) return null;
return sizeBitmap(bitmap, size, descriptor);
}
/** /**
* Calculates the sub-sampling factor {@link BitmapFactory#inSampleSize} option for a given * Calculates the sub-sampling factor {@link BitmapFactory#inSampleSize} option for a given
* image dimensions, which will be used to create a bitmap of a pre-determined size (as small as * image dimensions, which will be used to create a bitmap of a pre-determined size (as small as
...@@ -95,7 +116,16 @@ class BitmapUtils { ...@@ -95,7 +116,16 @@ class BitmapUtils {
private static Bitmap ensureMinSize(Bitmap bitmap, int size) { private static Bitmap ensureMinSize(Bitmap bitmap, int size) {
int width = bitmap.getWidth(); int width = bitmap.getWidth();
int height = bitmap.getHeight(); int height = bitmap.getHeight();
if (width >= size && height >= size) return bitmap; if (width == size && height == size) return bitmap;
if (width > size && height > size) {
// Both sides are larger than requested, which will lead to excessive amount of
// cropping. Shrink to a more manageable amount (shorter side becomes |size| in length).
float scale = (width < height) ? (float) width / size : (float) height / size;
width = Math.round(width / scale);
height = Math.round(height / scale);
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
if (width < size) { if (width < size) {
float scale = (float) size / width; float scale = (float) size / width;
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.photo_picker;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.util.Pair;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import java.io.FileNotFoundException;
import java.util.Locale;
/**
* A worker task to decode video and extract information from it off of the UI thread.
*/
class DecodeVideoTask extends AsyncTask<Pair<Bitmap, String>> {
/**
* An interface to use to communicate back the results to the client.
*/
public interface VideoDecodingCallback {
/**
* A callback to define to receive the list of all images on disk.
* @param uri The uri of the video decoded.
* @param bitmap A single frame (thumbnail) from the video.
* @param duration The duration of the video.
*/
void videoDecodedCallback(Uri uri, Bitmap bitmap, String duration);
}
// The callback to use to communicate the results.
private VideoDecodingCallback mCallback;
// The URI of the video to decode.
private Uri mUri;
// The desired width and height (in pixels) of the returned thumbnail from the video.
int mSize;
// The ContentResolver to use to retrieve image metadata from disk.
private ContentResolver mContentResolver;
// A metadata retriever, used to decode the video, and extract a thumbnail frame.
private MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
/**
* A DecodeVideoTask constructor.
* @param callback The callback to use to communicate back the results.
* @param contentResolver The ContentResolver to use to retrieve image metadata from disk.
* @param uri The URI of the video to decode.
* @param size The desired width and height (in pixels) of the returned thumbnail from the
* video.
*/
public DecodeVideoTask(
VideoDecodingCallback callback, ContentResolver contentResolver, Uri uri, int size) {
mCallback = callback;
mContentResolver = contentResolver;
mUri = uri;
mSize = size;
}
/**
* Converts a duration string in ms to a human-readable form.
* @param durationMs The duration in milliseconds.
* @return The duration in human-readable form.
*/
private String formatDuration(String durationMs) {
if (durationMs == null) return null;
long duration = Long.parseLong(durationMs) / 1000;
long hours = duration / 3600;
duration -= hours * 3600;
long minutes = duration / 60;
duration -= minutes * 60;
long seconds = duration;
if (hours > 0) {
return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
}
}
/**
* Decodes a video and extracts metadata and a thumbnail. Called on a non-UI thread
* @return A pair of bitmap (video thumbnail) and the duration of the video.
*/
@Override
protected Pair<Bitmap, String> doInBackground() {
assert !ThreadUtils.runningOnUiThread();
if (isCancelled()) return null;
try {
Bitmap bitmap = BitmapUtils.decodeVideoFromFileDescriptor(mRetriever,
mContentResolver.openAssetFileDescriptor(mUri, "r").getFileDescriptor(), mSize);
String duration =
mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
return new Pair<Bitmap, String>(bitmap, formatDuration(duration));
} catch (FileNotFoundException exception) {
return null;
}
}
/**
* Communicates the results back to the client. Called on the UI thread.
* @param results A pair of bitmap (video thumbnail) and the duration of the video.
*/
@Override
protected void onPostExecute(Pair<Bitmap, String> results) {
if (isCancelled()) {
return;
}
if (results == null) {
mCallback.videoDecodedCallback(mUri, null, "");
return;
}
mCallback.videoDecodedCallback(mUri, results.first, results.second);
}
}
...@@ -12,6 +12,7 @@ import android.content.ServiceConnection; ...@@ -12,6 +12,7 @@ import android.content.ServiceConnection;
import android.content.res.AssetFileDescriptor; import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
...@@ -23,6 +24,7 @@ import android.support.annotation.Nullable; ...@@ -23,6 +24,7 @@ import android.support.annotation.Nullable;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask; import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.util.ConversionUtils; import org.chromium.chrome.browser.util.ConversionUtils;
import org.chromium.content_public.browser.UiThreadTaskTraits; import org.chromium.content_public.browser.UiThreadTaskTraits;
...@@ -36,7 +38,8 @@ import java.util.List; ...@@ -36,7 +38,8 @@ import java.util.List;
/** /**
* A class to communicate with the {@link DecoderService}. * A class to communicate with the {@link DecoderService}.
*/ */
public class DecoderServiceHost extends IDecoderServiceCallback.Stub { public class DecoderServiceHost
extends IDecoderServiceCallback.Stub implements DecodeVideoTask.VideoDecodingCallback {
// A tag for logging error messages. // A tag for logging error messages.
private static final String TAG = "ImageDecoderHost"; private static final String TAG = "ImageDecoderHost";
...@@ -52,6 +55,9 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -52,6 +55,9 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
// The number of out of memory failures during decoding, per batch. // The number of out of memory failures during decoding, per batch.
private int mFailedDecodesMemory; private int mFailedDecodesMemory;
// A worker task for asynchronously handling video decode requests.
private DecodeVideoTask mWorkerTask;
// A callback to use for testing to see if decoder is ready. // A callback to use for testing to see if decoder is ready.
static ServiceReadyCallback sReadyCallbackForTesting; static ServiceReadyCallback sReadyCallbackForTesting;
...@@ -92,8 +98,9 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -92,8 +98,9 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
* A function to define to receive a notification that an image has been decoded. * A function to define to receive a notification that an image has been decoded.
* @param filePath The file path for the newly decoded image. * @param filePath The file path for the newly decoded image.
* @param bitmap The results of the decoding (or placeholder image, if failed). * @param bitmap The results of the decoding (or placeholder image, if failed).
* @param videoDuration The time-length of the video (null if not a video).
*/ */
void imageDecodedCallback(String filePath, Bitmap bitmap); void imageDecodedCallback(String filePath, Bitmap bitmap, String videoDuration);
} }
/** /**
...@@ -106,15 +113,21 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -106,15 +113,21 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
// The requested size (width and height) of the bitmap, once decoded. // The requested size (width and height) of the bitmap, once decoded.
public int mSize; public int mSize;
// The type of media being decoded.
@PickerBitmap.TileTypes
int mFileType;
// The callback to use to communicate the results of the decoding. // The callback to use to communicate the results of the decoding.
ImageDecodedCallback mCallback; ImageDecodedCallback mCallback;
// The timestamp for when the request was sent for decoding. // The timestamp for when the request was sent for decoding.
long mTimestamp; long mTimestamp;
public DecoderServiceParams(Uri uri, int size, ImageDecodedCallback callback) { public DecoderServiceParams(Uri uri, int size, @PickerBitmap.TileTypes int fileType,
ImageDecodedCallback callback) {
mUri = uri; mUri = uri;
mSize = size; mSize = size;
mFileType = fileType;
mCallback = callback; mCallback = callback;
} }
} }
...@@ -171,11 +184,13 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -171,11 +184,13 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
* Accepts a request to decode a single image. Queues up the request and reports back * Accepts a request to decode a single image. Queues up the request and reports back
* asynchronously on |callback|. * asynchronously on |callback|.
* @param uri The URI of the file to decode. * @param uri The URI of the file to decode.
* @param fileType The type of image being sent for decoding.
* @param size The requested size (width and height) of the resulting bitmap. * @param size The requested size (width and height) of the resulting bitmap.
* @param callback The callback to use to communicate the decoding results. * @param callback The callback to use to communicate the decoding results.
*/ */
public void decodeImage(Uri uri, int size, ImageDecodedCallback callback) { public void decodeImage(Uri uri, @PickerBitmap.TileTypes int fileType, int size,
DecoderServiceParams params = new DecoderServiceParams(uri, size, callback); ImageDecodedCallback callback) {
DecoderServiceParams params = new DecoderServiceParams(uri, size, fileType, callback);
mRequests.put(uri.getPath(), params); mRequests.put(uri.getPath(), params);
if (mRequests.size() == 1) dispatchNextDecodeImageRequest(); if (mRequests.size() == 1) dispatchNextDecodeImageRequest();
} }
...@@ -187,7 +202,7 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -187,7 +202,7 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
if (mRequests.entrySet().iterator().hasNext()) { if (mRequests.entrySet().iterator().hasNext()) {
DecoderServiceParams params = mRequests.entrySet().iterator().next().getValue(); DecoderServiceParams params = mRequests.entrySet().iterator().next().getValue();
params.mTimestamp = SystemClock.elapsedRealtime(); params.mTimestamp = SystemClock.elapsedRealtime();
dispatchDecodeImageRequest(params.mUri, params.mSize); dispatchDecodeImageRequest(params.mUri, params.mFileType, params.mSize);
} else { } else {
int totalRequests = mSuccessfulDecodes + mFailedDecodesRuntime + mFailedDecodesMemory; int totalRequests = mSuccessfulDecodes + mFailedDecodesRuntime + mFailedDecodesMemory;
if (totalRequests > 0) { if (totalRequests > 0) {
...@@ -206,6 +221,18 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -206,6 +221,18 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
} }
} }
/**
* A callback that receives the results of the video decoding.
* @param uri The uri of the decoded video.
* @param bitmap The thumbnail representing the decoded video.
* @param duration The video duration (a formatted human-readable string, for example "3:00").
*/
@Override
public void videoDecodedCallback(Uri uri, Bitmap bitmap, String duration) {
// TODO(finnur): Add corresponding UMA for video decoding.
closeRequest(uri.getPath(), bitmap, duration, -1);
}
@Override @Override
public void onDecodeImageDone(final Bundle payload) { public void onDecodeImageDone(final Bundle payload) {
// As per the Android documentation, AIDL callbacks can (and will) happen on any thread, so // As per the Android documentation, AIDL callbacks can (and will) happen on any thread, so
...@@ -221,7 +248,7 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -221,7 +248,7 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
: null; : null;
long decodeTime = payload.getLong(DecoderService.KEY_DECODE_TIME); long decodeTime = payload.getLong(DecoderService.KEY_DECODE_TIME);
mSuccessfulDecodes++; mSuccessfulDecodes++;
closeRequest(filePath, bitmap, decodeTime); closeRequest(filePath, bitmap, null, decodeTime);
} catch (RuntimeException e) { } catch (RuntimeException e) {
mFailedDecodesRuntime++; mFailedDecodesRuntime++;
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
...@@ -238,14 +265,15 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -238,14 +265,15 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
* @param bitmap The resulting decoded bitmap, or null if decoding fails. * @param bitmap The resulting decoded bitmap, or null if decoding fails.
* @param decodeTime The length of time it took to decode the bitmap. * @param decodeTime The length of time it took to decode the bitmap.
*/ */
public void closeRequest(String filePath, @Nullable Bitmap bitmap, long decodeTime) { public void closeRequest(
String filePath, @Nullable Bitmap bitmap, String videoDuration, long decodeTime) {
DecoderServiceParams params = getRequests().get(filePath); DecoderServiceParams params = getRequests().get(filePath);
if (params != null) { if (params != null) {
long endRpcCall = SystemClock.elapsedRealtime(); long endRpcCall = SystemClock.elapsedRealtime();
RecordHistogram.recordTimesHistogram( RecordHistogram.recordTimesHistogram(
"Android.PhotoPicker.RequestProcessTime", endRpcCall - params.mTimestamp); "Android.PhotoPicker.RequestProcessTime", endRpcCall - params.mTimestamp);
params.mCallback.imageDecodedCallback(filePath, bitmap); params.mCallback.imageDecodedCallback(filePath, bitmap, videoDuration);
if (decodeTime != -1 && bitmap != null) { if (decodeTime != -1 && bitmap != null) {
RecordHistogram.recordTimesHistogram( RecordHistogram.recordTimesHistogram(
...@@ -265,7 +293,17 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -265,7 +293,17 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
* @param uri The URI of the image on disk. * @param uri The URI of the image on disk.
* @param size The requested width and height of the resulting bitmap. * @param size The requested width and height of the resulting bitmap.
*/ */
private void dispatchDecodeImageRequest(Uri uri, int size) { private void dispatchDecodeImageRequest(
Uri uri, @PickerBitmap.TileTypes int fileType, int size) {
if (fileType == PickerBitmap.TileTypes.VIDEO) {
// Videos are decoded by the system (on N+) using a restricted helper process, so
// there's no need to use our custom sandboxed process.
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
mWorkerTask = new DecodeVideoTask(this, mContentResolver, uri, size);
mWorkerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return;
}
// Obtain a file descriptor to send over to the sandboxed process. // Obtain a file descriptor to send over to the sandboxed process.
ParcelFileDescriptor pfd = null; ParcelFileDescriptor pfd = null;
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
...@@ -279,12 +317,12 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -279,12 +317,12 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
afd = mContentResolver.openAssetFileDescriptor(uri, "r"); afd = mContentResolver.openAssetFileDescriptor(uri, "r");
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.e(TAG, "Unable to obtain FileDescriptor: " + e); Log.e(TAG, "Unable to obtain FileDescriptor: " + e);
closeRequest(uri.getPath(), null, -1); closeRequest(uri.getPath(), null, null, -1);
return; return;
} }
pfd = afd.getParcelFileDescriptor(); pfd = afd.getParcelFileDescriptor();
if (pfd == null) { if (pfd == null) {
closeRequest(uri.getPath(), null, -1); closeRequest(uri.getPath(), null, null, -1);
return; return;
} }
} finally { } finally {
...@@ -300,10 +338,10 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub { ...@@ -300,10 +338,10 @@ public class DecoderServiceHost extends IDecoderServiceCallback.Stub {
pfd.close(); pfd.close();
} catch (RemoteException e) { } catch (RemoteException e) {
Log.e(TAG, "Communications failed (Remote): " + e); Log.e(TAG, "Communications failed (Remote): " + e);
closeRequest(uri.getPath(), null, -1); closeRequest(uri.getPath(), null, null, -1);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Communications failed (IO): " + e); Log.e(TAG, "Communications failed (IO): " + e);
closeRequest(uri.getPath(), null, -1); closeRequest(uri.getPath(), null, null, -1);
} }
} }
......
...@@ -46,10 +46,16 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> { ...@@ -46,10 +46,16 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> {
// The filter to apply to the list. // The filter to apply to the list.
private MimeTypeFilter mFilter; private MimeTypeFilter mFilter;
// Whether any image MIME types were requested.
private boolean mIncludeImages;
// Whether any video MIME types were requested.
private boolean mIncludeVideos;
// The ContentResolver to use to retrieve image metadata from disk. // The ContentResolver to use to retrieve image metadata from disk.
private ContentResolver mContentResolver; private ContentResolver mContentResolver;
// The camera directory undir DCIM. // The camera directory under DCIM.
private static final String SAMPLE_DCIM_SOURCE_SUB_DIRECTORY = "Camera"; private static final String SAMPLE_DCIM_SOURCE_SUB_DIRECTORY = "Camera";
/** /**
...@@ -60,11 +66,21 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> { ...@@ -60,11 +66,21 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> {
* @param contentResolver The ContentResolver to use to retrieve image metadata from disk. * @param contentResolver The ContentResolver to use to retrieve image metadata from disk.
*/ */
public FileEnumWorkerTask(WindowAndroid windowAndroid, FilesEnumeratedCallback callback, public FileEnumWorkerTask(WindowAndroid windowAndroid, FilesEnumeratedCallback callback,
MimeTypeFilter filter, ContentResolver contentResolver) { MimeTypeFilter filter, List<String> mimeTypes, ContentResolver contentResolver) {
mWindowAndroid = windowAndroid; mWindowAndroid = windowAndroid;
mCallback = callback; mCallback = callback;
mFilter = filter; mFilter = filter;
mContentResolver = contentResolver; mContentResolver = contentResolver;
for (String mimeType : mimeTypes) {
if (mimeType.startsWith("image/")) {
mIncludeImages = true;
} else if (mimeType.startsWith("video/")) {
mIncludeVideos = true;
}
if (mIncludeImages && mIncludeVideos) break;
}
} }
/** /**
...@@ -76,7 +92,6 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> { ...@@ -76,7 +92,6 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> {
/** /**
* Enumerates (in the background) the image files on disk. Called on a non-UI thread * Enumerates (in the background) the image files on disk. Called on a non-UI thread
* @param params Ignored, do not use.
* @return A sorted list of images (by last-modified first). * @return A sorted list of images (by last-modified first).
*/ */
@Override @Override
...@@ -89,14 +104,29 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> { ...@@ -89,14 +104,29 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> {
// The DATA column is deprecated in the Android Q SDK. Replaced by relative_path. // The DATA column is deprecated in the Android Q SDK. Replaced by relative_path.
String directoryColumnName = String directoryColumnName =
BuildInfo.isAtLeastQ() ? "relative_path" : MediaStore.Images.Media.DATA; BuildInfo.isAtLeastQ() ? "relative_path" : MediaStore.Files.FileColumns.DATA;
final String[] selectColumns = {MediaStore.Images.Media._ID, final String[] selectColumns = {
MediaStore.Images.Media.DATE_TAKEN, directoryColumnName}; MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DATE_ADDED,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.Files.FileColumns.MIME_TYPE,
directoryColumnName,
};
String whereClause = "(" + directoryColumnName + " LIKE ? OR " + directoryColumnName String whereClause = "(" + directoryColumnName + " LIKE ? OR " + directoryColumnName
+ " LIKE ? OR " + directoryColumnName + " LIKE ?) AND " + directoryColumnName + " LIKE ? OR " + directoryColumnName + " LIKE ?) AND " + directoryColumnName
+ " NOT LIKE ?"; + " NOT LIKE ?";
String[] whereArgs = null; String additionalClause = "";
if (mIncludeImages) {
additionalClause = MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;
}
if (mIncludeVideos) {
if (mIncludeImages) additionalClause += " OR ";
additionalClause += MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
}
if (!additionalClause.isEmpty()) whereClause += " AND (" + additionalClause + ")";
String cameraDir = getCameraDirectory(); String cameraDir = getCameraDirectory();
String picturesDir = Environment.DIRECTORY_PICTURES; String picturesDir = Environment.DIRECTORY_PICTURES;
...@@ -110,24 +140,37 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> { ...@@ -110,24 +140,37 @@ class FileEnumWorkerTask extends AsyncTask<List<PickerBitmap>> {
Environment.getExternalStoragePublicDirectory(screenshotsDir).toString(); Environment.getExternalStoragePublicDirectory(screenshotsDir).toString();
} }
whereArgs = new String[] { String[] whereArgs = new String[] {
// Include: // Include:
cameraDir + "%", picturesDir + "%", downloadsDir + "%", cameraDir + "%",
picturesDir + "%",
downloadsDir + "%",
// Exclude low-quality sources, such as the screenshots directory: // Exclude low-quality sources, such as the screenshots directory:
screenshotsDir + "%"}; screenshotsDir + "%",
};
final String orderBy = MediaStore.Images.Media.DATE_TAKEN + " DESC"; final String orderBy = MediaStore.MediaColumns.DATE_ADDED + " DESC";
Cursor imageCursor = mContentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Uri contentUri = MediaStore.Files.getContentUri("external");
selectColumns, whereClause, whereArgs, orderBy); Cursor imageCursor =
mContentResolver.query(contentUri, selectColumns, whereClause, whereArgs, orderBy);
while (imageCursor.moveToNext()) { while (imageCursor.moveToNext()) {
int dateTakenIndex = imageCursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN); int mimeTypeIndex = imageCursor.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE);
int idIndex = imageCursor.getColumnIndex(MediaStore.Images.ImageColumns._ID); String mimeType = imageCursor.getString(mimeTypeIndex);
Uri uri = ContentUris.withAppendedId( if (!mFilter.accept(null, mimeType)) continue;
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageCursor.getInt(idIndex));
int dateTakenIndex =
imageCursor.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED);
int idIndex = imageCursor.getColumnIndex(MediaStore.Files.FileColumns._ID);
Uri uri = ContentUris.withAppendedId(contentUri, imageCursor.getInt(idIndex));
long dateTaken = imageCursor.getLong(dateTakenIndex); long dateTaken = imageCursor.getLong(dateTakenIndex);
pickerBitmaps.add(new PickerBitmap(uri, dateTaken, PickerBitmap.TileTypes.PICTURE));
@PickerBitmap.TileTypes
int type = PickerBitmap.TileTypes.PICTURE;
if (mimeType.startsWith("video/")) type = PickerBitmap.TileTypes.VIDEO;
pickerBitmaps.add(new PickerBitmap(uri, dateTaken, type));
} }
imageCursor.close(); imageCursor.close();
......
...@@ -19,12 +19,13 @@ import java.util.Date; ...@@ -19,12 +19,13 @@ import java.util.Date;
*/ */
public class PickerBitmap implements Comparable<PickerBitmap> { public class PickerBitmap implements Comparable<PickerBitmap> {
// The possible types of tiles involved in the viewer. // The possible types of tiles involved in the viewer.
@IntDef({TileTypes.PICTURE, TileTypes.CAMERA, TileTypes.GALLERY}) @IntDef({TileTypes.PICTURE, TileTypes.CAMERA, TileTypes.GALLERY, TileTypes.VIDEO})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface TileTypes { public @interface TileTypes {
int PICTURE = 0; int PICTURE = 0;
int CAMERA = 1; int CAMERA = 1;
int GALLERY = 2; int GALLERY = 2;
int VIDEO = 3;
} }
// The URI of the bitmap to show. // The URI of the bitmap to show.
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
package org.chromium.chrome.browser.photo_picker; package org.chromium.chrome.browser.photo_picker;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
...@@ -54,6 +56,9 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -54,6 +56,9 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
// The image view containing the bitmap. // The image view containing the bitmap.
private ImageView mIconView; private ImageView mIconView;
// For video tiles, this lists the duration of the video. Blank for other types.
private TextView mVideoDuration;
// The little shader in the top left corner (provides backdrop for selection ring on // The little shader in the top left corner (provides backdrop for selection ring on
// unfavorable image backgrounds). // unfavorable image backgrounds).
private ImageView mScrim; private ImageView mScrim;
...@@ -97,6 +102,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -97,6 +102,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
mSpecialTile = findViewById(R.id.special_tile); mSpecialTile = findViewById(R.id.special_tile);
mSpecialTileIcon = (ImageView) findViewById(R.id.special_tile_icon); mSpecialTileIcon = (ImageView) findViewById(R.id.special_tile_icon);
mSpecialTileLabel = (TextView) findViewById(R.id.special_tile_label); mSpecialTileLabel = (TextView) findViewById(R.id.special_tile_label);
mVideoDuration = (TextView) findViewById(R.id.video_duration);
} }
@Override @Override
...@@ -172,12 +178,22 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -172,12 +178,22 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
if (needsResize) { if (needsResize) {
float start; float start;
float end; float end;
float videoDurationOffsetX;
float videoDurationOffsetY;
if (size != mCategoryView.getImageSize()) { if (size != mCategoryView.getImageSize()) {
start = 1f; start = 1f;
end = 0.8f; end = 0.8f;
float pixels = getResources().getDimensionPixelSize(
R.dimen.photo_picker_video_duration_offset);
videoDurationOffsetX = -pixels;
videoDurationOffsetY = pixels;
} else { } else {
start = 0.8f; start = 0.8f;
end = 1f; end = 1f;
videoDurationOffsetX = 0;
videoDurationOffsetY = 0;
} }
Animation animation = new ScaleAnimation( Animation animation = new ScaleAnimation(
...@@ -188,6 +204,15 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -188,6 +204,15 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
animation.setDuration(ANIMATION_DURATION); animation.setDuration(ANIMATION_DURATION);
animation.setFillAfter(true); // Keep the results of the animation. animation.setFillAfter(true); // Keep the results of the animation.
mIconView.startAnimation(animation); mIconView.startAnimation(animation);
ObjectAnimator videoDurationX = ObjectAnimator.ofFloat(
mVideoDuration, View.TRANSLATION_X, videoDurationOffsetX);
ObjectAnimator videoDurationY = ObjectAnimator.ofFloat(
mVideoDuration, View.TRANSLATION_Y, videoDurationOffsetY);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(videoDurationX, videoDurationY);
animatorSet.setDuration(ANIMATION_DURATION);
animatorSet.start();
} }
} }
...@@ -222,10 +247,11 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -222,10 +247,11 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
* respond to click events. * respond to click events.
* @param bitmapDetails The details about the bitmap represented by this PickerBitmapView. * @param bitmapDetails The details about the bitmap represented by this PickerBitmapView.
* @param thumbnail The Bitmap to use for the thumbnail (or null). * @param thumbnail The Bitmap to use for the thumbnail (or null).
* @param videoDuration The time-length of the video (human-friendly string).
* @param placeholder Whether the image given is a placeholder or the actual image. * @param placeholder Whether the image given is a placeholder or the actual image.
*/ */
public void initialize( public void initialize(PickerBitmap bitmapDetails, @Nullable Bitmap thumbnail,
PickerBitmap bitmapDetails, @Nullable Bitmap thumbnail, boolean placeholder) { String videoDuration, boolean placeholder) {
resetTile(); resetTile();
mBitmapDetails = bitmapDetails; mBitmapDetails = bitmapDetails;
...@@ -234,7 +260,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -234,7 +260,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
initializeSpecialTile(mBitmapDetails); initializeSpecialTile(mBitmapDetails);
mImageLoaded = true; mImageLoaded = true;
} else { } else {
setThumbnailBitmap(thumbnail); setThumbnailBitmap(thumbnail, videoDuration);
mImageLoaded = !placeholder; mImageLoaded = !placeholder;
} }
...@@ -246,18 +272,20 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -246,18 +272,20 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
* @param bitmapDetails The details about the bitmap represented by this PickerBitmapView. * @param bitmapDetails The details about the bitmap represented by this PickerBitmapView.
*/ */
public void initializeSpecialTile(PickerBitmap bitmapDetails) { public void initializeSpecialTile(PickerBitmap bitmapDetails) {
int labelStringId; int labelStringId = 0;
Drawable image; Drawable image = null;
Resources resources = mContext.getResources(); Resources resources = mContext.getResources();
if (isCameraTile()) { if (isCameraTile()) {
image = VectorDrawableCompat.create( image = VectorDrawableCompat.create(
resources, R.drawable.ic_photo_camera_grey, mContext.getTheme()); resources, R.drawable.ic_photo_camera_grey, mContext.getTheme());
labelStringId = R.string.photo_picker_camera; labelStringId = R.string.photo_picker_camera;
} else { } else if (isGalleryTile()) {
image = VectorDrawableCompat.create( image = VectorDrawableCompat.create(
resources, R.drawable.ic_collections_grey, mContext.getTheme()); resources, R.drawable.ic_collections_grey, mContext.getTheme());
labelStringId = R.string.photo_picker_browse; labelStringId = R.string.photo_picker_browse;
} else {
assert false;
} }
mSpecialTileIcon.setImageDrawable(image); mSpecialTileIcon.setImageDrawable(image);
...@@ -277,10 +305,12 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -277,10 +305,12 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
* Sets a thumbnail bitmap for the current view and ensures the selection border is showing, if * Sets a thumbnail bitmap for the current view and ensures the selection border is showing, if
* the image has already been selected. * the image has already been selected.
* @param thumbnail The Bitmap to use for the icon ImageView. * @param thumbnail The Bitmap to use for the icon ImageView.
* @param videoDuration The time-length of the video (human-friendly string).
* @return True if no image was loaded before (e.g. not even a low-res image). * @return True if no image was loaded before (e.g. not even a low-res image).
*/ */
public boolean setThumbnailBitmap(Bitmap thumbnail) { public boolean setThumbnailBitmap(Bitmap thumbnail, String videoDuration) {
mIconView.setImageBitmap(thumbnail); mIconView.setImageBitmap(thumbnail);
if (videoDuration != null) mVideoDuration.setText(videoDuration);
// If the tile has been selected before the bitmap has loaded, make sure it shows up with // If the tile has been selected before the bitmap has loaded, make sure it shows up with
// a selection border on load. // a selection border on load.
...@@ -320,6 +350,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -320,6 +350,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
*/ */
private void resetTile() { private void resetTile() {
mIconView.setImageBitmap(null); mIconView.setImageBitmap(null);
mVideoDuration.setText("");
mUnselectedView.setVisibility(View.GONE); mUnselectedView.setVisibility(View.GONE);
mSelectedView.setVisibility(View.GONE); mSelectedView.setVisibility(View.GONE);
mScrim.setVisibility(View.GONE); mScrim.setVisibility(View.GONE);
...@@ -378,6 +409,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> { ...@@ -378,6 +409,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
} }
private boolean isPictureTile() { private boolean isPictureTile() {
return mBitmapDetails.type() == PickerBitmap.TileTypes.PICTURE; return mBitmapDetails.type() == PickerBitmap.TileTypes.PICTURE
|| mBitmapDetails.type() == PickerBitmap.TileTypes.VIDEO;
} }
} }
...@@ -42,20 +42,21 @@ public class PickerBitmapViewHolder ...@@ -42,20 +42,21 @@ public class PickerBitmapViewHolder
// DecoderServiceHost.ImageDecodedCallback // DecoderServiceHost.ImageDecodedCallback
@Override @Override
public void imageDecodedCallback(String filePath, Bitmap bitmap) { public void imageDecodedCallback(String filePath, Bitmap bitmap, String videoDuration) {
if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) { if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
return; return;
} }
if (mCategoryView.getHighResBitmaps().get(filePath) == null) { if (mCategoryView.getHighResThumbnails().get(filePath) == null) {
mCategoryView.getHighResBitmaps().put(filePath, bitmap); mCategoryView.getHighResThumbnails().put(
filePath, new PickerCategoryView.Thumbnail(bitmap, videoDuration));
} }
if (mCategoryView.getLowResBitmaps().get(filePath) == null) { if (mCategoryView.getLowResThumbnails().get(filePath) == null) {
Resources resources = mItemView.getContext().getResources(); Resources resources = mItemView.getContext().getResources();
new BitmapScalerTask(mCategoryView.getLowResBitmaps(), filePath, new BitmapScalerTask(mCategoryView.getLowResThumbnails(), bitmap, filePath,
resources.getDimensionPixelSize(R.dimen.photo_picker_grainy_thumbnail_size), videoDuration,
bitmap) resources.getDimensionPixelSize(R.dimen.photo_picker_grainy_thumbnail_size))
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
...@@ -63,7 +64,7 @@ public class PickerBitmapViewHolder ...@@ -63,7 +64,7 @@ public class PickerBitmapViewHolder
return; return;
} }
if (mItemView.setThumbnailBitmap(bitmap)) { if (mItemView.setThumbnailBitmap(bitmap, videoDuration)) {
mItemView.fadeInThumbnail(); mItemView.fadeInThumbnail();
} }
} }
...@@ -83,20 +84,21 @@ public class PickerBitmapViewHolder ...@@ -83,20 +84,21 @@ public class PickerBitmapViewHolder
if (mBitmapDetails.type() == PickerBitmap.TileTypes.CAMERA if (mBitmapDetails.type() == PickerBitmap.TileTypes.CAMERA
|| mBitmapDetails.type() == PickerBitmap.TileTypes.GALLERY) { || mBitmapDetails.type() == PickerBitmap.TileTypes.GALLERY) {
mItemView.initialize(mBitmapDetails, null, false); mItemView.initialize(mBitmapDetails, null, null, false);
return PickerAdapter.DecodeActions.NO_ACTION; return PickerAdapter.DecodeActions.NO_ACTION;
} }
String filePath = mBitmapDetails.getUri().getPath(); String filePath = mBitmapDetails.getUri().getPath();
Bitmap original = mCategoryView.getHighResBitmaps().get(filePath); PickerCategoryView.Thumbnail original = mCategoryView.getHighResThumbnails().get(filePath);
if (original != null) { if (original != null) {
mItemView.initialize(mBitmapDetails, original, false); mItemView.initialize(mBitmapDetails, original.bitmap, original.videoDuration, false);
return PickerAdapter.DecodeActions.FROM_CACHE; return PickerAdapter.DecodeActions.FROM_CACHE;
} }
int size = mCategoryView.getImageSize(); int size = mCategoryView.getImageSize();
Bitmap placeholder = mCategoryView.getLowResBitmaps().get(filePath); PickerCategoryView.Thumbnail payload = mCategoryView.getLowResThumbnails().get(filePath);
if (placeholder != null) { if (payload != null) {
Bitmap placeholder = payload.bitmap;
// For performance stats see http://crbug.com/719919. // For performance stats see http://crbug.com/719919.
long begin = SystemClock.elapsedRealtime(); long begin = SystemClock.elapsedRealtime();
placeholder = BitmapUtils.scale(placeholder, size, false); placeholder = BitmapUtils.scale(placeholder, size, false);
...@@ -104,12 +106,13 @@ public class PickerBitmapViewHolder ...@@ -104,12 +106,13 @@ public class PickerBitmapViewHolder
RecordHistogram.recordTimesHistogram( RecordHistogram.recordTimesHistogram(
"Android.PhotoPicker.UpscaleLowResBitmap", scaleTime); "Android.PhotoPicker.UpscaleLowResBitmap", scaleTime);
mItemView.initialize(mBitmapDetails, placeholder, true); mItemView.initialize(mBitmapDetails, placeholder, payload.videoDuration, true);
} else { } else {
mItemView.initialize(mBitmapDetails, null, true); mItemView.initialize(mBitmapDetails, null, null, true);
} }
mCategoryView.getDecoderServiceHost().decodeImage(mBitmapDetails.getUri(), size, this); mCategoryView.getDecoderServiceHost().decodeImage(
mBitmapDetails.getUri(), mBitmapDetails.type(), size, this);
return PickerAdapter.DecodeActions.DECODE; return PickerAdapter.DecodeActions.DECODE;
} }
......
...@@ -52,6 +52,20 @@ public class PickerCategoryView extends RelativeLayout ...@@ -52,6 +52,20 @@ public class PickerCategoryView extends RelativeLayout
private static final int ACTION_BROWSE = 3; private static final int ACTION_BROWSE = 3;
private static final int ACTION_BOUNDARY = 4; private static final int ACTION_BOUNDARY = 4;
/**
* A container class for keeping track of the data we need to show a photo/video tile in the
* photo picker (the data we store in the cache).
*/
static public class Thumbnail {
public Bitmap bitmap;
public String videoDuration;
Thumbnail(Bitmap bitmap, String videoDuration) {
this.bitmap = bitmap;
this.videoDuration = videoDuration;
}
}
// The dialog that owns us. // The dialog that owns us.
private PhotoPickerDialog mDialog; private PhotoPickerDialog mDialog;
...@@ -88,13 +102,13 @@ public class PickerCategoryView extends RelativeLayout ...@@ -88,13 +102,13 @@ public class PickerCategoryView extends RelativeLayout
// The {@link SelectionDelegate} keeping track of which images are selected. // The {@link SelectionDelegate} keeping track of which images are selected.
private SelectionDelegate<PickerBitmap> mSelectionDelegate; private SelectionDelegate<PickerBitmap> mSelectionDelegate;
// A low-resolution cache for images, lazily created. Helpful for cache misses from the // A low-resolution cache for thumbnails, lazily created. Helpful for cache misses from the
// high-resolution cache to avoid showing gray squares (we show pixelated versions instead until // high-resolution cache to avoid showing gray squares (we show pixelated versions instead until
// image can be loaded off disk, which is much less jarring). // image can be loaded off disk, which is much less jarring).
private DiscardableReference<LruCache<String, Bitmap>> mLowResBitmaps; private DiscardableReference<LruCache<String, Thumbnail>> mLowResThumbnails;
// A high-resolution cache for images, lazily created. // A high-resolution cache for thumbnails, lazily created.
private DiscardableReference<LruCache<String, Bitmap>> mHighResBitmaps; private DiscardableReference<LruCache<String, Thumbnail>> mHighResThumbnails;
// The size of the low-res cache. // The size of the low-res cache.
private int mCacheSizeLarge; private int mCacheSizeLarge;
...@@ -118,7 +132,7 @@ public class PickerCategoryView extends RelativeLayout ...@@ -118,7 +132,7 @@ public class PickerCategoryView extends RelativeLayout
// A worker task for asynchronously enumerating files off the main thread. // A worker task for asynchronously enumerating files off the main thread.
private FileEnumWorkerTask mWorkerTask; private FileEnumWorkerTask mWorkerTask;
// The timestap for the start of the enumeration of files on disk. // The timestamp for the start of the enumeration of files on disk.
private long mEnumStartTime; private long mEnumStartTime;
// Whether the connection to the service has been established. // Whether the connection to the service has been established.
...@@ -303,20 +317,20 @@ public class PickerCategoryView extends RelativeLayout ...@@ -303,20 +317,20 @@ public class PickerCategoryView extends RelativeLayout
return mDecoderServiceHost; return mDecoderServiceHost;
} }
public LruCache<String, Bitmap> getLowResBitmaps() { public LruCache<String, Thumbnail> getLowResThumbnails() {
if (mLowResBitmaps == null || mLowResBitmaps.get() == null) { if (mLowResThumbnails == null || mLowResThumbnails.get() == null) {
mLowResBitmaps = mLowResThumbnails = mActivity.getReferencePool().put(
mActivity.getReferencePool().put(new LruCache<String, Bitmap>(mCacheSizeSmall)); new LruCache<String, Thumbnail>(mCacheSizeSmall));
} }
return mLowResBitmaps.get(); return mLowResThumbnails.get();
} }
public LruCache<String, Bitmap> getHighResBitmaps() { public LruCache<String, Thumbnail> getHighResThumbnails() {
if (mHighResBitmaps == null || mHighResBitmaps.get() == null) { if (mHighResThumbnails == null || mHighResThumbnails.get() == null) {
mHighResBitmaps = mHighResThumbnails = mActivity.getReferencePool().put(
mActivity.getReferencePool().put(new LruCache<String, Bitmap>(mCacheSizeLarge)); new LruCache<String, Thumbnail>(mCacheSizeLarge));
} }
return mHighResBitmaps.get(); return mHighResThumbnails.get();
} }
public boolean isMultiSelectAllowed() { public boolean isMultiSelectAllowed() {
...@@ -372,7 +386,7 @@ public class PickerCategoryView extends RelativeLayout ...@@ -372,7 +386,7 @@ public class PickerCategoryView extends RelativeLayout
mEnumStartTime = SystemClock.elapsedRealtime(); mEnumStartTime = SystemClock.elapsedRealtime();
mWorkerTask = new FileEnumWorkerTask(mActivity.getWindowAndroid(), this, mWorkerTask = new FileEnumWorkerTask(mActivity.getWindowAndroid(), this,
new MimeTypeFilter(mMimeTypes, true), mActivity.getContentResolver()); new MimeTypeFilter(mMimeTypes, true), mMimeTypes, mActivity.getContentResolver());
mWorkerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); mWorkerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
......
...@@ -152,6 +152,7 @@ const base::Feature* kFeaturesExposedToJava[] = { ...@@ -152,6 +152,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&kOmniboxSpareRenderer, &kOmniboxSpareRenderer,
&kOverlayNewLayout, &kOverlayNewLayout,
&kPayWithGoogleV1, &kPayWithGoogleV1,
&kPhotoPickerVideoSupport,
&kProgressBarThrottleFeature, &kProgressBarThrottleFeature,
&kPwaImprovedSplashScreen, &kPwaImprovedSplashScreen,
&kPwaPersistentNotification, &kPwaPersistentNotification,
...@@ -470,6 +471,11 @@ const base::Feature kOverlayNewLayout{"OverlayNewLayout", ...@@ -470,6 +471,11 @@ const base::Feature kOverlayNewLayout{"OverlayNewLayout",
const base::Feature kPayWithGoogleV1{"PayWithGoogleV1", const base::Feature kPayWithGoogleV1{"PayWithGoogleV1",
base::FEATURE_ENABLED_BY_DEFAULT}; base::FEATURE_ENABLED_BY_DEFAULT};
// TODO(finnur): Before enabling by default, the issue of where decoding should
// take place needs to be resolved.
const base::Feature kPhotoPickerVideoSupport{"PhotoPickerVideoSupport",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kProgressBarThrottleFeature{ const base::Feature kProgressBarThrottleFeature{
"ProgressBarThrottle", base::FEATURE_DISABLED_BY_DEFAULT}; "ProgressBarThrottle", base::FEATURE_DISABLED_BY_DEFAULT};
......
...@@ -89,6 +89,7 @@ extern const base::Feature kNTPLaunchAfterInactivity; ...@@ -89,6 +89,7 @@ extern const base::Feature kNTPLaunchAfterInactivity;
extern const base::Feature kOmniboxSpareRenderer; extern const base::Feature kOmniboxSpareRenderer;
extern const base::Feature kOverlayNewLayout; extern const base::Feature kOverlayNewLayout;
extern const base::Feature kPayWithGoogleV1; extern const base::Feature kPayWithGoogleV1;
extern const base::Feature kPhotoPickerVideoSupport;
extern const base::Feature kProgressBarThrottleFeature; extern const base::Feature kProgressBarThrottleFeature;
extern const base::Feature kPwaImprovedSplashScreen; extern const base::Feature kPwaImprovedSplashScreen;
extern const base::Feature kPwaPersistentNotification; extern const base::Feature kPwaPersistentNotification;
......
...@@ -129,6 +129,11 @@ public class UiUtils { ...@@ -129,6 +129,11 @@ public class UiUtils {
* Called when the photo picker dialog has been dismissed. * Called when the photo picker dialog has been dismissed.
*/ */
void onPhotoPickerDismissed(); void onPhotoPickerDismissed();
/**
* Returns whether video decoding support is supported in the photo picker.
*/
boolean supportsVideos();
} }
// ContactsPickerDelegate: // ContactsPickerDelegate:
...@@ -186,6 +191,15 @@ public class UiUtils { ...@@ -186,6 +191,15 @@ public class UiUtils {
return sPhotoPickerDelegate != null; return sPhotoPickerDelegate != null;
} }
/**
* Returns whether the photo picker supports showing videos.
*/
public static boolean photoPickerSupportsVideo() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false;
if (!shouldShowPhotoPicker()) return false;
return sPhotoPickerDelegate.supportsVideos();
}
/** /**
* Called to display the photo picker. * Called to display the photo picker.
* @param context The context to use. * @param context The context to use.
......
...@@ -246,7 +246,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -246,7 +246,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
Activity activity = mWindowAndroid.getActivity().get(); Activity activity = mWindowAndroid.getActivity().get();
// Use the new photo picker, if available. // Use the new photo picker, if available.
List<String> imageMimeTypes = convertToImageMimeTypes(mFileTypes); List<String> imageMimeTypes = convertToSupportedPhotoPickerTypes(mFileTypes);
if (shouldUsePhotoPicker() if (shouldUsePhotoPicker()
&& UiUtils.showPhotoPicker(activity, this, mAllowMultiple, imageMimeTypes)) { && UiUtils.showPhotoPicker(activity, this, mAllowMultiple, imageMimeTypes)) {
return; return;
...@@ -308,26 +308,28 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -308,26 +308,28 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
* 4.) There is a valid Android Activity associated with the file request. * 4.) There is a valid Android Activity associated with the file request.
*/ */
private boolean shouldUsePhotoPicker() { private boolean shouldUsePhotoPicker() {
List<String> imageMimeTypes = convertToImageMimeTypes(mFileTypes); List<String> mediaMimeTypes = convertToSupportedPhotoPickerTypes(mFileTypes);
return !captureImage() && imageMimeTypes != null && UiUtils.shouldShowPhotoPicker() return !captureImage() && mediaMimeTypes != null && UiUtils.shouldShowPhotoPicker()
&& mWindowAndroid.getActivity().get() != null; && mWindowAndroid.getActivity().get() != null;
} }
/** /**
* Converts a list of extensions and Mime types to a list of de-duped Mime types containing * Converts a list of extensions and Mime types to a list of de-duped Mime types supported by
* image types only. If the input list contains a non-image type, then null is returned. * the photo picker only. If the input list contains a unsupported type, then null is returned.
* @param fileTypes the list of filetypes (extensions and Mime types) to convert. * @param fileTypes the list of filetypes (extensions and Mime types) to convert.
* @return A de-duped list of Image Mime types only, or null if one or more non-image types were * @return A de-duped list of supported types only, or null if one or more unsupported types
* given as input. * were given as input.
*/ */
@VisibleForTesting @VisibleForTesting
public static List<String> convertToImageMimeTypes(List<String> fileTypes) { public static List<String> convertToSupportedPhotoPickerTypes(List<String> fileTypes) {
if (fileTypes.size() == 0) return null; if (fileTypes.size() == 0) return null;
List<String> mimeTypes = new ArrayList<>(); List<String> mimeTypes = new ArrayList<>();
for (String type : fileTypes) { for (String type : fileTypes) {
String mimeType = ensureMimeType(type); String mimeType = ensureMimeType(type);
if (!mimeType.startsWith("image/")) { if (!mimeType.startsWith("image/")) {
return null; if (!UiUtils.photoPickerSupportsVideo() || !mimeType.startsWith("video/")) {
return null;
}
} }
if (!mimeTypes.contains(mimeType)) mimeTypes.add(mimeType); if (!mimeTypes.contains(mimeType)) mimeTypes.add(mimeType);
} }
...@@ -736,7 +738,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick ...@@ -736,7 +738,7 @@ public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPick
} }
private boolean eligibleForPhotoPicker() { private boolean eligibleForPhotoPicker() {
return convertToImageMimeTypes(mFileTypes) != null; return convertToSupportedPhotoPickerTypes(mFileTypes) != null;
} }
private void onFileSelected( private void onFileSelected(
......
...@@ -100,7 +100,7 @@ public class SelectFileDialogTest { ...@@ -100,7 +100,7 @@ public class SelectFileDialogTest {
shadowMimeTypeMap.addExtensionMimeTypMapping("jpg", "image/jpeg"); shadowMimeTypeMap.addExtensionMimeTypMapping("jpg", "image/jpeg");
shadowMimeTypeMap.addExtensionMimeTypMapping("gif", "image/gif"); shadowMimeTypeMap.addExtensionMimeTypMapping("gif", "image/gif");
shadowMimeTypeMap.addExtensionMimeTypMapping("txt", "text/plain"); shadowMimeTypeMap.addExtensionMimeTypMapping("txt", "text/plain");
shadowMimeTypeMap.addExtensionMimeTypMapping("mpg", "audio/mpeg"); shadowMimeTypeMap.addExtensionMimeTypMapping("mpg", "video/mpeg");
assertEquals("", SelectFileDialog.ensureMimeType("")); assertEquals("", SelectFileDialog.ensureMimeType(""));
assertEquals("image/jpeg", SelectFileDialog.ensureMimeType(".jpg")); assertEquals("image/jpeg", SelectFileDialog.ensureMimeType(".jpg"));
...@@ -108,26 +108,35 @@ public class SelectFileDialogTest { ...@@ -108,26 +108,35 @@ public class SelectFileDialogTest {
// Unknown extension, expect default response: // Unknown extension, expect default response:
assertEquals("application/octet-stream", SelectFileDialog.ensureMimeType(".flv")); assertEquals("application/octet-stream", SelectFileDialog.ensureMimeType(".flv"));
assertEquals(null, SelectFileDialog.convertToImageMimeTypes(new ArrayList<>())); assertEquals(null, SelectFileDialog.convertToSupportedPhotoPickerTypes(new ArrayList<>()));
assertEquals(null, SelectFileDialog.convertToImageMimeTypes(Arrays.asList(""))); assertEquals(null, SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("")));
assertEquals(null, SelectFileDialog.convertToImageMimeTypes(Arrays.asList("foo/bar"))); assertEquals(null,
SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("foo/bar")));
assertEquals(Arrays.asList("image/jpeg"), assertEquals(Arrays.asList("image/jpeg"),
SelectFileDialog.convertToImageMimeTypes(Arrays.asList(".jpg"))); SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList(".jpg")));
assertEquals(Arrays.asList("image/jpeg"), assertEquals(Arrays.asList("image/jpeg"),
SelectFileDialog.convertToImageMimeTypes(Arrays.asList("image/jpeg"))); SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("image/jpeg")));
assertEquals(Arrays.asList("image/jpeg"), assertEquals(Arrays.asList("image/jpeg"),
SelectFileDialog.convertToImageMimeTypes(Arrays.asList(".jpg", "image/jpeg"))); SelectFileDialog.convertToSupportedPhotoPickerTypes(
Arrays.asList(".jpg", "image/jpeg")));
assertEquals(Arrays.asList("image/gif", "image/jpeg"), assertEquals(Arrays.asList("image/gif", "image/jpeg"),
SelectFileDialog.convertToImageMimeTypes(Arrays.asList(".gif", "image/jpeg"))); SelectFileDialog.convertToSupportedPhotoPickerTypes(
Arrays.asList(".gif", "image/jpeg")));
// Returns null because generic picker is required (due to addition of .txt file). // Video and mixed video/images support. This feature is supported, but off by default, so
// expect failure until it is turned on by default.
assertEquals(
null, SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList(".mpg")));
assertEquals(null, assertEquals(null,
SelectFileDialog.convertToImageMimeTypes( SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("video/mpeg")));
Arrays.asList(".txt", ".jpg", "image/jpeg")));
// Returns null because video file is included.
assertEquals(null, assertEquals(null,
SelectFileDialog.convertToImageMimeTypes( SelectFileDialog.convertToSupportedPhotoPickerTypes(
Arrays.asList(".jpg", "image/jpeg", ".mpg"))); Arrays.asList(".jpg", "image/jpeg", ".mpg")));
// Returns null because generic picker is required (due to addition of .txt file).
assertEquals(null,
SelectFileDialog.convertToSupportedPhotoPickerTypes(
Arrays.asList(".txt", ".jpg", "image/jpeg")));
} }
@Test @Test
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment