Commit 5fc176f9 authored by Finnur Thorarinsson's avatar Finnur Thorarinsson Committed by Commit Bot

[Android] Photo Picker: Switch to overlaid video controls.

This removes the MediaController knob panel below the video
player, and replaces it with controls that are overlaid on
top of the video (and fade away after a small amount of time).

Also introduces a Mute button.

Bug: 895776, 656015
Change-Id: If89fa712c70b62eeda550be87478562c1a118914
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1992436Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Commit-Queue: Finnur Thorarinsson <finnur@chromium.org>
Cr-Commit-Position: refs/heads/master@{#732606}
parent 01403ca7
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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/white_alpha_70"
android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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_light"
android:pathData="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c0.03-0.2 0.05 -0.41 0.05 -0.63zm2.5 0c0 0.94-0.2 1.82-0.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89 0.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-0.67 0.52 -1.42 0.93 -2.25 1.18v2.06c1.38-0.31 2.63-0.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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_light"
android:pathData="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-0.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89 0.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-0.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="90"
android:startColor="@android:color/black"
android:endColor="@android:color/transparent" />
</shape>
......@@ -57,7 +57,7 @@
android:layout_marginTop="3dp"
android:layout_marginEnd="6dp"
android:contentDescription="@string/accessibility_play_video"
app:srcCompat="@drawable/ic_play_circle_24dp_white" />
app:srcCompat="@drawable/ic_play_circle_filled_white_24dp" />
</LinearLayout>
</FrameLayout>
......@@ -68,7 +68,7 @@
android:layout_gravity="center"
android:contentDescription="@string/accessibility_play_video"
android:visibility="gone"
app:srcCompat="@drawable/ic_play_circle_24dp_white" />
app:srcCompat="@drawable/ic_play_circle_filled_white_24dp" />
<ImageView
android:id="@+id/selected"
......
......@@ -28,42 +28,5 @@
app:srcCompat="@drawable/zoom_in"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/playback_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_alpha_65"
android:clickable="true"
android:gravity="bottom"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/black"
android:gravity="end">
<ImageView
android:id="@+id/close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="4dp"
android:layout_marginTop="8dp"
android:tint="@android:color/white"
android:contentDescription="@string/close"
app:srcCompat="@drawable/ic_cancel_circle" />
<VideoView
android:id="@+id/video_player"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<FrameLayout
android:id="@+id/controls_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</RelativeLayout>
<include layout="@layout/video_player" />
</org.chromium.ui.widget.OptimizedFrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/playback_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:clickable="true"
android:visibility="gone">
<VideoView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<FrameLayout
android:id="@+id/video_overlay_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="0.1dp"
android:clickable="true"
android:layout_gravity="center">
<FrameLayout
android:id="@+id/video_controls_gradient"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_gravity="bottom|start"
android:background="@drawable/video_player_gradient"/>
<FrameLayout
android:id="@+id/video_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingBottom="15dp"
android:paddingStart="2dp"
android:paddingEnd="2dp">
<ImageView
android:id="@+id/video_player_play_button"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_play_circle_filled_white_24dp" />
<TextView
android:id="@+id/remaining_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="16dp"
android:paddingBottom="24dp"
style="@style/TextAppearance.WhiteBody" />
<ImageView
android:id="@+id/mute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="14dp"
android:paddingBottom="24dp"
app:srcCompat="@drawable/ic_volume_on_white_24dp" />
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
style="@style/PhotoPicker.SeekBar" />
</FrameLayout>
</FrameLayout>
</FrameLayout>
......@@ -716,6 +716,14 @@
<item name="android:windowExitAnimation">@null</item>
</style>
<!-- Photo Picker Dialog -->
<style name="PhotoPicker"></style>
<style name="PhotoPicker.SeekBar" parent="Widget.AppCompat.SeekBar">
<item name="android:progressBackgroundTint">@color/modern_grey_600</item>
<item name="android:progressTint">@color/default_primary_color</item>
<item name="android:thumbTint">@color/default_primary_color</item>
</style>
<!-- Contacts Dialog -->
<style name="SuggestionChipContacts" parent="SuggestionChip">
<item name="iconWidth">16dp</item>
......
......@@ -113,19 +113,19 @@ class DecodeVideoTask extends AsyncTask<List<Bitmap>> {
* @param durationMs The duration in milliseconds.
* @return The duration in human-readable form.
*/
private String formatDuration(String durationMs) {
public static String formatDuration(Long durationMs) {
if (durationMs == null) return null;
long duration = Long.parseLong(durationMs) / 1000;
long duration = 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);
return String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
return String.format(Locale.US, "%d:%02d", minutes, seconds);
}
}
......@@ -152,7 +152,7 @@ class DecodeVideoTask extends AsyncTask<List<Bitmap>> {
if (mFrames > 1 && mFrames * mIntervalMs > durationMs) {
mIntervalMs = durationMs / mFrames;
}
duration = formatDuration(duration);
duration = formatDuration(durationMs);
}
Pair<List<Bitmap>, Float> bitmaps = BitmapUtils.decodeVideoFromFileDescriptor(
retriever, afd.getFileDescriptor(), mSize, mFrames, mFullWidth, mIntervalMs);
......
......@@ -96,6 +96,17 @@ public class PhotoPickerDialog
setView(mCategoryView);
}
@Override
public void onBackPressed() {
// Pressing Back when a video is playing, should only end the video playback.
boolean videoWasStopped = mCategoryView.closeVideoPlayer();
if (videoWasStopped) {
return;
} else {
super.onBackPressed();
}
}
@Override
public void dismiss() {
if (!mListenerWrapper.externalIntentSelected() || mDoneWaitingForExternalIntent) {
......
......@@ -166,7 +166,7 @@ public class PickerBitmapView extends SelectableItemView<PickerBitmap> {
@Override
public final void onClick(View view) {
if (view == mPlayButton || view == mPlayButtonLarge) {
mCategoryView.playVideo(mBitmapDetails.getUri());
mCategoryView.startVideoPlaybackAsync(mBitmapDetails.getUri());
} else {
super.onClick(view);
}
......
......@@ -126,7 +126,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
// Second decoding result is first frame of video 2, because that's higher priority than the
......@@ -134,7 +134,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName2));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
// Third in line should be the jpg file.
......@@ -148,14 +148,14 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
// Remaining frames of video 2.
waitForThumbnailDecode();
Assert.assertTrue(mLastDecodedPath.contains(fileName2));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
host.unbind(mContext);
......@@ -204,7 +204,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode(); // Initial frame.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(10, mLastInitialFrame.getWidth());
......@@ -212,7 +212,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode(); // Rest of frames.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(10, mLastInitialFrame.getWidth());
......@@ -224,7 +224,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode(); // Initial frame.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(1, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(2000, mLastInitialFrame.getWidth());
......@@ -232,7 +232,7 @@ public class DecoderServiceHostTest implements DecoderServiceHost.ServiceReadyCa
waitForThumbnailDecode(); // Rest of frames.
Assert.assertTrue(mLastDecodedPath.contains(fileName1));
Assert.assertEquals(true, mLastIsVideo);
Assert.assertEquals("00:00", mLastVideoDuration);
Assert.assertEquals("0:00", mLastVideoDuration);
Assert.assertEquals(10, mLastFrameCount);
Assert.assertEquals(0.5625f, mLastRatio, 0.0001f);
Assert.assertEquals(2000, mLastInitialFrame.getWidth());
......
......@@ -21,7 +21,9 @@ import org.junit.runner.RunWith;
import org.chromium.base.test.util.CallbackHelper;
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.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
......@@ -34,6 +36,7 @@ import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.PhotoPickerListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
......@@ -48,6 +51,7 @@ import java.util.concurrent.TimeUnit;
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObserver<PickerBitmap>,
DecoderServiceHost.ServiceReadyCallback,
PickerCategoryView.VideoPlaybackStatusCallback,
AnimationListener {
// The timeout (in seconds) to wait for the decoder service to be ready.
private static final long WAIT_TIMEOUT_SECONDS = 30L;
......@@ -88,6 +92,12 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
// A callback that fires when a PickerBitmapView is animated in the dialog.
public final CallbackHelper onAnimatedCallback = new CallbackHelper();
// A callback that fires when playback starts for a video.
public final CallbackHelper onVideoPlayingCallback = new CallbackHelper();
// A callback that fires when playback ends for a video.
public final CallbackHelper onVideoEndedCallback = new CallbackHelper();
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
......@@ -99,6 +109,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
mTestFiles.add(new PickerBitmap(Uri.parse("e"), 1L, PickerBitmap.TileTypes.PICTURE));
mTestFiles.add(new PickerBitmap(Uri.parse("f"), 0L, PickerBitmap.TileTypes.PICTURE));
PickerCategoryView.setTestFiles(mTestFiles);
PickerCategoryView.setProgressCallback(this);
PickerBitmapView.setAnimationListenerForTest(this);
DecoderServiceHost.setReadyCallback(this);
......@@ -121,6 +132,18 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
onDecoderReadyCallback.notifyCalled();
}
// PickerCategoryView.VideoStatusCallback:
@Override
public void onVideoPlaying() {
onVideoPlayingCallback.notifyCalled();
}
@Override
public void onVideoEnded() {
onVideoEndedCallback.notifyCalled();
}
// SelectionObserver:
@Override
......@@ -198,7 +221,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
Assert.assertEquals(PhotoPickerAction.PHOTOS_SELECTED, mLastActionRecorded);
}
public void clickCancel() throws Exception {
private void clickCancel() throws Exception {
mLastActionRecorded = PhotoPickerAction.NUM_ENTRIES;
PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();
......@@ -209,6 +232,13 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
Assert.assertEquals(PhotoPickerAction.CANCEL, mLastActionRecorded);
}
private void playVideo(Uri uri) throws Exception {
int callCount = onVideoPlayingCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(
() -> { mDialog.getCategoryViewForTesting().startVideoPlaybackAsync(uri); });
onVideoPlayingCallback.waitForCallback(callCount, 1);
}
private void dismissDialog() {
TestThreadUtils.runOnUiThreadBlocking(() -> mDialog.dismiss());
}
......@@ -294,4 +324,44 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
dismissDialog();
}
@Test
@LargeTest
@DisableIf.Build(sdk_is_less_than = Build.VERSION_CODES.N) // Video is only supported on N+.
public void testVideoPlayerPlayAndRestart() throws Throwable {
createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
Assert.assertTrue(mDialog.isShowing());
waitForDecoder();
PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();
View container = categoryView.findViewById(R.id.playback_container);
Assert.assertTrue(container.getVisibility() == View.GONE);
// This test video takes less than a second to play.
String fileName = "chrome/test/data/android/photo_picker/noogler.mp4";
File file = new File(UrlUtils.getIsolatedTestFilePath(fileName));
int callCount = onVideoEndedCallback.getCallCount();
playVideo(Uri.fromFile(file));
Assert.assertTrue(container.getVisibility() == View.VISIBLE);
onVideoEndedCallback.waitForCallback(callCount, 1);
TestThreadUtils.runOnUiThreadBlocking(() -> {
View mute = categoryView.findViewById(R.id.mute);
categoryView.onClick(mute);
});
// Clicking the play button should restart playback.
callCount = onVideoEndedCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(() -> {
View playbutton = categoryView.findViewById(R.id.video_player_play_button);
categoryView.onClick(playbutton);
});
onVideoEndedCallback.waitForCallback(callCount, 1);
}
}
......@@ -512,8 +512,6 @@ const base::Feature kOverlayNewLayout{"OverlayNewLayout",
const base::Feature kPayWithGoogleV1{"PayWithGoogleV1",
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};
......
......@@ -3847,6 +3847,9 @@ The site does NOT gain access to the camera. The camera images are only visible
<message name="IDS_PHOTO_PICKER_BROWSE" desc="The label for the Browse action in the Photo Picker (browsing for photos).">
Browse
</message>
<message name="IDS_PHOTO_PICKER_VIDEO_DURATION" desc="The label showing the duration time and current position of the video, as in: 0:01 / 0:10.">
<ph name="POSITION">%1$s<ex>0:01</ex></ph> / <ph name="DURATION">%2$s<ex>0:10</ex></ph>
</message>
<!-- Interventions -->
<message name="IDS_REDIRECT_BLOCKED_MESSAGE" desc="The message stating that a redirect (noun) was blocked on this page. This will be followed on a separate line with the address the user was being redirected to.">
......
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