Commit 14bee33d authored by Finnur Thorarinsson's avatar Finnur Thorarinsson Committed by Commit Bot

Photo Picker: Implement Zoom-in functionality.

The background for the Zoom icon is placeholder (haven't received
the 9patch image yet).

Animations between states will be included in a follow-up CL.

Bug: 1029056, 656015
Change-Id: Ic5657246ca21f7d6a1fcf26a42e2148d0771a05d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1936770
Commit-Queue: Finnur Thorarinsson <finnur@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#719757}
parent b2703ae9
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<!-- TODO(finnur): Replace with real background -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<size android:width="42dp" android:height="42dp" />
<solid android:color="@color/modern_primary_color" />
<corners android:radius="42dp" />
</shape>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/default_icon_color"
android:pathData="M15.5 14h-0.79l-0.28-0.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-0.59 4.23-1.57l0.27 0.28 v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
<path
android:pathData="M0 0h24v24H0V0z" />
<path
android:fillColor="@color/default_icon_color"
android:pathData="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0 0h24v24H0V0z" />
<path
android:fillColor="@color/default_icon_color"
android:pathData="M15.5 14h-0.79l-0.28-0.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-0.59 4.23-1.57l0.27 0.28 v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z" />
</vector>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<FrameLayout <FrameLayout
android:id="@+id/border" android:id="@+id/border"
...@@ -96,7 +96,7 @@ ...@@ -96,7 +96,7 @@
<TextView <TextView
android:id="@+id/special_tile_label" android:id="@+id/special_tile_label"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/photo_picker_label_gap" android:layout_marginTop="@dimen/photo_picker_label_gap"
android:gravity="center" android:gravity="center"
......
...@@ -3,10 +3,9 @@ ...@@ -3,10 +3,9 @@
Use of this source code is governed by a BSD-style license that can be Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. --> found in the LICENSE file. -->
<FrameLayout <org.chromium.ui.widget.OptimizedFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/default_bg_color_elev_0" > android:background="@color/default_bg_color_elev_0" >
...@@ -17,6 +16,18 @@ ...@@ -17,6 +16,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/modern_primary_color" /> android:background="@color/modern_primary_color" />
<ImageView
android:id="@+id/zoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:layout_centerHorizontal="true"
android:background="@drawable/photo_picker_zoom_background"
android:src="@drawable/zoom_in"
android:visibility="gone" />
<RelativeLayout <RelativeLayout
android:id="@+id/playback_container" android:id="@+id/playback_container"
android:layout_width="match_parent" android:layout_width="match_parent"
...@@ -55,4 +66,4 @@ ...@@ -55,4 +66,4 @@
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>
</FrameLayout> </org.chromium.ui.widget.OptimizedFrameLayout>
\ No newline at end of file \ No newline at end of file
...@@ -510,7 +510,6 @@ ...@@ -510,7 +510,6 @@
<dimen name="contact_picker_icon_size">36dp</dimen> <dimen name="contact_picker_icon_size">36dp</dimen>
<!-- Photo Picker dimensions --> <!-- Photo Picker dimensions -->
<dimen name="photo_picker_selected_padding">12dp</dimen>
<dimen name="photo_picker_label_gap">10dp</dimen> <dimen name="photo_picker_label_gap">10dp</dimen>
<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>
......
...@@ -307,6 +307,7 @@ public abstract class ChromeFeatureList { ...@@ -307,6 +307,7 @@ public abstract class ChromeFeatureList {
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 PHOTO_PICKER_VIDEO_SUPPORT = "PhotoPickerVideoSupport";
public static final String PHOTO_PICKER_ZOOM = "PhotoPickerZoom";
public static final String PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES = public static final String PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES =
"PredictivePrefetchingAllowedOnAllConnectionTypes"; "PredictivePrefetchingAllowedOnAllConnectionTypes";
public static final String PRIORITIZE_BOOTSTRAP_TASKS = "PrioritizeBootstrapTasks"; public static final String PRIORITIZE_BOOTSTRAP_TASKS = "PrioritizeBootstrapTasks";
......
...@@ -24,17 +24,19 @@ class BitmapScalerTask extends AsyncTask<Bitmap> { ...@@ -24,17 +24,19 @@ class BitmapScalerTask extends AsyncTask<Bitmap> {
private final int mSize; private final int mSize;
private final Bitmap mBitmap; private final Bitmap mBitmap;
private final String mVideoDuration; private final String mVideoDuration;
private final float mRatio;
/** /**
* A BitmapScalerTask constructor. * A BitmapScalerTask constructor.
*/ */
public BitmapScalerTask(LruCache<String, PickerCategoryView.Thumbnail> cache, Bitmap bitmap, public BitmapScalerTask(LruCache<String, PickerCategoryView.Thumbnail> cache, Bitmap bitmap,
String filePath, String videoDuration, int size) { String filePath, String videoDuration, int size, float ratio) {
mCache = cache; mCache = cache;
mFilePath = filePath; mFilePath = filePath;
mSize = size; mSize = size;
mBitmap = bitmap; mBitmap = bitmap;
mVideoDuration = videoDuration; mVideoDuration = videoDuration;
mRatio = ratio;
} }
/** /**
...@@ -66,6 +68,8 @@ class BitmapScalerTask extends AsyncTask<Bitmap> { ...@@ -66,6 +68,8 @@ class BitmapScalerTask extends AsyncTask<Bitmap> {
List<Bitmap> bitmaps = new ArrayList<>(1); List<Bitmap> bitmaps = new ArrayList<>(1);
bitmaps.add(bitmap); bitmaps.add(bitmap);
mCache.put(mFilePath, new PickerCategoryView.Thumbnail(bitmaps, mVideoDuration)); mCache.put(mFilePath,
new PickerCategoryView.Thumbnail(
bitmaps, mVideoDuration, /*fullWidth=*/false, mRatio));
} }
} }
...@@ -10,6 +10,7 @@ import android.graphics.Matrix; ...@@ -10,6 +10,7 @@ import android.graphics.Matrix;
import android.media.ExifInterface; import android.media.ExifInterface;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.os.Build; import android.os.Build;
import android.util.Pair;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
...@@ -37,29 +38,44 @@ class BitmapUtils { ...@@ -37,29 +38,44 @@ class BitmapUtils {
private static final int EXIF_ORIENTATION_ACTION_BOUNDARY = 9; private static final int EXIF_ORIENTATION_ACTION_BOUNDARY = 9;
/** /**
* Takes a |bitmap| and returns a square thumbnail of |size|x|size| from the center of the * Takes a |bitmap| and (if |!fullWidth|) returns a square thumbnail of |width|x|width| from the
* bitmap specified, rotating it according to the Exif information, if needed (on Nougat and * center of the bitmap specified, or (if |fullWidth|) an image that scaled to fit within
* up only). * |width|. The image is rotated according to the Exif information, if needed (on Nougat and up
* only).
* @param bitmap The bitmap to adjust. * @param bitmap The bitmap to adjust.
* @param size The desired size (width and height). * @param width The desired width (and height if fullWidth is false).
* @param fullWidth Whether full screen width is in use. When true, the image returned is
* |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
* image is returned.
* @param descriptor The file descriptor to read the Exif information from. * @param descriptor The file descriptor to read the Exif information from.
* @return The new bitmap thumbnail. * @return The new bitmap thumbnail.
*/ */
private static Bitmap sizeBitmap(Bitmap bitmap, int size, FileDescriptor descriptor) { private static Bitmap sizeBitmap(
Bitmap bitmap, int width, boolean fullWidth, FileDescriptor descriptor) {
// TODO(finnur): Investigate options that require fewer bitmaps to be created. // TODO(finnur): Investigate options that require fewer bitmaps to be created.
bitmap = ensureMinSize(bitmap, size); if (!fullWidth) {
bitmap = rotateAndCropToSquare(bitmap, size, descriptor); bitmap = ensureMinSize(bitmap, width);
return bitmap; bitmap = rotateAndCropToSquare(bitmap, width, descriptor);
return bitmap;
} else {
return rotateAndFitToMaxWidth(bitmap, width, descriptor);
}
} }
/** /**
* Given a FileDescriptor, decodes the contents and returns a bitmap of * Given a FileDescriptor, decodes the contents and returns a square thumbnail of
* dimensions |size|x|size|. * |width|x|width| from the center of the bitmap specified, or (if |fullwidth|) an image that
* scaled to fit within |width|. The image is rotated according to the Exif information, if
* needed (on Nougat and up only).
* @param descriptor The FileDescriptor for the file to read. * @param descriptor The FileDescriptor for the file to read.
* @param size The width and height of the bitmap to return. * @param size The width of the bitmap to return.
* @return The resulting bitmap. * @param fullWidth Whether full screen width is in use. When true, the image returned is
* |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
* image is returned.
* @return The resulting bitmap and its ratio.
*/ */
public static Bitmap decodeBitmapFromFileDescriptor(FileDescriptor descriptor, int size) { public static Pair<Bitmap, Float> decodeBitmapFromFileDescriptor(
FileDescriptor descriptor, int size, boolean fullWidth) {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(descriptor, null, options); BitmapFactory.decodeFileDescriptor(descriptor, null, options);
...@@ -69,31 +85,41 @@ class BitmapUtils { ...@@ -69,31 +85,41 @@ class BitmapUtils {
if (bitmap == null) return null; if (bitmap == null) return null;
return sizeBitmap(bitmap, size, descriptor); return new Pair<Bitmap, Float>(sizeBitmap(bitmap, size, fullWidth, descriptor),
(float) bitmap.getHeight() / bitmap.getWidth());
} }
/** /**
* Given a FileDescriptor, decodes the video and returns a bitmap of dimensions |size|x|size|. * Given a FileDescriptor, decodes the video and returns a square thumbnail of |width|x|width|
* from the center of the bitmap specified, or (if |fullwidth|) an image that scaled to fit
* within |width|. The image is rotated according to the Exif information, if needed (on Nougat
* and up only).
* @param retriever The MediaMetadataRetriever to use (must have source already set). * @param retriever The MediaMetadataRetriever to use (must have source already set).
* @param descriptor The FileDescriptor for the file to read. * @param descriptor The FileDescriptor for the file to read.
* @param size The width and height of the bitmap to return. * @param width The width of the bitmap to return.
* @param frames The number of frames to extract. * @param frames The number of frames to extract.
* @param fullWidth Whether full screen width is in use. When true, the image returned is
* |width| wide and whatever height scales to. When false, a rectangular |width|x|width|
* image is returned.
* @param intervalMs The interval between frames (in milliseconds). * @param intervalMs The interval between frames (in milliseconds).
* @return A list of extracted frames. * @return A list of extracted frames.
*/ */
public static List<Bitmap> decodeVideoFromFileDescriptor(MediaMetadataRetriever retriever, public static Pair<List<Bitmap>, Float> decodeVideoFromFileDescriptor(
FileDescriptor descriptor, int size, int frames, long intervalMs) { MediaMetadataRetriever retriever, FileDescriptor descriptor, int width, int frames,
boolean fullWidth, long intervalMs) {
List<Bitmap> bitmaps = new ArrayList<Bitmap>(); List<Bitmap> bitmaps = new ArrayList<Bitmap>();
Bitmap bitmap = null; Bitmap bitmap = null;
Float ratio = null;
for (int frame = 0; frame < frames; ++frame) { for (int frame = 0; frame < frames; ++frame) {
bitmap = retriever.getFrameAtTime(frame * intervalMs * 1000); bitmap = retriever.getFrameAtTime(frame * intervalMs * 1000);
if (bitmap == null) continue; if (bitmap == null) continue;
if (ratio == null) ratio = (float) bitmap.getHeight() / bitmap.getWidth();
bitmap = sizeBitmap(bitmap, size, descriptor); bitmap = sizeBitmap(bitmap, width, fullWidth, descriptor);
bitmaps.add(bitmap); bitmaps.add(bitmap);
} }
return bitmaps; return new Pair<List<Bitmap>, Float>(bitmaps, ratio);
} }
/** /**
...@@ -158,14 +184,12 @@ class BitmapUtils { ...@@ -158,14 +184,12 @@ class BitmapUtils {
} }
/** /**
* Crops a |bitmap| to a certain square |size| and rotates it according to the Exif information, * Returns the rotation matrix from the Exif information in the file descriptor (on Nougat and
* if needed (on Nougat and up only). * up only).
* @param bitmap The bitmap to crop. * @param descriptor The FileDescriptor containing the Exif information.
* @param size The size desired (width and height). * @return The resulting rotation matrix (or null, if Android < N).
* @return The resulting (square) bitmap.
*/ */
private static Bitmap rotateAndCropToSquare( private static Matrix getRotationMatrix(FileDescriptor descriptor) {
Bitmap bitmap, int size, FileDescriptor descriptor) {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try { try {
...@@ -215,7 +239,19 @@ class BitmapUtils { ...@@ -215,7 +239,19 @@ class BitmapUtils {
} catch (IOException e) { } catch (IOException e) {
} }
} }
return matrix;
}
/**
* Crops a |bitmap| to a certain square |size| and (on Nougat and up only) rotates it according
* to the Exif information, if needed.
* @param bitmap The bitmap to crop.
* @param size The size desired (width and height).
* @param descriptor The FileDescriptor containing the Exif information.
* @return The resulting (square) bitmap.
*/
private static Bitmap rotateAndCropToSquare(
Bitmap bitmap, int size, FileDescriptor descriptor) {
int x = 0; int x = 0;
int y = 0; int y = 0;
int width = bitmap.getWidth(); int width = bitmap.getWidth();
...@@ -224,7 +260,25 @@ class BitmapUtils { ...@@ -224,7 +260,25 @@ class BitmapUtils {
if (width > size) x = (width - size) / 2; if (width > size) x = (width - size) / 2;
if (height > size) y = (height - size) / 2; if (height > size) y = (height - size) / 2;
return Bitmap.createBitmap(bitmap, x, y, size, size, matrix, true); return Bitmap.createBitmap(bitmap, x, y, size, size, getRotationMatrix(descriptor), true);
}
/**
* Rotate a bitmap according to its Exif information and make sure it fits to the maximum width.
* @param bitmap The input bitmap.
* @param maxWidth The maximum width available.
* @param descriptor The FileDescriptor containing the Exif information.
*/
private static Bitmap rotateAndFitToMaxWidth(
Bitmap bitmap, int maxWidth, FileDescriptor descriptor) {
Bitmap rotated = bitmap;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
getRotationMatrix(descriptor), true);
}
float ratio = (float) maxWidth / rotated.getWidth();
int height = (int) (rotated.getHeight() * ratio);
return Bitmap.createScaledBitmap(rotated, maxWidth, height, true);
} }
/** /**
......
...@@ -26,7 +26,7 @@ import java.util.Locale; ...@@ -26,7 +26,7 @@ import java.util.Locale;
/** /**
* A worker task to decode video and extract information from it off of the UI thread. * A worker task to decode video and extract information from it off of the UI thread.
*/ */
class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { class DecodeVideoTask extends AsyncTask<List<Bitmap>> {
/** /**
* The possible error states while decoding. * The possible error states while decoding.
*/ */
...@@ -49,10 +49,11 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -49,10 +49,11 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
* @param uri The uri of the video decoded. * @param uri The uri of the video decoded.
* @param bitmaps An array of thumbnails extracted from the video. * @param bitmaps An array of thumbnails extracted from the video.
* @param duration The duration of the video. * @param duration The duration of the video.
* @param fullWidth Whether the image is using the full width of the screen.
* @param decodingStatus Whether the decoding was successful. * @param decodingStatus Whether the decoding was successful.
*/ */
void videoDecodedCallback( void videoDecodedCallback(Uri uri, List<Bitmap> bitmaps, String duration, boolean fullWidth,
Uri uri, List<Bitmap> bitmaps, String duration, @DecodingResult int decodingStatus); @DecodingResult int decodingStatus, float ratio);
} }
// The callback to use to communicate the results. // The callback to use to communicate the results.
...@@ -64,6 +65,9 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -64,6 +65,9 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
// The desired width and height (in pixels) of the returned thumbnail from the video. // The desired width and height (in pixels) of the returned thumbnail from the video.
int mSize; int mSize;
// Whether the image is taking up the full width of the screen.
boolean mFullWidth;
// The number of frames to extract. // The number of frames to extract.
int mFrames; int mFrames;
...@@ -73,12 +77,15 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -73,12 +77,15 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
// 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;
// A metadata retriever, used to decode the video, and extract a thumbnail frame.
private MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();
// Keeps track of errors during decoding. // Keeps track of errors during decoding.
private @DecodingResult int mDecodingResult; private @DecodingResult int mDecodingResult;
// The duration of the video.
private String mDuration;
// The ratio of the first frame of the video.
private float mRatio;
/** /**
* A DecodeVideoTask constructor. * A DecodeVideoTask constructor.
* @param callback The callback to use to communicate back the results. * @param callback The callback to use to communicate back the results.
...@@ -86,15 +93,17 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -86,15 +93,17 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
* @param uri The URI of the video to decode. * @param uri The URI of the video to decode.
* @param size The desired width and height (in pixels) of the returned thumbnail from the * @param size The desired width and height (in pixels) of the returned thumbnail from the
* video. * video.
* @param fullWidth Whether this is a video thumbnail that takes up the full screen width.
* @param frames The number of frames to extract. * @param frames The number of frames to extract.
* @param intervalMs The interval between frames (in milliseconds). * @param intervalMs The interval between frames (in milliseconds).
*/ */
public DecodeVideoTask(VideoDecodingCallback callback, ContentResolver contentResolver, Uri uri, public DecodeVideoTask(VideoDecodingCallback callback, ContentResolver contentResolver, Uri uri,
int size, int frames, long intervalMs) { int size, boolean fullWidth, int frames, long intervalMs) {
mCallback = callback; mCallback = callback;
mContentResolver = contentResolver; mContentResolver = contentResolver;
mUri = uri; mUri = uri;
mSize = size; mSize = size;
mFullWidth = fullWidth;
mFrames = frames; mFrames = frames;
mIntervalMs = intervalMs; mIntervalMs = intervalMs;
} }
...@@ -122,10 +131,10 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -122,10 +131,10 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
/** /**
* Decodes a video and extracts metadata and a thumbnail. Called on a non-UI thread * 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. * @return A list of bitmaps (video thumbnails).
*/ */
@Override @Override
protected Pair<List<Bitmap>, String> doInBackground() { protected List<Bitmap> doInBackground() {
assert !ThreadUtils.runningOnUiThread(); assert !ThreadUtils.runningOnUiThread();
if (isCancelled()) return null; if (isCancelled()) return null;
...@@ -133,9 +142,10 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -133,9 +142,10 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
AssetFileDescriptor afd = null; AssetFileDescriptor afd = null;
try { try {
afd = mContentResolver.openAssetFileDescriptor(mUri, "r"); afd = mContentResolver.openAssetFileDescriptor(mUri, "r");
mRetriever.setDataSource(afd.getFileDescriptor()); MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(afd.getFileDescriptor());
String duration = String duration =
mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if (duration != null) { if (duration != null) {
// Adjust to a shorter video, if the frame requests exceed the length of the video. // Adjust to a shorter video, if the frame requests exceed the length of the video.
long durationMs = Long.parseLong(duration); long durationMs = Long.parseLong(duration);
...@@ -144,10 +154,11 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -144,10 +154,11 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
} }
duration = formatDuration(duration); duration = formatDuration(duration);
} }
List<Bitmap> bitmaps = BitmapUtils.decodeVideoFromFileDescriptor( Pair<List<Bitmap>, Float> bitmaps = BitmapUtils.decodeVideoFromFileDescriptor(
mRetriever, afd.getFileDescriptor(), mSize, mFrames, mIntervalMs); retriever, afd.getFileDescriptor(), mSize, mFrames, mFullWidth, mIntervalMs);
mDuration = duration;
return new Pair<List<Bitmap>, String>(bitmaps, duration); mRatio = bitmaps.second;
return bitmaps.first;
} catch (FileNotFoundException exception) { } catch (FileNotFoundException exception) {
mDecodingResult = DecodingResult.FILE_ERROR; mDecodingResult = DecodingResult.FILE_ERROR;
return null; return null;
...@@ -170,16 +181,17 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> { ...@@ -170,16 +181,17 @@ class DecodeVideoTask extends AsyncTask<Pair<List<Bitmap>, String>> {
* @param results A pair of bitmap (video thumbnail) and the duration of the video. * @param results A pair of bitmap (video thumbnail) and the duration of the video.
*/ */
@Override @Override
protected void onPostExecute(Pair<List<Bitmap>, String> results) { protected void onPostExecute(List<Bitmap> results) {
if (isCancelled()) { if (isCancelled()) {
return; return;
} }
if (results == null) { if (results == null) {
mCallback.videoDecodedCallback(mUri, null, "", mDecodingResult); mCallback.videoDecodedCallback(mUri, null, "", mFullWidth, mDecodingResult, 1.0f);
return; return;
} }
mCallback.videoDecodedCallback(mUri, results.first, results.second, mDecodingResult); mCallback.videoDecodedCallback(
mUri, results, mDuration, mFullWidth, mDecodingResult, mRatio);
} }
} }
...@@ -12,6 +12,7 @@ import android.os.IBinder; ...@@ -12,6 +12,7 @@ import android.os.IBinder;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Pair;
import org.chromium.base.CommandLine; import org.chromium.base.CommandLine;
import org.chromium.base.Log; import org.chromium.base.Log;
...@@ -36,7 +37,9 @@ public class DecoderService extends Service { ...@@ -36,7 +37,9 @@ public class DecoderService extends Service {
static final String KEY_FILE_DESCRIPTOR = "file_descriptor"; static final String KEY_FILE_DESCRIPTOR = "file_descriptor";
static final String KEY_FILE_PATH = "file_path"; static final String KEY_FILE_PATH = "file_path";
static final String KEY_IMAGE_BITMAP = "image_bitmap"; static final String KEY_IMAGE_BITMAP = "image_bitmap";
static final String KEY_SIZE = "size"; static final String KEY_WIDTH = "width";
static final String KEY_RATIO = "ratio";
static final String KEY_FULL_WIDTH = "full_width";
static final String KEY_SUCCESS = "success"; static final String KEY_SUCCESS = "success";
static final String KEY_DECODE_TIME = "decode_time"; static final String KEY_DECODE_TIME = "decode_time";
...@@ -79,11 +82,13 @@ public class DecoderService extends Service { ...@@ -79,11 +82,13 @@ public class DecoderService extends Service {
public void decodeImage(Bundle payload, IDecoderServiceCallback callback) { public void decodeImage(Bundle payload, IDecoderServiceCallback callback) {
Bundle bundle = null; Bundle bundle = null;
String filePath = ""; String filePath = "";
int size = 0; int width = 0;
boolean fullWidth = false;
try { try {
filePath = payload.getString(KEY_FILE_PATH); filePath = payload.getString(KEY_FILE_PATH);
ParcelFileDescriptor pfd = payload.getParcelable(KEY_FILE_DESCRIPTOR); ParcelFileDescriptor pfd = payload.getParcelable(KEY_FILE_DESCRIPTOR);
size = payload.getInt(KEY_SIZE); width = payload.getInt(KEY_WIDTH);
fullWidth = payload.getBoolean(KEY_FULL_WIDTH);
// Setup a minimum viable response to parent process. Will be fleshed out // Setup a minimum viable response to parent process. Will be fleshed out
// further below. // further below.
...@@ -92,7 +97,7 @@ public class DecoderService extends Service { ...@@ -92,7 +97,7 @@ public class DecoderService extends Service {
bundle.putBoolean(KEY_SUCCESS, false); bundle.putBoolean(KEY_SUCCESS, false);
if (!mNativeLibraryAndSandboxInitialized) { if (!mNativeLibraryAndSandboxInitialized) {
Log.e(TAG, "Decode failed %s (size: %d): no sandbox", filePath, size); Log.e(TAG, "Decode failed %s (width: %d): no sandbox", filePath, width);
sendReply(callback, bundle); // Sends SUCCESS == false; sendReply(callback, bundle); // Sends SUCCESS == false;
return; return;
} }
...@@ -100,17 +105,19 @@ public class DecoderService extends Service { ...@@ -100,17 +105,19 @@ public class DecoderService extends Service {
FileDescriptor fd = pfd.getFileDescriptor(); FileDescriptor fd = pfd.getFileDescriptor();
long begin = SystemClock.elapsedRealtime(); long begin = SystemClock.elapsedRealtime();
Bitmap bitmap = BitmapUtils.decodeBitmapFromFileDescriptor(fd, size); Pair<Bitmap, Float> decodedBitmap =
BitmapUtils.decodeBitmapFromFileDescriptor(fd, width, fullWidth);
long decodeTime = SystemClock.elapsedRealtime() - begin; long decodeTime = SystemClock.elapsedRealtime() - begin;
try { try {
pfd.close(); pfd.close();
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Closing failed " + filePath + " (size: " + size + ") " + e); Log.e(TAG, "Closing failed " + filePath + " (width: " + width + ") " + e);
} }
Bitmap bitmap = decodedBitmap.first;
if (bitmap == null) { if (bitmap == null) {
Log.e(TAG, "Decode failed " + filePath + " (size: " + size + ")"); Log.e(TAG, "Decode failed " + filePath + " (width: " + width + ")");
sendReply(callback, bundle); // Sends SUCCESS == false; sendReply(callback, bundle); // Sends SUCCESS == false;
return; return;
} }
...@@ -121,8 +128,10 @@ public class DecoderService extends Service { ...@@ -121,8 +128,10 @@ public class DecoderService extends Service {
// descriptor. In the receiving process it will just leave the bitmap on // descriptor. In the receiving process it will just leave the bitmap on
// ashmem since it's immutable and carry on. // ashmem since it's immutable and carry on.
bundle.putParcelable(KEY_IMAGE_BITMAP, bitmap); bundle.putParcelable(KEY_IMAGE_BITMAP, bitmap);
bundle.putFloat(KEY_RATIO, decodedBitmap.second);
bundle.putBoolean(KEY_SUCCESS, true); bundle.putBoolean(KEY_SUCCESS, true);
bundle.putLong(KEY_DECODE_TIME, decodeTime); bundle.putLong(KEY_DECODE_TIME, decodeTime);
bundle.putBoolean(KEY_FULL_WIDTH, payload.getBoolean(KEY_FULL_WIDTH));
sendReply(callback, bundle); sendReply(callback, bundle);
bitmap.recycle(); bitmap.recycle();
} catch (Exception e) { } catch (Exception e) {
...@@ -130,7 +139,7 @@ public class DecoderService extends Service { ...@@ -130,7 +139,7 @@ public class DecoderService extends Service {
// decoding a photo, it is better UX to eat the exception instead of showing // decoding a photo, it is better UX to eat the exception instead of showing
// a crash dialog and discarding other requests that have already been sent. // a crash dialog and discarding other requests that have already been sent.
Log.e(TAG, Log.e(TAG,
"Unexpected error during decoding " + filePath + " (size: " + size + ") " "Unexpected error during decoding " + filePath + " (width: " + width + ") "
+ e); + e);
if (bundle != null) sendReply(callback, bundle); if (bundle != null) sendReply(callback, bundle);
......
...@@ -43,8 +43,8 @@ public class PickerBitmapViewHolder ...@@ -43,8 +43,8 @@ public class PickerBitmapViewHolder
// DecoderServiceHost.ImageDecodedCallback // DecoderServiceHost.ImageDecodedCallback
@Override @Override
public void imagesDecodedCallback( public void imagesDecodedCallback(String filePath, boolean isVideo, boolean fullWidth,
String filePath, boolean isVideo, List<Bitmap> bitmaps, String videoDuration) { List<Bitmap> bitmaps, String videoDuration, float ratio) {
if (bitmaps == null || bitmaps.size() == 0) return; if (bitmaps == null || bitmaps.size() == 0) return;
if (!isVideo) { if (!isVideo) {
...@@ -52,20 +52,27 @@ public class PickerBitmapViewHolder ...@@ -52,20 +52,27 @@ public class PickerBitmapViewHolder
if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) return; if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) return;
} }
PickerCategoryView.Thumbnail cachedThumbnail = PickerCategoryView.Thumbnail cachedThumbnail = fullWidth
mCategoryView.getHighResThumbnails().get(filePath); ? mCategoryView.getFullScreenBitmaps().get(filePath)
: mCategoryView.getHighResThumbnails().get(filePath);
if (cachedThumbnail == null if (cachedThumbnail == null
|| (cachedThumbnail.bitmaps != null || (cachedThumbnail.bitmaps != null
&& cachedThumbnail.bitmaps.size() < bitmaps.size())) { && cachedThumbnail.bitmaps.size() < bitmaps.size())) {
mCategoryView.getHighResThumbnails().put( if (fullWidth) {
filePath, new PickerCategoryView.Thumbnail(bitmaps, videoDuration)); mCategoryView.getFullScreenBitmaps().put(filePath,
new PickerCategoryView.Thumbnail(bitmaps, videoDuration, fullWidth, ratio));
} else {
mCategoryView.getHighResThumbnails().put(filePath,
new PickerCategoryView.Thumbnail(bitmaps, videoDuration, fullWidth, ratio));
}
} }
if (mCategoryView.getLowResThumbnails().get(filePath) == null) { if (mCategoryView.getLowResThumbnails().get(filePath) == null) {
Resources resources = mItemView.getContext().getResources(); Resources resources = mItemView.getContext().getResources();
new BitmapScalerTask(mCategoryView.getLowResThumbnails(), bitmaps.get(0), filePath, new BitmapScalerTask(mCategoryView.getLowResThumbnails(), bitmaps.get(0), filePath,
videoDuration, videoDuration,
resources.getDimensionPixelSize(R.dimen.photo_picker_grainy_thumbnail_size)) resources.getDimensionPixelSize(R.dimen.photo_picker_grainy_thumbnail_size),
ratio)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
...@@ -73,7 +80,7 @@ public class PickerBitmapViewHolder ...@@ -73,7 +80,7 @@ public class PickerBitmapViewHolder
return; return;
} }
if (mItemView.setThumbnailBitmap(bitmaps, videoDuration)) { if (mItemView.setThumbnailBitmap(bitmaps, videoDuration, ratio)) {
mItemView.fadeInThumbnail(); mItemView.fadeInThumbnail();
} }
} }
...@@ -93,37 +100,48 @@ public class PickerBitmapViewHolder ...@@ -93,37 +100,48 @@ 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, null, false); mItemView.initialize(mBitmapDetails, null, null, false, -1);
return PickerAdapter.DecodeActions.NO_ACTION; return PickerAdapter.DecodeActions.NO_ACTION;
} }
String filePath = mBitmapDetails.getUri().getPath(); String filePath = mBitmapDetails.getUri().getPath();
PickerCategoryView.Thumbnail original = mCategoryView.getHighResThumbnails().get(filePath); PickerCategoryView.Thumbnail original = mCategoryView.isInMagnifyingMode()
? mCategoryView.getFullScreenBitmaps().get(filePath)
: mCategoryView.getHighResThumbnails().get(filePath);
if (original != null) { if (original != null) {
mItemView.initialize(mBitmapDetails, original.bitmaps, original.videoDuration, false); mItemView.initialize(mBitmapDetails, original.bitmaps, original.videoDuration, false,
original.ratio);
return PickerAdapter.DecodeActions.FROM_CACHE; return PickerAdapter.DecodeActions.FROM_CACHE;
} }
int size = mCategoryView.getImageSize(); int width = mCategoryView.getImageWidth();
PickerCategoryView.Thumbnail payload = mCategoryView.getLowResThumbnails().get(filePath); PickerCategoryView.Thumbnail payload = null;
if (mCategoryView.isInMagnifyingMode()) {
payload = mCategoryView.getHighResThumbnails().get(filePath);
}
if (payload == null) {
payload = mCategoryView.getLowResThumbnails().get(filePath);
}
if (payload != null) { if (payload != null) {
Bitmap placeholder = payload.bitmaps.get(0); Bitmap placeholder = payload.bitmaps.get(0);
// 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, width, false);
long scaleTime = SystemClock.elapsedRealtime() - begin; long scaleTime = SystemClock.elapsedRealtime() - begin;
RecordHistogram.recordTimesHistogram( RecordHistogram.recordTimesHistogram(
"Android.PhotoPicker.UpscaleLowResBitmap", scaleTime); "Android.PhotoPicker.UpscaleLowResBitmap", scaleTime);
List<Bitmap> bitmaps = new ArrayList<>(1); List<Bitmap> bitmaps = new ArrayList<>(1);
bitmaps.add(placeholder); bitmaps.add(placeholder);
mItemView.initialize(mBitmapDetails, bitmaps, payload.videoDuration, true); mItemView.initialize(
mBitmapDetails, bitmaps, payload.videoDuration, true, payload.ratio);
} else { } else {
mItemView.initialize(mBitmapDetails, null, null, true); mItemView.initialize(mBitmapDetails, null, null, true, -1);
} }
mCategoryView.getDecoderServiceHost().decodeImage( mCategoryView.getDecoderServiceHost().decodeImage(mBitmapDetails.getUri(),
mBitmapDetails.getUri(), mBitmapDetails.type(), size, this); mBitmapDetails.type(), width, mCategoryView.isInMagnifyingMode(), this);
return PickerAdapter.DecodeActions.DECODE; return PickerAdapter.DecodeActions.DECODE;
} }
...@@ -134,8 +152,9 @@ public class PickerBitmapViewHolder ...@@ -134,8 +152,9 @@ public class PickerBitmapViewHolder
public String getFilePath() { public String getFilePath() {
if (mBitmapDetails == null if (mBitmapDetails == null
|| (mBitmapDetails.type() != PickerBitmap.TileTypes.PICTURE || (mBitmapDetails.type() != PickerBitmap.TileTypes.PICTURE
&& mBitmapDetails.type() != PickerBitmap.TileTypes.VIDEO)) && mBitmapDetails.type() != PickerBitmap.TileTypes.VIDEO)) {
return null; return null;
}
return mBitmapDetails.getUri().getPath(); return mBitmapDetails.getUri().getPath();
} }
} }
...@@ -22,6 +22,7 @@ import android.view.View; ...@@ -22,6 +22,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.MediaController; import android.widget.MediaController;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.VideoView; import android.widget.VideoView;
...@@ -33,6 +34,7 @@ import org.chromium.base.metrics.RecordHistogram; ...@@ -33,6 +34,7 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.util.ConversionUtils; import org.chromium.chrome.browser.util.ConversionUtils;
import org.chromium.chrome.browser.widget.selection.SelectableListLayout; import org.chromium.chrome.browser.widget.selection.SelectableListLayout;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate; import org.chromium.chrome.browser.widget.selection.SelectionDelegate;
...@@ -65,11 +67,19 @@ public class PickerCategoryView extends RelativeLayout ...@@ -65,11 +67,19 @@ public class PickerCategoryView extends RelativeLayout
*/ */
public static class Thumbnail { public static class Thumbnail {
public List<Bitmap> bitmaps; public List<Bitmap> bitmaps;
public Boolean fullWidth;
public String videoDuration; public String videoDuration;
Thumbnail(List<Bitmap> bitmaps, String videoDuration) { // The calculated ratio of the originals for the bitmaps above, were they to be shown
// un-cropped. NOTE: The |bitmaps| above may already have been cropped and as such might
// have a different ratio.
public float ratio;
Thumbnail(List<Bitmap> bitmaps, String videoDuration, Boolean fullWidth, float ratio) {
this.bitmaps = bitmaps; this.bitmaps = bitmaps;
this.videoDuration = videoDuration; this.videoDuration = videoDuration;
this.fullWidth = fullWidth;
this.ratio = ratio;
} }
} }
...@@ -117,12 +127,24 @@ public class PickerCategoryView extends RelativeLayout ...@@ -117,12 +127,24 @@ public class PickerCategoryView extends RelativeLayout
// A high-resolution cache for thumbnails, lazily created. // A high-resolution cache for thumbnails, lazily created.
private DiscardableReference<LruCache<String, Thumbnail>> mHighResThumbnails; private DiscardableReference<LruCache<String, Thumbnail>> mHighResThumbnails;
// A cache for full-screen versions of images, lazily created.
private DiscardableReference<LruCache<String, Thumbnail>> mFullScreenBitmaps;
// The size of the low-res cache. // The size of the low-res cache.
private int mCacheSizeLarge; private int mCacheSizeLarge;
// The size of the high-res cache. // The size of the high-res cache.
private int mCacheSizeSmall; private int mCacheSizeSmall;
// The size of the full-screen cache.
private int mCacheSizeFullScreen;
// Whether we are in magnifying mode (one image per column).
private boolean mMagnifyingMode;
// Whether we are in the middle of animating between magnifying modes.
private boolean mZoomSwitchingInEffect;
/** /**
* The number of columns to show. Note: mColumns and mPadding (see below) should both be even * The number of columns to show. Note: mColumns and mPadding (see below) should both be even
* numbers or both odd, not a mix (the column padding will not be of uniform thickness if they * numbers or both odd, not a mix (the column padding will not be of uniform thickness if they
...@@ -133,8 +155,14 @@ public class PickerCategoryView extends RelativeLayout ...@@ -133,8 +155,14 @@ public class PickerCategoryView extends RelativeLayout
// The padding between columns. See also comment for mColumns. // The padding between columns. See also comment for mColumns.
private int mPadding; private int mPadding;
// The size of the bitmaps (equal length for width and height). // The width of the bitmaps.
private int mImageSize; private int mImageWidth;
// The height of the bitmaps.
private int mImageHeight;
// The height of the special tiles.
private int mSpecialTileHeight;
// 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;
...@@ -191,6 +219,12 @@ public class PickerCategoryView extends RelativeLayout ...@@ -191,6 +219,12 @@ public class PickerCategoryView extends RelativeLayout
doneButton.setOnClickListener(this); doneButton.setOnClickListener(this);
mVideoView = findViewById(R.id.video_player); mVideoView = findViewById(R.id.video_player);
if (ChromeFeatureList.isEnabled(ChromeFeatureList.PHOTO_PICKER_ZOOM)) {
ImageView zoom = findViewById(R.id.zoom);
zoom.setVisibility(View.VISIBLE);
zoom.setOnClickListener(this);
}
calculateGridMetrics(); calculateGridMetrics();
mLayoutManager = new GridLayoutManager(mActivity, mColumns); mLayoutManager = new GridLayoutManager(mActivity, mColumns);
...@@ -201,7 +235,13 @@ public class PickerCategoryView extends RelativeLayout ...@@ -201,7 +235,13 @@ public class PickerCategoryView extends RelativeLayout
mRecyclerView.setRecyclerListener(this); mRecyclerView.setRecyclerListener(this);
final long maxMemory = ConversionUtils.bytesToKilobytes(Runtime.getRuntime().maxMemory()); final long maxMemory = ConversionUtils.bytesToKilobytes(Runtime.getRuntime().maxMemory());
mCacheSizeLarge = (int) (maxMemory / 2); // 1/2 of the available memory. if (ChromeFeatureList.isEnabled(ChromeFeatureList.PHOTO_PICKER_ZOOM)) {
mCacheSizeFullScreen = (int) (maxMemory / 4); // 1/4 of the available memory.
mCacheSizeLarge = (int) (maxMemory / 4); // 1/4 of the available memory.
} else {
mCacheSizeFullScreen = 0;
mCacheSizeLarge = (int) (maxMemory / 2); // 1/2 of the available memory.
}
mCacheSizeSmall = (int) (maxMemory / 8); // 1/8th of the available memory. mCacheSizeSmall = (int) (maxMemory / 8); // 1/8th of the available memory.
} }
...@@ -219,7 +259,10 @@ public class PickerCategoryView extends RelativeLayout ...@@ -219,7 +259,10 @@ public class PickerCategoryView extends RelativeLayout
// enumerated (when mPickerBitmaps is null, causing: https://crbug.com/947657). There's no // enumerated (when mPickerBitmaps is null, causing: https://crbug.com/947657). There's no
// need to call notifyDataSetChanged in that case because it will be called once the photo // need to call notifyDataSetChanged in that case because it will be called once the photo
// list becomes ready. // list becomes ready.
if (mPickerBitmaps != null) mPickerAdapter.notifyDataSetChanged(); if (mPickerBitmaps != null) {
mPickerAdapter.notifyDataSetChanged();
mRecyclerView.requestLayout();
}
} }
/** /**
...@@ -347,6 +390,8 @@ public class PickerCategoryView extends RelativeLayout ...@@ -347,6 +390,8 @@ public class PickerCategoryView extends RelativeLayout
int id = view.getId(); int id = view.getId();
if (id == R.id.done) { if (id == R.id.done) {
notifyPhotosSelected(); notifyPhotosSelected();
} else if (id == R.id.zoom) {
flipZoomMode();
} else if (id == R.id.close) { } else if (id == R.id.close) {
stopVideo(); stopVideo();
} else { } else {
...@@ -364,10 +409,39 @@ public class PickerCategoryView extends RelativeLayout ...@@ -364,10 +409,39 @@ public class PickerCategoryView extends RelativeLayout
} }
} }
private void flipZoomMode() {
mMagnifyingMode = !mMagnifyingMode;
ImageView zoom = findViewById(R.id.zoom);
if (mMagnifyingMode) {
zoom.setImageResource(R.drawable.zoom_out);
} else {
zoom.setImageResource(R.drawable.zoom_in);
}
calculateGridMetrics();
mLayoutManager.setSpanCount(mColumns);
mPickerAdapter.notifyDataSetChanged();
mRecyclerView.requestLayout();
}
// Simple accessors: // Simple accessors:
public int getImageSize() { public int getImageWidth() {
return mImageSize; return mImageWidth;
}
public int getImageHeight() {
return mImageHeight;
}
public int getSpecialTileHeight() {
return mSpecialTileHeight;
}
public boolean isInMagnifyingMode() {
return mMagnifyingMode;
} }
public SelectionDelegate<PickerBitmap> getSelectionDelegate() { public SelectionDelegate<PickerBitmap> getSelectionDelegate() {
...@@ -398,6 +472,14 @@ public class PickerCategoryView extends RelativeLayout ...@@ -398,6 +472,14 @@ public class PickerCategoryView extends RelativeLayout
return mHighResThumbnails.get(); return mHighResThumbnails.get();
} }
public LruCache<String, Thumbnail> getFullScreenBitmaps() {
if (mFullScreenBitmaps == null || mFullScreenBitmaps.get() == null) {
mFullScreenBitmaps = mActivity.getReferencePool().put(
new LruCache<String, Thumbnail>(mCacheSizeFullScreen));
}
return mFullScreenBitmaps.get();
}
public boolean isMultiSelectAllowed() { public boolean isMultiSelectAllowed() {
return mMultiSelectionAllowed; return mMultiSelectionAllowed;
} }
...@@ -426,12 +508,18 @@ public class PickerCategoryView extends RelativeLayout ...@@ -426,12 +508,18 @@ public class PickerCategoryView extends RelativeLayout
int width = displayMetrics.widthPixels; int width = displayMetrics.widthPixels;
int minSize = int minSize =
mActivity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_min_size); mActivity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_min_size);
mPadding = mActivity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_gap); mPadding = mMagnifyingMode
mColumns = Math.max(1, (width - mPadding) / (minSize + mPadding)); ? 0
mImageSize = (width - mPadding * (mColumns + 1)) / (mColumns); : mActivity.getResources().getDimensionPixelSize(R.dimen.photo_picker_tile_gap);
mColumns = mMagnifyingMode ? 1 : Math.max(1, (width - mPadding) / (minSize + mPadding));
mImageWidth = (width - mPadding * (mColumns + 1)) / (mColumns);
mImageHeight = mMagnifyingMode
? displayMetrics.heightPixels - findViewById(R.id.action_bar_bg).getHeight()
: mImageWidth;
if (!mMagnifyingMode) mSpecialTileHeight = mImageWidth;
// Make sure columns and padding are either both even or both odd. // Make sure columns and padding are either both even or both odd.
if (((mColumns % 2) == 0) != ((mPadding % 2) == 0)) { if (!mMagnifyingMode && ((mColumns % 2) == 0) != ((mPadding % 2) == 0)) {
mPadding++; mPadding++;
} }
} }
...@@ -495,6 +583,11 @@ public class PickerCategoryView extends RelativeLayout ...@@ -495,6 +583,11 @@ public class PickerCategoryView extends RelativeLayout
@Override @Override
public void getItemOffsets( public void getItemOffsets(
Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mMagnifyingMode) {
outRect.set(0, 0, 0, mSpacing);
return;
}
int left = 0, right = 0, top = 0, bottom = 0; int left = 0, right = 0, top = 0, bottom = 0;
int position = parent.getChildAdapterPosition(view); int position = parent.getChildAdapterPosition(view);
......
...@@ -18,7 +18,7 @@ import org.junit.runner.RunWith; ...@@ -18,7 +18,7 @@ import org.junit.runner.RunWith;
import org.chromium.base.test.util.CallbackHelper; import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags; import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisableIf; import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.UrlUtils; import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.ChromeSwitches;
...@@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; ...@@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit;
* Tests for the DecoderServiceHost. * Tests for the DecoderServiceHost.
*/ */
@RunWith(ChromeJUnit4ClassRunner.class) @RunWith(ChromeJUnit4ClassRunner.class)
@MinAndroidSdkLevel(Build.VERSION_CODES.N)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCallback, public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCallback,
DecoderServiceHost.ImagesDecodedCallback { DecoderServiceHost.ImagesDecodedCallback {
...@@ -53,8 +54,10 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa ...@@ -53,8 +54,10 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
private String mLastDecodedPath; private String mLastDecodedPath;
private boolean mLastIsVideo; private boolean mLastIsVideo;
private Bitmap mLastInitialFrame;
private int mLastFrameCount; private int mLastFrameCount;
private String mLastVideoDuration; private String mLastVideoDuration;
private float mLastRatio;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
...@@ -74,12 +77,14 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa ...@@ -74,12 +77,14 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
// DecoderServiceHost.ImagesDecodedCallback: // DecoderServiceHost.ImagesDecodedCallback:
@Override @Override
public void imagesDecodedCallback( public void imagesDecodedCallback(String filePath, boolean isVideo, boolean isZoomedIn,
String filePath, boolean isVideo, List<Bitmap> bitmaps, String videoDuration) { List<Bitmap> bitmaps, String videoDuration, float ratio) {
mLastDecodedPath = filePath; mLastDecodedPath = filePath;
mLastIsVideo = isVideo; mLastIsVideo = isVideo;
mLastFrameCount = bitmaps != null ? bitmaps.size() : -1; mLastFrameCount = bitmaps != null ? bitmaps.size() : -1;
mLastInitialFrame = bitmaps != null ? bitmaps.get(0) : null;
mLastVideoDuration = videoDuration; mLastVideoDuration = videoDuration;
mLastRatio = ratio;
onDecodedCallback.notifyCalled(); onDecodedCallback.notifyCalled();
} }
...@@ -96,7 +101,6 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa ...@@ -96,7 +101,6 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
} }
@Test @Test
@DisableIf.Build(sdk_is_less_than = Build.VERSION_CODES.N)
@LargeTest @LargeTest
public void testDecodingOrder() throws Throwable { public void testDecodingOrder() throws Throwable {
DecoderServiceHost host = new DecoderServiceHost(this, mContext); DecoderServiceHost host = new DecoderServiceHost(this, mContext);
...@@ -111,9 +115,12 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa ...@@ -111,9 +115,12 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
File file2 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName2)); File file2 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName2));
File file3 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName3)); File file3 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName3));
host.decodeImage(Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 10, this); host.decodeImage(
host.decodeImage(Uri.fromFile(file2), PickerBitmap.TileTypes.VIDEO, 10, this); Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 10, /*fullWidth=*/false, this);
host.decodeImage(Uri.fromFile(file3), PickerBitmap.TileTypes.PICTURE, 10, this); host.decodeImage(
Uri.fromFile(file2), PickerBitmap.TileTypes.VIDEO, 10, /*fullWidth=*/false, this);
host.decodeImage(
Uri.fromFile(file3), PickerBitmap.TileTypes.PICTURE, 10, /*fullWidth=*/false, this);
// First decoding result should be first frame only of video 1. // First decoding result should be first frame only of video 1.
waitForThumbnailDecode(); waitForThumbnailDecode();
...@@ -153,4 +160,84 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa ...@@ -153,4 +160,84 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
host.unbind(mContext); host.unbind(mContext);
} }
@Test
@LargeTest
public void testDecodingSizes() throws Throwable {
DecoderServiceHost host = new DecoderServiceHost(this, mContext);
host.bind(mContext);
waitForDecoder();
String fileName1 = "noogler.mp4"; // 1920 x 1080 video.
String fileName2 = "blue100x100.jpg";
String filePath = "chrome/test/data/android/photo_picker/";
File file1 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName1));
File file2 = new File(UrlUtils.getIsolatedTestFilePath(filePath + fileName2));
// Thumbnail photo. 100 x 100 -> 10 x 10.
host.decodeImage(
Uri.fromFile(file2), PickerBitmap.TileTypes.PICTURE, 10, /*fullWidth=*/false, this);
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName2));
Assert.assertEquals(false, mLastIsVideo);
Assert.assertEquals(null, mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(1.0f, mLastRatio, 0.1f);
Assert.assertEquals(10, mLastInitialFrame.getWidth());
Assert.assertEquals(10, mLastInitialFrame.getHeight());
// Full-width photo. 100 x 100 -> 200 x 200.
host.decodeImage(
Uri.fromFile(file2), PickerBitmap.TileTypes.PICTURE, 200, /*fullWidth=*/true, this);
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName2));
Assert.assertEquals(false, mLastIsVideo);
Assert.assertEquals(null, mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(1.0f, mLastRatio, 0.1f);
Assert.assertEquals(200, mLastInitialFrame.getWidth());
Assert.assertEquals(200, mLastInitialFrame.getHeight());
// Thumbnail video. 1920 x 1080 -> 10 x 10.
host.decodeImage(
Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 10, /*fullWidth=*/false, this);
waitForThumbnailDecode(); // Initial frame.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(10, mLastInitialFrame.getWidth());
Assert.assertEquals(10, mLastInitialFrame.getHeight());
waitForThumbnailDecode(); // Rest of frames.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(10, mLastInitialFrame.getWidth());
Assert.assertEquals(10, mLastInitialFrame.getHeight());
// Full-width video. 1920 x 1080 -> 2000 x 1125.
host.decodeImage(
Uri.fromFile(file1), PickerBitmap.TileTypes.VIDEO, 2000, /*fullWidth=*/true, this);
waitForThumbnailDecode(); // Initial frame.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(2000, mLastInitialFrame.getWidth());
Assert.assertEquals(1125, mLastInitialFrame.getHeight());
waitForThumbnailDecode(); // Rest of frames.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(2000, mLastInitialFrame.getWidth());
Assert.assertEquals(1125, mLastInitialFrame.getHeight());
host.unbind(mContext);
}
} }
...@@ -102,7 +102,7 @@ public class DecoderServiceTest { ...@@ -102,7 +102,7 @@ public class DecoderServiceTest {
}); });
} }
private void decode(String filePath, FileDescriptor fd, int size, private void decode(String filePath, FileDescriptor fd, int width,
final DecoderServiceCallback callback) throws Exception { final DecoderServiceCallback callback) throws Exception {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(DecoderService.KEY_FILE_PATH, filePath); bundle.putString(DecoderService.KEY_FILE_PATH, filePath);
...@@ -112,7 +112,7 @@ public class DecoderServiceTest { ...@@ -112,7 +112,7 @@ public class DecoderServiceTest {
Assert.assertTrue(pfd != null); Assert.assertTrue(pfd != null);
} }
bundle.putParcelable(DecoderService.KEY_FILE_DESCRIPTOR, pfd); bundle.putParcelable(DecoderService.KEY_FILE_DESCRIPTOR, pfd);
bundle.putInt(DecoderService.KEY_SIZE, size); bundle.putInt(DecoderService.KEY_WIDTH, width);
mIRemoteService.decodeImage(bundle, callback); mIRemoteService.decodeImage(bundle, callback);
CriteriaHelper.pollUiThread(new Criteria() { CriteriaHelper.pollUiThread(new Criteria() {
......
...@@ -21,7 +21,7 @@ import org.junit.runner.RunWith; ...@@ -21,7 +21,7 @@ import org.junit.runner.RunWith;
import org.chromium.base.test.util.CallbackHelper; import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags; import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisableIf; import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.ChromeSwitches;
...@@ -44,6 +44,7 @@ import java.util.concurrent.TimeUnit; ...@@ -44,6 +44,7 @@ import java.util.concurrent.TimeUnit;
* Tests for the PhotoPickerDialog class. * Tests for the PhotoPickerDialog class.
*/ */
@RunWith(ChromeJUnit4ClassRunner.class) @RunWith(ChromeJUnit4ClassRunner.class)
@MinAndroidSdkLevel(Build.VERSION_CODES.LOLLIPOP) // See crbug.com/888931 for details.
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE}) @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObserver<PickerBitmap>, public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObserver<PickerBitmap>,
DecoderServiceHost.ServiceReadyCallback, DecoderServiceHost.ServiceReadyCallback,
...@@ -213,7 +214,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -213,7 +214,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
} }
@Test @Test
@DisableIf.Build(sdk_is_less_than = Build.VERSION_CODES.LOLLIPOP, message = "crbug.com/888931")
@LargeTest @LargeTest
public void testNoSelection() throws Throwable { public void testNoSelection() throws Throwable {
createDialog(false, Arrays.asList("image/*")); // Multi-select = false. createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
...@@ -231,7 +231,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -231,7 +231,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
} }
@Test @Test
@DisableIf.Build(sdk_is_less_than = Build.VERSION_CODES.LOLLIPOP, message = "crbug.com/888931")
@LargeTest @LargeTest
public void testSingleSelectionPhoto() throws Throwable { public void testSingleSelectionPhoto() throws Throwable {
createDialog(false, Arrays.asList("image/*")); // Multi-select = false. createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
...@@ -261,7 +260,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -261,7 +260,6 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
} }
@Test @Test
@DisableIf.Build(sdk_is_less_than = Build.VERSION_CODES.LOLLIPOP, message = "crbug.com/888931")
@LargeTest @LargeTest
public void testMultiSelectionPhoto() throws Throwable { public void testMultiSelectionPhoto() throws Throwable {
createDialog(true, Arrays.asList("image/*")); // Multi-select = true. createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
......
...@@ -158,6 +158,7 @@ const base::Feature* kFeaturesExposedToJava[] = { ...@@ -158,6 +158,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&kOverlayNewLayout, &kOverlayNewLayout,
&kPayWithGoogleV1, &kPayWithGoogleV1,
&kPhotoPickerVideoSupport, &kPhotoPickerVideoSupport,
&kPhotoPickerZoom,
&kReachedCodeProfiler, &kReachedCodeProfiler,
&kReaderModeInCCT, &kReaderModeInCCT,
&kReorderBookmarks, &kReorderBookmarks,
...@@ -491,6 +492,9 @@ const base::Feature kPayWithGoogleV1{"PayWithGoogleV1", ...@@ -491,6 +492,9 @@ const base::Feature kPayWithGoogleV1{"PayWithGoogleV1",
const base::Feature kPhotoPickerVideoSupport{"PhotoPickerVideoSupport", const base::Feature kPhotoPickerVideoSupport{"PhotoPickerVideoSupport",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kPhotoPickerZoom{"PhotoPickerZoom",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::Feature kReachedCodeProfiler{"ReachedCodeProfiler", const base::Feature kReachedCodeProfiler{"ReachedCodeProfiler",
base::FEATURE_DISABLED_BY_DEFAULT}; base::FEATURE_DISABLED_BY_DEFAULT};
......
...@@ -91,6 +91,7 @@ extern const base::Feature kOmniboxSpareRenderer; ...@@ -91,6 +91,7 @@ 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 kPhotoPickerVideoSupport;
extern const base::Feature kPhotoPickerZoom;
extern const base::Feature kReachedCodeProfiler; extern const base::Feature kReachedCodeProfiler;
extern const base::Feature kReorderBookmarks; extern const base::Feature kReorderBookmarks;
extern const base::Feature kReaderModeInCCT; extern const base::Feature kReaderModeInCCT;
......
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