Skip to content

Commit

Permalink
[Merge 105] Android pip: fix control not working properly.
Browse files Browse the repository at this point in the history
This CL fix the issue where default controls in Picture-in-picture window is controlling wrong media when pip'ed media isn't active. This is caused by default controls in pip window are meant for active media session. The CL fix the issue by adding custom controls (play/pause, previous track, next track) to Picture-in-picture params to override the default controls.

Bug: 1345255

(cherry picked from commit 0dbc538)

Change-Id: I92d87513d12c919eb0a4ebc7d37d4de6fec10766
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3773420
Commit-Queue: Jazz Xu <[email protected]>
Reviewed-by: Frank Liberato <[email protected]>
Reviewed-by: Yaron Friedman <[email protected]>
Cr-Original-Commit-Position: refs/heads/main@{#1027363}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3785119
Owners-Override: Krishna Govind <[email protected]>
Bot-Commit: Rubber Stamper <[email protected]>
Commit-Queue: Krishna Govind <[email protected]>
Auto-Submit: Jazz Xu <[email protected]>
Reviewed-by: Krishna Govind <[email protected]>
Cr-Commit-Position: refs/branch-heads/5195@{#53}
Cr-Branched-From: 7aa3f07-refs/heads/main@{#1027018}
  • Loading branch information
Jazz Xu authored and Chromium LUCI CQ committed Jul 27, 2022
1 parent 80a04f5 commit 815cfd5
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,45 @@
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.MathUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.components.browser_ui.media.R;
import org.chromium.components.thinwebview.CompositorView;
import org.chromium.components.thinwebview.CompositorViewFactory;
import org.chromium.components.thinwebview.ThinWebViewConstraints;
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.WebContents;
import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.WindowAndroid;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

/**
* A picture in picture activity which get created when requesting
* PiP from web API. The activity will connect to web API through
* OverlayWindowAndroid.
*/
public class PictureInPictureActivity extends AsyncInitializationActivity {
private static final String ACTION_PLAY =
"org.chromium.chrome.browser.media.PictureInPictureActivity.Play";
// Used to filter media buttons' remote action intents.
private static final String MEDIA_ACTION =
"org.chromium.chrome.browser.media.PictureInPictureActivity.MediaAction";
// Used to determine which action button has been touched.
private static final String CONTROL_TYPE =
"org.chromium.chrome.browser.media.PictureInPictureActivity.ControlType";

// Used to verify Pre-T that the broadcast sender was Chrome. This extra can be removed when the
// min supported version is Android T.
private static final String EXTRA_RECEIVER_TOKEN =
"org.chromium.chrome.browser.media.PictureInPictureActivity.ReceiverToken";

// Use for passing unique window id to each PictureInPictureActivity instance.
private static final String NATIVE_POINTER_KEY =
Expand All @@ -87,13 +96,12 @@ public class PictureInPictureActivity extends AsyncInitializationActivity {
private Tab mInitiatorTab;
private InitiatorTabObserver mTabObserver;

// Used to verify Pre-T that the broadcast sender was Chrome. This extra can be removed when the
// min supported version is Android T.
private static final String EXTRA_RECEIVER_TOKEN = "receiver_token";

private CompositorView mCompositorView;
private MediaSessionObserver mMediaSessionObserver;
private boolean mIsPlayPauseVisible;

private boolean mIsPlaying;

// MediaSessionaction, RemoteAction pairs.
private HashMap<Integer, RemoteAction> mRemoteActions = new HashMap<>();

// If present, this is the video's aspect ratio.
private Rational mAspectRatio;
Expand All @@ -112,13 +120,27 @@ public void onReceive(Context context, Intent intent) {
return;
}

if (intent.getAction() == null || !intent.getAction().equals(ACTION_PLAY)) return;
if (intent.getAction() == null || !intent.getAction().equals(MEDIA_ACTION)) return;
if (!intent.hasExtra(EXTRA_RECEIVER_TOKEN)
|| intent.getIntExtra(EXTRA_RECEIVER_TOKEN, 0) != this.hashCode()) {
return;
}

PictureInPictureActivityJni.get().play(nativeOverlayWindowAndroid);
switch (intent.getIntExtra(CONTROL_TYPE, -1)) {
case MediaSessionAction.PLAY:
case MediaSessionAction.PAUSE:
// TODO(crbug.com/1345956): Play/pause state might get out of sync.
PictureInPictureActivityJni.get().togglePlayPause(nativeOverlayWindowAndroid);
return;
case MediaSessionAction.PREVIOUS_TRACK:
PictureInPictureActivityJni.get().previousTrack(nativeOverlayWindowAndroid);
return;
case MediaSessionAction.NEXT_TRACK:
PictureInPictureActivityJni.get().nextTrack(nativeOverlayWindowAndroid);
return;
default:
return;
}
}
};

Expand Down Expand Up @@ -249,21 +271,13 @@ public void onStart() {

mMediaSessionReceiver = new MediaSessionBroadcastReceiver();
ContextUtils.registerNonExportedBroadcastReceiver(
this, mMediaSessionReceiver, new IntentFilter(ACTION_PLAY));
this, mMediaSessionReceiver, new IntentFilter(MEDIA_ACTION));

initializeRemoteActions();

PictureInPictureActivityJni.get().onActivityStart(
mNativeOverlayWindowAndroid, this, getWindowAndroid());

// Add an observer to refresh the Picture-in-Picture params if the media
// session state changes.
MediaSession mediaSession = MediaSession.fromWebContents(mInitiatorTab.getWebContents());
mMediaSessionObserver = new MediaSessionObserver(mediaSession) {
@Override
public void mediaSessionStateChanged(boolean isControllable, boolean isSuspended) {
updatePictureInPictureParams();
}
};

// See if there are PiP hints in the extras.
Size size = new Size(
intent.getIntExtra(SOURCE_WIDTH_KEY, 0), intent.getIntExtra(SOURCE_HEIGHT_KEY, 0));
Expand Down Expand Up @@ -293,11 +307,6 @@ public void onDestroy() {
mMediaSessionReceiver = null;
}

if (mMediaSessionObserver != null) {
mMediaSessionObserver.stopObserving();
mMediaSessionObserver = null;
}

if (mInitiatorTab != null) {
mInitiatorTab.removeObserver(mTabObserver);
mInitiatorTab = null;
Expand Down Expand Up @@ -346,23 +355,10 @@ public void close() {
private PictureInPictureParams getPictureInPictureParams() {
ArrayList<RemoteAction> actions = new ArrayList<>();

// If the associated media session is not controllable then we should
// place a play button in the Picture-in-Picture window that will
// trigger playback.
if (mMediaSessionObserver != null
&& !mMediaSessionObserver.getMediaSession().isControllable()
&& mIsPlayPauseVisible) {
Intent intent = new Intent(ACTION_PLAY);
intent.putExtra(EXTRA_RECEIVER_TOKEN, mMediaSessionReceiver.hashCode());
intent.putExtra(NATIVE_POINTER_KEY, mNativeOverlayWindowAndroid);
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,
intent, IntentUtils.getPendingIntentMutabilityFlag(false));

actions.add(new RemoteAction(Icon.createWithResource(getApplicationContext(),
R.drawable.ic_play_arrow_white_36dp),
getApplicationContext().getResources().getText(R.string.accessibility_play), "",
pendingIntent));
}
actions.add(mRemoteActions.get(MediaSessionAction.PREVIOUS_TRACK));
actions.add(mRemoteActions.get(
mIsPlaying ? MediaSessionAction.PAUSE : MediaSessionAction.PLAY));
actions.add(mRemoteActions.get(MediaSessionAction.NEXT_TRACK));

PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
builder.setActions(actions);
Expand All @@ -371,6 +367,67 @@ private PictureInPictureParams getPictureInPictureParams() {
return builder.build();
}

@SuppressLint("NewApi")
private void initializeRemoteActions() {
mRemoteActions.put(MediaSessionAction.PLAY, createRemoteAction(MediaSessionAction.PLAY));
mRemoteActions.put(MediaSessionAction.PAUSE, createRemoteAction(MediaSessionAction.PAUSE));
mRemoteActions.put(MediaSessionAction.PREVIOUS_TRACK,
createRemoteAction(MediaSessionAction.PREVIOUS_TRACK));
mRemoteActions.put(
MediaSessionAction.NEXT_TRACK, createRemoteAction(MediaSessionAction.NEXT_TRACK));
}

/**
* Create remote actions for picutre-in-picture window.
*
* @param action {@link MediaSessionAction} that the action button is corresponding to.
*/
@SuppressLint("NewApi")
private RemoteAction createRemoteAction(int action) {
Intent intent = new Intent(MEDIA_ACTION);
intent.putExtra(EXTRA_RECEIVER_TOKEN, mMediaSessionReceiver.hashCode());
intent.putExtra(CONTROL_TYPE, action);
intent.putExtra(NATIVE_POINTER_KEY, mNativeOverlayWindowAndroid);
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), action,
intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

RemoteAction remoteAction;
switch (action) {
case MediaSessionAction.PLAY:
remoteAction = new RemoteAction(Icon.createWithResource(getApplicationContext(),
R.drawable.ic_play_arrow_white_36dp),
getApplicationContext().getResources().getText(R.string.accessibility_play),
"", pendingIntent);
break;
case MediaSessionAction.PAUSE:
remoteAction = new RemoteAction(Icon.createWithResource(getApplicationContext(),
R.drawable.ic_pause_white_36dp),
getApplicationContext().getResources().getText(
R.string.accessibility_pause),
"", pendingIntent);
break;
case MediaSessionAction.NEXT_TRACK:
remoteAction = new RemoteAction(Icon.createWithResource(getApplicationContext(),
R.drawable.ic_skip_next_white_36dp),
getApplicationContext().getResources().getText(
R.string.accessibility_next_track),
"", pendingIntent);
break;
case MediaSessionAction.PREVIOUS_TRACK:
remoteAction = new RemoteAction(Icon.createWithResource(getApplicationContext(),
R.drawable.ic_skip_previous_white_36dp),
getApplicationContext().getResources().getText(
R.string.accessibility_previous_track),
"", pendingIntent);
break;
default:
remoteAction = null;
}

remoteAction.setEnabled(false);
return remoteAction;
}

@SuppressLint("NewApi")
private void updatePictureInPictureParams() {
setPictureInPictureParams(getPictureInPictureParams());
Expand Down Expand Up @@ -400,8 +457,22 @@ private void clampAndStoreAspectRatio(int width, int height) {

@CalledByNative
@SuppressLint("NewAPI")
private void setPlayPauseButtonVisibility(boolean isVisible) {
mIsPlayPauseVisible = isVisible;
private void setPlaybackState(boolean isPlaying) {
mIsPlaying = isPlaying;
updatePictureInPictureParams();
}

@CalledByNative
@SuppressLint("NewAPI")
private void updateVisibleActions(int[] actions) {
HashSet<Integer> visibleActions = new HashSet<>();
for (int action : actions) visibleActions.add(action);

for (Integer actionKey : mRemoteActions.keySet()) {
RemoteAction remoteAction = mRemoteActions.get(actionKey);
if (remoteAction.isEnabled() == visibleActions.contains(actionKey)) continue;
remoteAction.setEnabled(!remoteAction.isEnabled());
}
updatePictureInPictureParams();
}

Expand Down Expand Up @@ -508,7 +579,9 @@ void onActivityStart(long nativeOverlayWindowAndroid, PictureInPictureActivity s

void destroy(long nativeOverlayWindowAndroid);

void play(long nativeOverlayWindowAndroid);
void togglePlayPause(long nativeOverlayWindowAndroid);
void nextTrack(long nativeOverlayWindowAndroid);
void previousTrack(long nativeOverlayWindowAndroid);

void compositorViewCreated(long nativeOverlayWindowAndroid, CompositorView compositorView);

Expand Down
80 changes: 73 additions & 7 deletions chrome/browser/ui/android/overlay/overlay_window_android.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "chrome/browser/ui/android/overlay/overlay_window_android.h"

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/memory/ptr_util.h"
#include "cc/layers/surface_layer.h"
#include "chrome/android/chrome_jni_headers/PictureInPictureActivity_jni.h"
Expand Down Expand Up @@ -90,8 +91,9 @@ void OverlayWindowAndroid::OnActivityStart(
window_android_ = ui::WindowAndroid::FromJavaWindowAndroid(jwindow_android);
window_android_->AddObserver(this);

Java_PictureInPictureActivity_setPlayPauseButtonVisibility(
env, java_ref_.get(env), is_play_pause_button_visible_);
Java_PictureInPictureActivity_setPlaybackState(env, java_ref_.get(env),
is_playing_);
MaybeNotifyVisibleActionsChanged();

if (video_size_.IsEmpty())
return;
Expand Down Expand Up @@ -125,11 +127,19 @@ void OverlayWindowAndroid::Destroy(JNIEnv* env) {
controller_->OnWindowDestroyed(/*should_pause_video=*/true);
}

void OverlayWindowAndroid::Play(JNIEnv* env) {
void OverlayWindowAndroid::TogglePlayPause(JNIEnv* env) {
DCHECK(!controller_->IsPlayerActive());
controller_->TogglePlayPause();
}

void OverlayWindowAndroid::NextTrack(JNIEnv* env) {
controller_->NextTrack();
}

void OverlayWindowAndroid::PreviousTrack(JNIEnv* env) {
controller_->PreviousTrack();
}

void OverlayWindowAndroid::CompositorViewCreated(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& compositor_view) {
Expand Down Expand Up @@ -206,14 +216,41 @@ void OverlayWindowAndroid::UpdateNaturalSize(const gfx::Size& natural_size) {
env, java_ref_.get(env), natural_size.width(), natural_size.height());
}

void OverlayWindowAndroid::SetPlayPauseButtonVisibility(bool is_visible) {
is_play_pause_button_visible_ = is_visible;
// TODO(crbug.com/1345953): Handle replay action when video reaches the end.
void OverlayWindowAndroid::SetPlaybackState(PlaybackState playback_state) {
is_playing_ = playback_state == PlaybackState::kPlaying;
if (java_ref_.is_uninitialized())
return;

JNIEnv* env = base::android::AttachCurrentThread();
Java_PictureInPictureActivity_setPlayPauseButtonVisibility(
env, java_ref_.get(env), is_visible);
Java_PictureInPictureActivity_setPlaybackState(env, java_ref_.get(env),
is_playing_);
}

void OverlayWindowAndroid::SetPlayPauseButtonVisibility(bool is_visible) {
if (!MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kPlay,
is_visible)) {
return;
}

MaybeUpdateVisibleAction(media_session::mojom::MediaSessionAction::kPause,
is_visible);
MaybeNotifyVisibleActionsChanged();
}

void OverlayWindowAndroid::SetNextTrackButtonVisibility(bool is_visible) {
if (MaybeUpdateVisibleAction(
media_session::mojom::MediaSessionAction::kNextTrack, is_visible)) {
MaybeNotifyVisibleActionsChanged();
}
}

void OverlayWindowAndroid::SetPreviousTrackButtonVisibility(bool is_visible) {
if (MaybeUpdateVisibleAction(
media_session::mojom::MediaSessionAction::kPreviousTrack,
is_visible)) {
MaybeNotifyVisibleActionsChanged();
}
}

void OverlayWindowAndroid::SetSurfaceId(const viz::SurfaceId& surface_id) {
Expand All @@ -237,3 +274,32 @@ void OverlayWindowAndroid::SetSurfaceId(const viz::SurfaceId& surface_id) {
cc::Layer* OverlayWindowAndroid::GetLayerForTesting() {
return nullptr;
}

void OverlayWindowAndroid::MaybeNotifyVisibleActionsChanged() {
if (java_ref_.is_uninitialized())
return;

JNIEnv* env = base::android::AttachCurrentThread();
Java_PictureInPictureActivity_updateVisibleActions(
env, java_ref_.get(env),
base::android::ToJavaIntArray(
env,
std::vector<int>(visible_actions_.begin(), visible_actions_.end())));
}

bool OverlayWindowAndroid::MaybeUpdateVisibleAction(
const media_session::mojom::MediaSessionAction& action,
bool is_visible) {
int action_code = static_cast<int>(action);
if ((visible_actions_.find(action_code) != visible_actions_.end()) ==
is_visible) {
return false;
}

if (is_visible)
visible_actions_.insert(action_code);
else
visible_actions_.erase(action_code);

return true;
}
Loading

0 comments on commit 815cfd5

Please sign in to comment.