Commit 22d34c20 authored by Finnur Thorarinsson's avatar Finnur Thorarinsson Committed by Commit Bot

[Android] Photo Picker: Refactor Video Player out of PickerCategoryView.

This CL creates a custom View (PickerVideoPlayer) to encapsulate the
code in PickerCategoryView that relates to playing videos. The xml
components have already been factored out to a separate xml, and this
moves the corresponding Java code to a separate class also.

Bug: 895776, 656015
Change-Id: I4435f3a02bf18bc7c1afb35f35f34cb156908544
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2016649
Commit-Queue: Finnur Thorarinsson <finnur@chromium.org>
Reviewed-by: default avatarBoris Sazonov <bsazonov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#735819}
parent c33a2044
...@@ -1316,6 +1316,7 @@ chrome_java_sources = [ ...@@ -1316,6 +1316,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/photo_picker/PickerBitmapView.java", "java/src/org/chromium/chrome/browser/photo_picker/PickerBitmapView.java",
"java/src/org/chromium/chrome/browser/photo_picker/PickerBitmapViewHolder.java", "java/src/org/chromium/chrome/browser/photo_picker/PickerBitmapViewHolder.java",
"java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java", "java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java",
"java/src/org/chromium/chrome/browser/photo_picker/PickerVideoPlayer.java",
"java/src/org/chromium/chrome/browser/policy/PolicyAuditor.java", "java/src/org/chromium/chrome/browser/policy/PolicyAuditor.java",
"java/src/org/chromium/chrome/browser/prerender/ChromePrerenderService.java", "java/src/org/chromium/chrome/browser/prerender/ChromePrerenderService.java",
"java/src/org/chromium/chrome/browser/prerender/ExternalPrerenderHandler.java", "java/src/org/chromium/chrome/browser/prerender/ExternalPrerenderHandler.java",
......
...@@ -28,5 +28,11 @@ ...@@ -28,5 +28,11 @@
app:srcCompat="@drawable/zoom_in" app:srcCompat="@drawable/zoom_in"
android:visibility="gone" /> android:visibility="gone" />
<include layout="@layout/video_player" /> <org.chromium.chrome.browser.photo_picker.PickerVideoPlayer
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/playback_container"
android:background="@android:color/black"
android:clickable="true"
android:visibility="gone" />
</org.chromium.ui.widget.OptimizedFrameLayout> </org.chromium.ui.widget.OptimizedFrameLayout>
\ No newline at end of file
...@@ -3,15 +3,8 @@ ...@@ -3,15 +3,8 @@
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 <merge 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"
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 <VideoView
android:id="@+id/video_player" android:id="@+id/video_player"
...@@ -72,4 +65,4 @@ ...@@ -72,4 +65,4 @@
style="@style/PhotoPicker.SeekBar" /> style="@style/PhotoPicker.SeekBar" />
</FrameLayout> </FrameLayout>
</FrameLayout> </FrameLayout>
</FrameLayout> </merge>
...@@ -4,15 +4,12 @@ ...@@ -4,15 +4,12 @@
package org.chromium.chrome.browser.photo_picker; package org.chromium.chrome.browser.photo_picker;
import android.animation.Animator;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaPlayer;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
...@@ -24,20 +21,14 @@ import android.util.LruCache; ...@@ -24,20 +21,14 @@ import android.util.LruCache;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.VideoView;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import org.chromium.base.DiscardableReferencePool.DiscardableReference; import org.chromium.base.DiscardableReferencePool.DiscardableReference;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
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.flags.ChromeFeatureList; import org.chromium.chrome.browser.flags.ChromeFeatureList;
...@@ -45,7 +36,6 @@ import org.chromium.chrome.browser.util.ConversionUtils; ...@@ -45,7 +36,6 @@ import org.chromium.chrome.browser.util.ConversionUtils;
import org.chromium.chrome.browser.vr.VrModeProviderImpl; import org.chromium.chrome.browser.vr.VrModeProviderImpl;
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;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.net.MimeTypeFilter; import org.chromium.net.MimeTypeFilter;
import org.chromium.ui.PhotoPickerListener; import org.chromium.ui.PhotoPickerListener;
import org.chromium.ui.UiUtils; import org.chromium.ui.UiUtils;
...@@ -62,7 +52,6 @@ import java.util.List; ...@@ -62,7 +52,6 @@ import java.util.List;
public class PickerCategoryView extends RelativeLayout public class PickerCategoryView extends RelativeLayout
implements FileEnumWorkerTask.FilesEnumeratedCallback, RecyclerView.RecyclerListener, implements FileEnumWorkerTask.FilesEnumeratedCallback, RecyclerView.RecyclerListener,
DecoderServiceHost.ServiceReadyCallback, View.OnClickListener, DecoderServiceHost.ServiceReadyCallback, View.OnClickListener,
SeekBar.OnSeekBarChangeListener,
SelectionDelegate.SelectionObserver<PickerBitmap> { SelectionDelegate.SelectionObserver<PickerBitmap> {
// These values are written to logs. New enum values can be added, but existing // These values are written to logs. New enum values can be added, but existing
// enums must never be renumbered or deleted and reused. // enums must never be renumbered or deleted and reused.
...@@ -94,17 +83,6 @@ public class PickerCategoryView extends RelativeLayout ...@@ -94,17 +83,6 @@ public class PickerCategoryView extends RelativeLayout
} }
} }
/**
* A callback interface for notifying about video playback status.
*/
public interface VideoPlaybackStatusCallback {
// Called when the video starts playing.
void onVideoPlaying();
// Called when the video stops playing.
void onVideoEnded();
}
// The dialog that owns us. // The dialog that owns us.
private PhotoPickerDialog mDialog; private PhotoPickerDialog mDialog;
...@@ -201,35 +179,8 @@ public class PickerCategoryView extends RelativeLayout ...@@ -201,35 +179,8 @@ public class PickerCategoryView extends RelativeLayout
// A list of files to use for testing (instead of reading files on disk). // A list of files to use for testing (instead of reading files on disk).
private static List<PickerBitmap> sTestFiles; private static List<PickerBitmap> sTestFiles;
// The callback to use for reporting playback progress in tests. // The Video Player.
private static VideoPlaybackStatusCallback sProgressCallback; private final PickerVideoPlayer mVideoPlayer;
// The video preview view.
private final VideoView mVideoView;
// The MediaPlayer object used to control the VideoView.
private MediaPlayer mMediaPlayer;
// The container view for all the UI elements overlaid on top of the video.
private final View mVideoOverlayContainer;
// The container view for the UI video controls within the overlaid window.
private final View mVideoControls;
// The large Play button overlaid on top of the video.
private ImageView mLargePlayButton;
// The Mute button for the video.
private ImageView mMuteButton;
// Keeps track of whether audio track is enabled or not.
private boolean mAudioOn = true;
// The SeekBar showing the video playback progress (allows user seeking).
private SeekBar mSeekBar;
// A flag to control when the playback monitor schedules new tasks.
private boolean mRunPlaybackMonitoringTask;
// The Zoom (floating action) button. // The Zoom (floating action) button.
private ImageView mZoom; private ImageView mZoom;
...@@ -269,17 +220,7 @@ public class PickerCategoryView extends RelativeLayout ...@@ -269,17 +220,7 @@ public class PickerCategoryView extends RelativeLayout
toolbar.setDelegate(delegate); toolbar.setDelegate(delegate);
Button doneButton = (Button) toolbar.findViewById(R.id.done); Button doneButton = (Button) toolbar.findViewById(R.id.done);
doneButton.setOnClickListener(this); doneButton.setOnClickListener(this);
mVideoView = findViewById(R.id.video_player); mVideoPlayer = findViewById(R.id.playback_container);
mVideoOverlayContainer = findViewById(R.id.video_overlay_container);
mVideoOverlayContainer.setOnClickListener(this);
mVideoControls = findViewById(R.id.video_controls);
mLargePlayButton = findViewById(R.id.video_player_play_button);
mLargePlayButton.setOnClickListener(this);
mMuteButton = findViewById(R.id.mute);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
mMuteButton.setOnClickListener(this);
mSeekBar = findViewById(R.id.seek_bar);
mSeekBar.setOnSeekBarChangeListener(this);
mZoom = findViewById(R.id.zoom); mZoom = findViewById(R.id.zoom);
calculateGridMetrics(); calculateGridMetrics();
...@@ -320,13 +261,6 @@ public class PickerCategoryView extends RelativeLayout ...@@ -320,13 +261,6 @@ public class PickerCategoryView extends RelativeLayout
mPickerAdapter.notifyDataSetChanged(); mPickerAdapter.notifyDataSetChanged();
mRecyclerView.requestLayout(); mRecyclerView.requestLayout();
} }
if (mVideoControls.getVisibility() != View.GONE) {
// When configuration changes, the video overlay controls need to be synced to the new
// video size. Post a task, so that size adjustments happen after layout of the video
// controls has completed.
ThreadUtils.postOnUiThread(() -> { syncOverlayControlsSize(); });
}
} }
/** /**
...@@ -349,45 +283,7 @@ public class PickerCategoryView extends RelativeLayout ...@@ -349,45 +283,7 @@ public class PickerCategoryView extends RelativeLayout
* @param uri The uri of the video to start playing. * @param uri The uri of the video to start playing.
*/ */
public void startVideoPlaybackAsync(Uri uri) { public void startVideoPlaybackAsync(Uri uri) {
View playbackContainer = findViewById(R.id.playback_container); mVideoPlayer.startVideoPlaybackAsync(uri);
playbackContainer.setVisibility(View.VISIBLE);
mVideoView.setVisibility(View.VISIBLE);
mVideoView.setVideoURI(uri);
mVideoView.setOnPreparedListener((MediaPlayer mediaPlayer) -> {
mMediaPlayer = mediaPlayer;
startVideoPlayback();
mMediaPlayer.setOnVideoSizeChangedListener(
(MediaPlayer player, int width, int height) -> { syncOverlayControlsSize(); });
if (sProgressCallback != null) {
mMediaPlayer.setOnInfoListener((MediaPlayer player, int what, int extra) -> {
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
sProgressCallback.onVideoPlaying();
return true;
}
return false;
});
}
});
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
// Once we reach the completion point, show the overlay controls (without fading
// away) to indicate that playback has reached the end of the video (and didn't
// break before reaching the end). This also allows the user to restart playback
// from the start, by pressing Play.
mLargePlayButton.setImageResource(R.drawable.ic_play_circle_filled_white_24dp);
updateProgress();
showOverlayControls(/*animateAway=*/false);
if (sProgressCallback != null) {
sProgressCallback.onVideoEnded();
}
}
});
} }
/** /**
...@@ -396,16 +292,7 @@ public class PickerCategoryView extends RelativeLayout ...@@ -396,16 +292,7 @@ public class PickerCategoryView extends RelativeLayout
* @return true if a video container was showing, false otherwise. * @return true if a video container was showing, false otherwise.
*/ */
public boolean closeVideoPlayer() { public boolean closeVideoPlayer() {
View playbackContainer = findViewById(R.id.playback_container); return mVideoPlayer.closeVideoPlayer();
if (playbackContainer.getVisibility() != View.VISIBLE) {
return false;
}
playbackContainer.setVisibility(View.GONE);
stopVideoPlayback();
mVideoView.setMediaController(null);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
return true;
} }
/** /**
...@@ -490,52 +377,11 @@ public class PickerCategoryView extends RelativeLayout ...@@ -490,52 +377,11 @@ public class PickerCategoryView extends RelativeLayout
if (!mZoomSwitchingInEffect) { if (!mZoomSwitchingInEffect) {
flipZoomMode(); flipZoomMode();
} }
} else if (id == R.id.video_overlay_container) {
showOverlayControls(/*animateAway=*/true);
} else if (id == R.id.video_player_play_button) {
toggleVideoPlayback();
} else if (id == R.id.mute) {
toggleMute();
} else { } else {
executeAction(PhotoPickerListener.PhotoPickerAction.CANCEL, null, ACTION_CANCEL); executeAction(PhotoPickerListener.PhotoPickerAction.CANCEL, null, ACTION_CANCEL);
} }
} }
// SeekBar.OnSeekBarChangeListener:
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
final boolean seekDuringPlay = mVideoView.isPlaying();
mMediaPlayer.setOnSeekCompleteListener(mp -> {
mMediaPlayer.setOnSeekCompleteListener(null);
if (seekDuringPlay) {
startVideoPlayback();
}
});
float percentage = progress / 100f;
int seekTo = Math.round(percentage * mVideoView.getDuration());
if (Build.VERSION.SDK_INT >= 26) {
mMediaPlayer.seekTo(seekTo, MediaPlayer.SEEK_CLOSEST);
} else {
// On older versions, sync to nearest previous key frame.
mVideoView.seekTo(seekTo);
}
updateProgress();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
cancelFadeAwayAnimation();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
fadeAwayVideoControls();
}
/** /**
* Start loading of bitmaps, once files have been enumerated and service is * Start loading of bitmaps, once files have been enumerated and service is
* ready to decode. * ready to decode.
...@@ -806,148 +652,19 @@ public class PickerCategoryView extends RelativeLayout ...@@ -806,148 +652,19 @@ public class PickerCategoryView extends RelativeLayout
"Android.PhotoPicker.CacheHits", mPickerAdapter.getCacheHitCount()); "Android.PhotoPicker.CacheHits", mPickerAdapter.getCacheHitCount());
} }
private void showOverlayControls(boolean animateAway) {
cancelFadeAwayAnimation();
if (animateAway && mVideoView.isPlaying()) {
fadeAwayVideoControls();
startPlaybackMonitor();
}
}
private void fadeAwayVideoControls() {
mVideoOverlayContainer.animate()
.alpha(0.0f)
.setStartDelay(3000)
.setDuration(1000)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
enableClickableButtons(false);
stopPlaybackMonitor();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
}
private void cancelFadeAwayAnimation() {
// Canceling the animation will leave the alpha in the state it had reached while animating,
// so we need to explicitly set the alpha to 1.0 to reset it.
mVideoOverlayContainer.animate().cancel();
mVideoOverlayContainer.setAlpha(1.0f);
enableClickableButtons(true);
}
private void enableClickableButtons(boolean enable) {
mLargePlayButton.setClickable(enable);
mMuteButton.setClickable(enable);
}
private void updateProgress() {
String current;
String total;
try {
current = DecodeVideoTask.formatDuration(Long.valueOf(mVideoView.getCurrentPosition()));
total = DecodeVideoTask.formatDuration(Long.valueOf(mVideoView.getDuration()));
} catch (IllegalStateException exception) {
// VideoView#getCurrentPosition throws this error if the dialog has been dismissed while
// waiting to update the status.
return;
}
SeekBar seekBar = findViewById(R.id.seek_bar);
if (seekBar == null) {
return;
}
TextView remainingTime = findViewById(R.id.remaining_time);
String formattedProgress = mActivity.getResources().getString(
R.string.photo_picker_video_duration, current, total);
remainingTime.setText(formattedProgress);
int percentage = mVideoView.getDuration() == 0
? 0
: mVideoView.getCurrentPosition() * 100 / mVideoView.getDuration();
seekBar.setProgress(percentage);
if (mRunPlaybackMonitoringTask) {
startPlaybackMonitorTask();
}
}
private void startVideoPlayback() {
mMediaPlayer.start();
mLargePlayButton.setImageResource(R.drawable.ic_pause_circle_outline_white_24dp);
showOverlayControls(/*animateAway=*/true);
}
private void stopVideoPlayback() {
stopPlaybackMonitor();
mMediaPlayer.pause();
mLargePlayButton.setImageResource(R.drawable.ic_play_circle_filled_white_24dp);
showOverlayControls(/*animateAway=*/false);
}
private void toggleVideoPlayback() {
if (mVideoView.isPlaying()) {
stopVideoPlayback();
} else {
startVideoPlayback();
}
}
private void syncOverlayControlsSize() {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
mVideoView.getMeasuredWidth(), mVideoView.getMeasuredHeight());
mVideoControls.setLayoutParams(params);
}
private void toggleMute() {
mAudioOn = !mAudioOn;
if (mAudioOn) {
mMediaPlayer.setVolume(1f, 1f);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
} else {
mMediaPlayer.setVolume(0f, 0f);
mMuteButton.setImageResource(R.drawable.ic_volume_off_white_24dp);
}
}
private void startPlaybackMonitor() {
mRunPlaybackMonitoringTask = true;
startPlaybackMonitorTask();
}
private void startPlaybackMonitorTask() {
PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT, () -> updateProgress(), 250);
}
private void stopPlaybackMonitor() {
mRunPlaybackMonitoringTask = false;
}
/** Sets a list of files to use as data for the dialog. For testing use only. */ /** Sets a list of files to use as data for the dialog. For testing use only. */
@VisibleForTesting @VisibleForTesting
public static void setTestFiles(List<PickerBitmap> testFiles) { public static void setTestFiles(List<PickerBitmap> testFiles) {
sTestFiles = new ArrayList<>(testFiles); sTestFiles = new ArrayList<>(testFiles);
} }
/** Sets the video playback progress callback. For testing use only. */
@VisibleForTesting @VisibleForTesting
public static void setProgressCallback(VideoPlaybackStatusCallback callback) { public SelectionDelegate<PickerBitmap> getSelectionDelegateForTesting() {
sProgressCallback = callback; return mSelectionDelegate;
} }
@VisibleForTesting @VisibleForTesting
public SelectionDelegate<PickerBitmap> getSelectionDelegateForTesting() { public PickerVideoPlayer getVideoPlayerForTesting() {
return mSelectionDelegate; return mVideoPlayer;
} }
} }
// 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.
package org.chromium.chrome.browser.photo_picker;
import android.animation.Animator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.VideoView;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.R;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/**
* Encapsulates the video player functionality of the Photo Picker dialog.
*/
public class PickerVideoPlayer
extends FrameLayout implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
/**
* A callback interface for notifying about video playback status.
*/
public interface VideoPlaybackStatusCallback {
// Called when the video starts playing.
void onVideoPlaying();
// Called when the video stops playing.
void onVideoEnded();
}
// The callback to use for reporting playback progress in tests.
private static VideoPlaybackStatusCallback sProgressCallback;
// The resources to use.
private Resources mResources;
// The video preview view.
private final VideoView mVideoView;
// The MediaPlayer object used to control the VideoView.
private MediaPlayer mMediaPlayer;
// The container view for all the UI elements overlaid on top of the video.
private final View mVideoOverlayContainer;
// The container view for the UI video controls within the overlaid window.
private final View mVideoControls;
// The large Play button overlaid on top of the video.
private final ImageView mLargePlayButton;
// The Mute button for the video.
private final ImageView mMuteButton;
// Keeps track of whether audio track is enabled or not.
private boolean mAudioOn = true;
// The remaining video playback time.
private final TextView mRemainingTime;
// The SeekBar showing the video playback progress (allows user seeking).
private final SeekBar mSeekBar;
// A flag to control when the playback monitor schedules new tasks.
private boolean mRunPlaybackMonitoringTask;
/**
* Constructor for inflating from XML.
*/
public PickerVideoPlayer(Context context, AttributeSet attrs) {
super(context, attrs);
mResources = context.getResources();
LayoutInflater.from(context).inflate(R.layout.video_player, this);
mVideoView = findViewById(R.id.video_player);
mVideoOverlayContainer = findViewById(R.id.video_overlay_container);
mVideoControls = findViewById(R.id.video_controls);
mLargePlayButton = findViewById(R.id.video_player_play_button);
mMuteButton = findViewById(R.id.mute);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
mRemainingTime = findViewById(R.id.remaining_time);
mSeekBar = findViewById(R.id.seek_bar);
mVideoOverlayContainer.setOnClickListener(this);
mLargePlayButton.setOnClickListener(this);
mMuteButton.setOnClickListener(this);
mSeekBar.setOnSeekBarChangeListener(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (mVideoControls.getVisibility() != View.GONE) {
// When configuration changes, the video overlay controls need to be synced to the new
// video size. Post a task, so that size adjustments happen after layout of the video
// controls has completed.
ThreadUtils.postOnUiThread(() -> { syncOverlayControlsSize(); });
}
}
/**
* Start playback of a video in an overlay above the photo picker.
* @param uri The uri of the video to start playing.
*/
public void startVideoPlaybackAsync(Uri uri) {
setVisibility(View.VISIBLE);
mVideoView.setVisibility(View.VISIBLE);
mVideoView.setVideoURI(uri);
mVideoView.setOnPreparedListener((MediaPlayer mediaPlayer) -> {
mMediaPlayer = mediaPlayer;
startVideoPlayback();
mMediaPlayer.setOnVideoSizeChangedListener(
(MediaPlayer player, int width, int height) -> { syncOverlayControlsSize(); });
if (sProgressCallback != null) {
mMediaPlayer.setOnInfoListener((MediaPlayer player, int what, int extra) -> {
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
sProgressCallback.onVideoPlaying();
return true;
}
return false;
});
}
});
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
// Once we reach the completion point, show the overlay controls (without fading
// away) to indicate that playback has reached the end of the video (and didn't
// break before reaching the end). This also allows the user to restart playback
// from the start, by pressing Play.
mLargePlayButton.setImageResource(R.drawable.ic_play_circle_filled_white_24dp);
updateProgress();
showOverlayControls(/*animateAway=*/false);
if (sProgressCallback != null) {
sProgressCallback.onVideoEnded();
}
}
});
}
/**
* Ends video playback (if a video is playing) and closes the video player. Aborts if the video
* playback container is not showing.
* @return true if a video container was showing, false otherwise.
*/
public boolean closeVideoPlayer() {
if (getVisibility() != View.VISIBLE) {
return false;
}
setVisibility(View.GONE);
stopVideoPlayback();
mVideoView.setMediaController(null);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
return true;
}
// OnClickListener:
@Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.video_overlay_container) {
showOverlayControls(/*animateAway=*/true);
} else if (id == R.id.video_player_play_button) {
toggleVideoPlayback();
} else if (id == R.id.mute) {
toggleMute();
}
}
// SeekBar.OnSeekBarChangeListener:
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
final boolean seekDuringPlay = mVideoView.isPlaying();
mMediaPlayer.setOnSeekCompleteListener(mp -> {
mMediaPlayer.setOnSeekCompleteListener(null);
if (seekDuringPlay) {
startVideoPlayback();
}
});
float percentage = progress / 100f;
int seekTo = Math.round(percentage * mVideoView.getDuration());
if (Build.VERSION.SDK_INT >= 26) {
mMediaPlayer.seekTo(seekTo, MediaPlayer.SEEK_CLOSEST);
} else {
// On older versions, sync to nearest previous key frame.
mVideoView.seekTo(seekTo);
}
updateProgress();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
cancelFadeAwayAnimation();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
fadeAwayVideoControls();
}
private void showOverlayControls(boolean animateAway) {
cancelFadeAwayAnimation();
if (animateAway && mVideoView.isPlaying()) {
fadeAwayVideoControls();
startPlaybackMonitor();
}
}
private void fadeAwayVideoControls() {
mVideoOverlayContainer.animate()
.alpha(0.0f)
.setStartDelay(3000)
.setDuration(1000)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
enableClickableButtons(false);
stopPlaybackMonitor();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
}
private void cancelFadeAwayAnimation() {
// Canceling the animation will leave the alpha in the state it had reached while animating,
// so we need to explicitly set the alpha to 1.0 to reset it.
mVideoOverlayContainer.animate().cancel();
mVideoOverlayContainer.setAlpha(1.0f);
enableClickableButtons(true);
}
private void enableClickableButtons(boolean enable) {
mLargePlayButton.setClickable(enable);
mMuteButton.setClickable(enable);
}
private void updateProgress() {
String current;
String total;
try {
current = DecodeVideoTask.formatDuration(Long.valueOf(mVideoView.getCurrentPosition()));
total = DecodeVideoTask.formatDuration(Long.valueOf(mVideoView.getDuration()));
} catch (IllegalStateException exception) {
// VideoView#getCurrentPosition throws this error if the dialog has been dismissed while
// waiting to update the status.
return;
}
String formattedProgress =
mResources.getString(R.string.photo_picker_video_duration, current, total);
mRemainingTime.setText(formattedProgress);
int percentage = mVideoView.getDuration() == 0
? 0
: mVideoView.getCurrentPosition() * 100 / mVideoView.getDuration();
mSeekBar.setProgress(percentage);
if (mVideoView.isPlaying() && mRunPlaybackMonitoringTask) {
startPlaybackMonitor();
}
}
private void startVideoPlayback() {
mMediaPlayer.start();
mLargePlayButton.setImageResource(R.drawable.ic_pause_circle_outline_white_24dp);
showOverlayControls(/*animateAway=*/true);
}
private void stopVideoPlayback() {
stopPlaybackMonitor();
mMediaPlayer.pause();
mLargePlayButton.setImageResource(R.drawable.ic_play_circle_filled_white_24dp);
showOverlayControls(/*animateAway=*/false);
}
private void toggleVideoPlayback() {
if (mVideoView.isPlaying()) {
stopVideoPlayback();
} else {
startVideoPlayback();
}
}
private void syncOverlayControlsSize() {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
mVideoView.getMeasuredWidth(), mVideoView.getMeasuredHeight());
mVideoControls.setLayoutParams(params);
}
private void toggleMute() {
mAudioOn = !mAudioOn;
if (mAudioOn) {
mMediaPlayer.setVolume(1f, 1f);
mMuteButton.setImageResource(R.drawable.ic_volume_on_white_24dp);
} else {
mMediaPlayer.setVolume(0f, 0f);
mMuteButton.setImageResource(R.drawable.ic_volume_off_white_24dp);
}
}
private void startPlaybackMonitor() {
mRunPlaybackMonitoringTask = true;
startPlaybackMonitorTask();
}
private void startPlaybackMonitorTask() {
PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT, () -> updateProgress(), 250);
}
private void stopPlaybackMonitor() {
mRunPlaybackMonitoringTask = false;
}
/** Sets the video playback progress callback. For testing use only. */
@VisibleForTesting
public static void setProgressCallback(VideoPlaybackStatusCallback callback) {
sProgressCallback = callback;
}
}
...@@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit; ...@@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit;
@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,
PickerCategoryView.VideoPlaybackStatusCallback, PickerVideoPlayer.VideoPlaybackStatusCallback,
AnimationListener { AnimationListener {
// The timeout (in seconds) to wait for the decoder service to be ready. // The timeout (in seconds) to wait for the decoder service to be ready.
private static final long WAIT_TIMEOUT_SECONDS = 30L; private static final long WAIT_TIMEOUT_SECONDS = 30L;
...@@ -110,7 +110,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -110,7 +110,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
mTestFiles.add(new PickerBitmap(Uri.parse("e"), 1L, PickerBitmap.TileTypes.PICTURE)); mTestFiles.add(new PickerBitmap(Uri.parse("e"), 1L, PickerBitmap.TileTypes.PICTURE));
mTestFiles.add(new PickerBitmap(Uri.parse("f"), 0L, PickerBitmap.TileTypes.PICTURE)); mTestFiles.add(new PickerBitmap(Uri.parse("f"), 0L, PickerBitmap.TileTypes.PICTURE));
PickerCategoryView.setTestFiles(mTestFiles); PickerCategoryView.setTestFiles(mTestFiles);
PickerCategoryView.setProgressCallback(this); PickerVideoPlayer.setProgressCallback(this);
PickerBitmapView.setAnimationListenerForTest(this); PickerBitmapView.setAnimationListenerForTest(this);
DecoderServiceHost.setReadyCallback(this); DecoderServiceHost.setReadyCallback(this);
...@@ -357,7 +357,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -357,7 +357,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
TestThreadUtils.runOnUiThreadBlocking(() -> { TestThreadUtils.runOnUiThreadBlocking(() -> {
View mute = categoryView.findViewById(R.id.mute); View mute = categoryView.findViewById(R.id.mute);
categoryView.onClick(mute); categoryView.getVideoPlayerForTesting().onClick(mute);
}); });
// Clicking the play button should restart playback. // Clicking the play button should restart playback.
...@@ -365,7 +365,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse ...@@ -365,7 +365,7 @@ public class PhotoPickerDialogTest implements PhotoPickerListener, SelectionObse
TestThreadUtils.runOnUiThreadBlocking(() -> { TestThreadUtils.runOnUiThreadBlocking(() -> {
View playbutton = categoryView.findViewById(R.id.video_player_play_button); View playbutton = categoryView.findViewById(R.id.video_player_play_button);
categoryView.onClick(playbutton); categoryView.getVideoPlayerForTesting().onClick(playbutton);
}); });
onVideoEndedCallback.waitForCallback(callCount, 1); onVideoEndedCallback.waitForCallback(callCount, 1);
......
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