Commit 7bb74836 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

Refactor Android MediaSession UI code.

This is the first refactor that prepares the code for
componentization. After further refacrtoring, the next step
will be to relocate the relevant code and resources
to //components/browser_ui/media_session/.

This patch splits MediaSessionTabHelper into two classes,
separated by what is componentizable and what is Chrome
specific.

Most of this class is moved into a new class MediaSessionHelper,
which operates on a WebContents rather than a (Chrome) Tab.
MediaSessionTabHelper remains as a thin wrapper and implementor
of MediaSessionHelper.Delegate. The most substantive change is
moving functionality from TabObserver (in MediaSessionTabHelper)
to WebContentsObserver (in MediaSessionHelper).

Bug: 1066263
Change-Id: I9259a082686c0e0f6b9788bc15a19e887c0e95e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2204964
Commit-Queue: Evan Stade <estade@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Cr-Commit-Position: refs/heads/master@{#777374}
parent 7c3ba563
......@@ -900,9 +900,11 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/media/remote/RecordCastAction.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaImageCallback.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaImageManager.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaNotificationImageUtils.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaNotificationInfo.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaNotificationListener.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaNotificationManager.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaSessionHelper.java",
"java/src/org/chromium/chrome/browser/media/ui/MediaSessionTabHelper.java",
"java/src/org/chromium/chrome/browser/metrics/ActivityTabStartupMetricsTracker.java",
"java/src/org/chromium/chrome/browser/metrics/BackgroundTaskMemoryMetricsEmitter.java",
......
// 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.media.ui;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import androidx.annotation.Nullable;
import org.chromium.base.SysUtils;
/** A collection of utilities and constants for the images used in MediaSession notifications. */
public class MediaNotificationImageUtils {
static final int MINIMAL_MEDIA_IMAGE_SIZE_PX = 114;
// The media artwork image resolution on high-end devices.
private static final int HIGH_IMAGE_SIZE_PX = 512;
// The media artwork image resolution on high-end devices.
private static final int LOW_IMAGE_SIZE_PX = 256;
/**
* Downscale |icon| for display in the notification if needed. Returns null if |icon| is null.
* If |icon| is larger than {@link getIdealMediaImageSize()}, scale it down to
* {@link getIdealMediaImageSize()} and return. Otherwise return the original |icon|.
* @param icon The icon to be scaled.
*/
@Nullable
public static Bitmap downscaleIconToIdealSize(@Nullable Bitmap icon) {
if (icon == null) return null;
int targetSize = getIdealMediaImageSize();
Matrix m = new Matrix();
int dominantLength = Math.max(icon.getWidth(), icon.getHeight());
if (dominantLength < getIdealMediaImageSize()) return icon;
// Move the center to (0,0).
m.postTranslate(icon.getWidth() / -2.0f, icon.getHeight() / -2.0f);
// Scale to desired size.
float scale = 1.0f * targetSize / dominantLength;
m.postScale(scale, scale);
// Move to the desired place.
m.postTranslate(targetSize / 2.0f, targetSize / 2.0f);
// Draw the image.
Bitmap paddedBitmap = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(paddedBitmap);
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(icon, m, paint);
return paddedBitmap;
}
/**
* @return The ideal size of the media image.
*/
public static int getIdealMediaImageSize() {
return SysUtils.isLowEndDevice() ? LOW_IMAGE_SIZE_PX : HIGH_IMAGE_SIZE_PX;
}
/**
* @param icon The icon to be checked.
* @return Whether |icon| is suitable as the media image, i.e. bigger than the minimal size.
*/
public static boolean isBitmapSuitableAsMediaImage(Bitmap icon) {
return icon != null && icon.getWidth() >= MINIMAL_MEDIA_IMAGE_SIZE_PX
&& icon.getHeight() >= MINIMAL_MEDIA_IMAGE_SIZE_PX;
}
}
// 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.media.ui;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.SysUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.metrics.MediaNotificationUma;
import org.chromium.chrome.browser.metrics.MediaSessionUMA;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.services.media_session.MediaImage;
import org.chromium.services.media_session.MediaMetadata;
import org.chromium.services.media_session.MediaPosition;
import org.chromium.ui.base.WindowAndroid;
import java.util.List;
import java.util.Set;
/**
* Glue code that relays events from the {@link org.chromium.content.browser.MediaSession} for a
* WebContents to a delegate (ultimately, to {@link MediaNotificationController}).
*/
public class MediaSessionHelper implements MediaImageCallback {
private static final String TAG = "MediaSession";
private static final String UNICODE_PLAY_CHARACTER = "\u25B6";
@VisibleForTesting
static final int HIDE_NOTIFICATION_DELAY_MILLIS = 2500;
private Delegate mDelegate;
private WebContents mWebContents;
@VisibleForTesting
WebContentsObserver mWebContentsObserver;
@VisibleForTesting
MediaSessionObserver mMediaSessionObserver;
private MediaImageManager mMediaImageManager;
private Bitmap mPageMediaImage;
@VisibleForTesting
Bitmap mFavicon;
private Bitmap mCurrentMediaImage;
private String mOrigin;
private int mPreviousVolumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
private MediaNotificationInfo.Builder mNotificationInfoBuilder;
// The fallback title if |mPageMetadata| is null or its title is empty.
private String mFallbackTitle;
// Set to true if favicon update callback was called at least once.
private boolean mMaybeHasFavicon;
// The metadata set by the page.
private MediaMetadata mPageMetadata;
// The currently showing metadata.
private MediaMetadata mCurrentMetadata;
private Set<Integer> mMediaSessionActions;
private @Nullable MediaPosition mMediaPosition;
private Handler mHandler;
// The delayed task to hide notification. Hiding notification can be immediate or delayed.
// Delayed hiding will schedule this delayed task to |mHandler|. The task will be canceled when
// showing or immediate hiding.
private Runnable mHideNotificationDelayedTask;
// Used to override the MediaSession object get from WebContents. This is to work around the
// static getter {@link MediaSession#fromWebContents()}.
@VisibleForTesting
static MediaSession sOverriddenMediaSession;
@VisibleForTesting
@Nullable
MediaSessionObserver getMediaSessionObserverForTesting() {
return mMediaSessionObserver;
}
private MediaNotificationListener mControlsListener = new MediaNotificationListener() {
@Override
public void onPlay(int actionSource) {
if (isNotificationHidingOrHidden()) return;
MediaSessionUMA.recordPlay(
MediaSessionHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() == null) return;
mMediaSessionObserver.getMediaSession().resume();
}
@Override
public void onPause(int actionSource) {
if (isNotificationHidingOrHidden()) return;
MediaSessionUMA.recordPause(
MediaSessionHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() == null) return;
mMediaSessionObserver.getMediaSession().suspend();
}
@Override
public void onStop(int actionSource) {
if (isNotificationHidingOrHidden()) return;
MediaSessionUMA.recordStop(
MediaSessionHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() != null) {
mMediaSessionObserver.getMediaSession().stop();
}
}
@Override
public void onMediaSessionAction(int action) {
if (!MediaSessionAction.isKnownValue(action)) return;
if (mMediaSessionObserver != null) {
mMediaSessionObserver.getMediaSession().didReceiveAction(action);
}
}
@Override
public void onMediaSessionSeekTo(long pos) {
if (mMediaSessionObserver == null) return;
mMediaSessionObserver.getMediaSession().seekTo(pos);
}
};
private void hideNotificationDelayed() {
if (mWebContentsObserver == null) return;
if (mHideNotificationDelayedTask != null) return;
mHideNotificationDelayedTask = new Runnable() {
@Override
public void run() {
mHideNotificationDelayedTask = null;
hideNotificationInternal();
}
};
mHandler.postDelayed(mHideNotificationDelayedTask, HIDE_NOTIFICATION_DELAY_MILLIS);
mNotificationInfoBuilder = null;
mFavicon = null;
}
private void hideNotificationImmediately() {
if (mWebContentsObserver == null) return;
if (mHideNotificationDelayedTask != null) {
mHandler.removeCallbacks(mHideNotificationDelayedTask);
mHideNotificationDelayedTask = null;
}
hideNotificationInternal();
mNotificationInfoBuilder = null;
}
/**
* This method performs the common steps for hiding the notification. It should only be called
* by {@link #hideNotificationDelayed()} and {@link #hideNotificationImmediately()}.
*/
private void hideNotificationInternal() {
mDelegate.hideMediaNotification();
Activity activity = getActivity();
if (activity != null) {
activity.setVolumeControlStream(mPreviousVolumeControlStream);
}
}
private void showNotification() {
assert mNotificationInfoBuilder != null;
if (mHideNotificationDelayedTask != null) {
mHandler.removeCallbacks(mHideNotificationDelayedTask);
mHideNotificationDelayedTask = null;
}
mDelegate.showMediaNotification(mNotificationInfoBuilder.build());
}
private MediaSessionObserver createMediaSessionObserver(MediaSession mediaSession) {
return new MediaSessionObserver(mediaSession) {
@Override
public void mediaSessionDestroyed() {
hideNotificationImmediately();
cleanupMediaSessionObserver();
}
@Override
public void mediaSessionStateChanged(boolean isControllable, boolean isPaused) {
if (!isControllable) {
hideNotificationDelayed();
return;
}
Intent contentIntent = mDelegate.createBringTabToFrontIntent();
if (contentIntent != null) {
contentIntent.putExtra(MediaNotificationUma.INTENT_EXTRA_NAME,
MediaNotificationUma.Source.MEDIA);
}
if (mFallbackTitle == null) mFallbackTitle = sanitizeMediaTitle(mOrigin);
mCurrentMetadata = getMetadata();
mCurrentMediaImage = getCachedNotificationImage();
mNotificationInfoBuilder =
mDelegate.createMediaNotificationInfoBuilder()
.setMetadata(mCurrentMetadata)
.setPaused(isPaused)
.setOrigin(mOrigin)
.setPrivate(mWebContents.isIncognito())
.setNotificationSmallIcon(R.drawable.audio_playing)
.setNotificationLargeIcon(mCurrentMediaImage)
.setMediaSessionImage(mPageMediaImage)
.setActions(MediaNotificationInfo.ACTION_PLAY_PAUSE
| MediaNotificationInfo.ACTION_SWIPEAWAY)
.setContentIntent(contentIntent)
.setListener(mControlsListener)
.setMediaSessionActions(mMediaSessionActions)
.setMediaPosition(mMediaPosition);
// Show a default icon in incognito contents, as they don't show the media icon.
// Also show a default icon if we won't get a favicon from {@link mDelegate}. If the
// delegate will pass a favicon later, show nothing for now; we expect the favicon
// to arrive quickly.
if (mWebContents.isIncognito()
|| (mCurrentMediaImage == null && fetchLargeFaviconImage())) {
mNotificationInfoBuilder.setDefaultNotificationLargeIcon(
R.drawable.audio_playing_square);
}
showNotification();
Activity activity = getActivity();
if (activity != null) {
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
@Override
public void mediaSessionMetadataChanged(MediaMetadata metadata) {
mPageMetadata = metadata;
updateNotificationMetadata();
}
@Override
public void mediaSessionActionsChanged(Set<Integer> actions) {
mMediaSessionActions = actions;
updateNotificationActions();
}
@Override
public void mediaSessionArtworkChanged(List<MediaImage> images) {
mMediaImageManager.downloadImage(images, MediaSessionHelper.this);
updateNotificationMetadata();
}
@Override
public void mediaSessionPositionChanged(@Nullable MediaPosition position) {
mMediaPosition = position;
updateNotificationPosition();
}
};
}
void setWebContents(@NonNull WebContents webContents) {
mWebContents = webContents;
if (mWebContentsObserver != null) mWebContentsObserver.destroy();
mWebContentsObserver = new WebContentsObserver(webContents) {
@Override
public void didFinishNavigation(NavigationHandle navigation) {
if (!navigation.hasCommitted() || !navigation.isInMainFrame()
|| navigation.isSameDocument()) {
return;
}
mOrigin = UrlFormatter.formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(
webContents.getVisibleUrl().getOrigin().getSpec());
mFavicon = null;
mPageMediaImage = null;
mPageMetadata = null;
// |mCurrentMetadata| selects either |mPageMetadata| or |mFallbackTitle|. As
// there is no guarantee {@link #titleWasSet()} will be called before or
// after this method, |mFallbackTitle| is not reset in this callback, i.e.
// relying solely on
// {@link #titleWasSet()}. The following assignment is to keep
// |mCurrentMetadata| up to date as |mPageMetadata| may have changed.
mCurrentMetadata = getMetadata();
mMediaSessionActions = null;
if (isNotificationHidingOrHidden()) return;
mNotificationInfoBuilder.setOrigin(mOrigin);
mNotificationInfoBuilder.setNotificationLargeIcon(mFavicon);
mNotificationInfoBuilder.setMediaSessionImage(mPageMediaImage);
mNotificationInfoBuilder.setMetadata(mCurrentMetadata);
mNotificationInfoBuilder.setMediaSessionActions(mMediaSessionActions);
showNotification();
}
@Override
public void titleWasSet(String title) {
String newFallbackTitle = sanitizeMediaTitle(title);
if (!TextUtils.equals(mFallbackTitle, newFallbackTitle)) {
mFallbackTitle = newFallbackTitle;
updateNotificationMetadata();
}
}
@Override
public void wasShown() {
mDelegate.activateAndroidMediaSession();
}
};
MediaSession mediaSession = getMediaSession(webContents);
if (mMediaSessionObserver != null
&& mediaSession == mMediaSessionObserver.getMediaSession()) {
return;
}
cleanupMediaSessionObserver();
mMediaImageManager.setWebContents(webContents);
if (mediaSession != null) {
mMediaSessionObserver = createMediaSessionObserver(mediaSession);
}
}
private void cleanupMediaSessionObserver() {
if (mMediaSessionObserver == null) return;
mMediaSessionObserver.stopObserving();
mMediaSessionObserver = null;
mMediaSessionActions = null;
}
/** An interface for dispatching embedder-specific behavior. */
public interface Delegate {
/** Returns an intent that brings the associated web contents to the front. */
Intent createBringTabToFrontIntent();
/**
* Called to asynchronously fetch a larger favicon image.
*
* Normal, smaller favicons are passed in automatically. This call triggers lookup of a
* larger icon, which will also be passed in via {@link updateFavicon()}, or not at all if
* this method returns false.
* @return true if the favicon will be updated.
*/
boolean fetchLargeFaviconImage();
/**
* Creates a {@link MediaNotificationInfo.Builder} with basic embedder-specific
* initialization.
*/
public MediaNotificationInfo.Builder createMediaNotificationInfoBuilder();
/** Shows a notification with the given metadata. */
void showMediaNotification(MediaNotificationInfo notificationInfo);
/** Hides the active notification. */
void hideMediaNotification();
/** Activates the Android MediaSession. */
void activateAndroidMediaSession();
}
MediaSessionHelper(@NonNull WebContents webContents, @NonNull Delegate delegate) {
mDelegate = delegate;
mMediaImageManager =
new MediaImageManager(MediaNotificationImageUtils.MINIMAL_MEDIA_IMAGE_SIZE_PX,
MediaNotificationImageUtils.getIdealMediaImageSize());
mHandler = new Handler();
setWebContents(webContents);
Activity activity = getActivity();
if (activity != null) {
mPreviousVolumeControlStream = activity.getVolumeControlStream();
}
}
/**
* Called when this object should no longer manage a media session because owning code no longer
* requires it.
*/
void destroy() {
cleanupMediaSessionObserver();
hideNotificationImmediately();
if (mWebContentsObserver != null) mWebContentsObserver.destroy();
mWebContentsObserver = null;
}
/**
* Removes all the leading/trailing white spaces and the quite common unicode play character.
* It improves the visibility of the title in the notification.
*
* @param title The original tab title, e.g. " ▶ Foo - Bar "
* @return The sanitized tab title, e.g. "Foo - Bar"
*/
private String sanitizeMediaTitle(String title) {
title = title.trim();
return title.startsWith(UNICODE_PLAY_CHARACTER) ? title.substring(1).trim() : title;
}
/**
* Converts the {@link MediaNotificationListener} action source enum into the
* {@link MediaSessionUMA} one to ensure matching the histogram values.
* @param source the source id, must be one of the ACTION_SOURCE_* constants defined in the
* {@link MediaNotificationListener} interface.
* @return the corresponding histogram value.
*/
public static @MediaSessionUMA.MediaSessionActionSource int convertMediaActionSourceToUMA(
int source) {
if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION) {
return MediaSessionUMA.MediaSessionActionSource.MEDIA_NOTIFICATION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION) {
return MediaSessionUMA.MediaSessionActionSource.MEDIA_SESSION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_HEADSET_UNPLUG) {
return MediaSessionUMA.MediaSessionActionSource.HEADSET_UNPLUG;
}
assert false;
return MediaSessionUMA.MediaSessionActionSource.NUM_ENTRIES;
}
private Activity getActivity() {
assert mWebContents != null;
WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();
if (windowAndroid == null) return null;
return windowAndroid.getActivity().get();
}
/** Returns true if a large favicon might be found. */
boolean fetchLargeFaviconImage() {
// The page does not have a favicon yet to fetch since onFaviconUpdated was never called.
// Don't waste time trying to find it.
if (!mMaybeHasFavicon) return false;
return mDelegate.fetchLargeFaviconImage();
}
/**
* Updates the best favicon if the given icon is better and the favicon is shown in
* notification.
*/
void updateFavicon(Bitmap icon) {
if (icon == null) return;
mMaybeHasFavicon = true;
// Store the favicon only if notification is being shown. Otherwise the favicon is
// obtained from large icon bridge when needed.
if (isNotificationHidingOrHidden() || mPageMediaImage != null) return;
// Disable favicons in notifications for low memory devices on O
// where the notification icon is optional.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && SysUtils.isLowEndDevice()) return;
if (!MediaNotificationImageUtils.isBitmapSuitableAsMediaImage(icon)) return;
if (mFavicon != null
&& (icon.getWidth() < mFavicon.getWidth()
|| icon.getHeight() < mFavicon.getHeight())) {
return;
}
mFavicon = MediaNotificationImageUtils.downscaleIconToIdealSize(icon);
updateNotificationImage(mFavicon);
}
/** Sets an icon which will preferentially be used in place of a smaller favicon. */
void setLargeIcon(Bitmap icon) {
if (isNotificationHidingOrHidden()) return;
if (icon == null) {
// If we do not have any favicon then make sure we show default sound icon. This
// icon is used by notification manager only if we do not show any icon.
mNotificationInfoBuilder.setDefaultNotificationLargeIcon(
R.drawable.audio_playing_square);
showNotification();
} else {
updateFavicon(icon);
}
}
/**
* Updates the metadata in media notification. This method should be called whenever
* |mPageMetadata| or |mFallbackTitle| is changed.
*/
private void updateNotificationMetadata() {
if (isNotificationHidingOrHidden()) return;
MediaMetadata newMetadata = getMetadata();
if (mCurrentMetadata.equals(newMetadata)) return;
mCurrentMetadata = newMetadata;
mNotificationInfoBuilder.setMetadata(mCurrentMetadata);
showNotification();
}
/**
* @return The up-to-date MediaSession metadata. Returns the cached object like |mPageMetadata|
* or |mCurrentMetadata| if it reflects the current state. Otherwise will return a new
* {@link MediaMetadata} object.
*/
private MediaMetadata getMetadata() {
String title = mFallbackTitle;
String artist = "";
String album = "";
if (mPageMetadata != null) {
if (!TextUtils.isEmpty(mPageMetadata.getTitle())) return mPageMetadata;
artist = mPageMetadata.getArtist();
album = mPageMetadata.getAlbum();
}
if (mCurrentMetadata != null && TextUtils.equals(title, mCurrentMetadata.getTitle())
&& TextUtils.equals(artist, mCurrentMetadata.getArtist())
&& TextUtils.equals(album, mCurrentMetadata.getAlbum())) {
return mCurrentMetadata;
}
return new MediaMetadata(title, artist, album);
}
private void updateNotificationActions() {
if (isNotificationHidingOrHidden()) return;
mNotificationInfoBuilder.setMediaSessionActions(mMediaSessionActions);
showNotification();
}
private void updateNotificationPosition() {
if (isNotificationHidingOrHidden()) return;
mNotificationInfoBuilder.setMediaPosition(mMediaPosition);
showNotification();
}
@Override
public void onImageDownloaded(Bitmap image) {
mPageMediaImage = MediaNotificationImageUtils.downscaleIconToIdealSize(image);
mFavicon = null;
updateNotificationImage(mPageMediaImage);
}
private void updateNotificationImage(Bitmap newMediaImage) {
if (mCurrentMediaImage == newMediaImage) return;
mCurrentMediaImage = newMediaImage;
if (isNotificationHidingOrHidden()) return;
mNotificationInfoBuilder.setNotificationLargeIcon(mCurrentMediaImage);
mNotificationInfoBuilder.setMediaSessionImage(mPageMediaImage);
showNotification();
}
private Bitmap getCachedNotificationImage() {
if (mPageMediaImage != null) return mPageMediaImage;
if (mFavicon != null) return mFavicon;
return null;
}
private boolean isNotificationHidingOrHidden() {
return mNotificationInfoBuilder == null;
}
private MediaSession getMediaSession(WebContents contents) {
return (sOverriddenMediaSession != null) ? sOverriddenMediaSession
: MediaSession.fromWebContents(contents);
}
}
......@@ -4,371 +4,50 @@
package org.chromium.chrome.browser.media.ui;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.SysUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.document.ChromeIntentUtil;
import org.chromium.chrome.browser.metrics.MediaNotificationUma;
import org.chromium.chrome.browser.metrics.MediaSessionUMA;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.ui.favicon.LargeIconBridge;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.net.GURLUtils;
import org.chromium.services.media_session.MediaImage;
import org.chromium.services.media_session.MediaMetadata;
import org.chromium.services.media_session.MediaPosition;
import org.chromium.ui.base.WindowAndroid;
import java.util.List;
import java.util.Set;
/**
* A tab helper responsible for enabling/disabling media controls and passing
* media actions from the controls to the {@link org.chromium.content.browser.MediaSession}
* A tab helper that wraps {@link MediaSessionHelper} and is responsible for Chrome-specific
* behavior.
*/
public class MediaSessionTabHelper implements MediaImageCallback {
private static final String TAG = "MediaSession";
private static final String UNICODE_PLAY_CHARACTER = "\u25B6";
@VisibleForTesting
static final int HIDE_NOTIFICATION_DELAY_MILLIS = 2500;
public class MediaSessionTabHelper implements MediaSessionHelper.Delegate {
private Tab mTab;
@VisibleForTesting
LargeIconBridge mLargeIconBridge;
private Bitmap mPageMediaImage;
@VisibleForTesting
Bitmap mFavicon;
// Set to true if favicon update callback was called at least once for the current tab.
private boolean mMaybeHasFavicon;
private Bitmap mCurrentMediaImage;
private String mOrigin;
MediaSessionHelper mMediaSessionHelper;
@VisibleForTesting
MediaSessionObserver mMediaSessionObserver;
private int mPreviousVolumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
private MediaNotificationInfo.Builder mNotificationInfoBuilder;
// The fallback title if |mPageMetadata| is null or its title is empty.
private String mFallbackTitle;
// The metadata set by the page.
private MediaMetadata mPageMetadata;
// The currently showing metadata.
private MediaMetadata mCurrentMetadata;
private MediaImageManager mMediaImageManager;
private Set<Integer> mMediaSessionActions;
private @Nullable MediaPosition mMediaPosition;
private Handler mHandler;
// The delayed task to hide notification. Hiding notification can be immediate or delayed.
// Delayed hiding will schedule this delayed task to |mHandler|. The task will be canceled when
// showing or immediate hiding.
private Runnable mHideNotificationDelayedTask;
// Used to override the MediaSession object get from WebContents. This is to work around the
// static getter {@link MediaSession#fromWebContents()}.
@VisibleForTesting
static MediaSession sOverriddenMediaSession;
@VisibleForTesting
@Nullable
MediaSessionObserver getMediaSessionObserverForTesting() {
return mMediaSessionObserver;
}
private MediaNotificationListener mControlsListener = new MediaNotificationListener() {
@Override
public void onPlay(int actionSource) {
if (isNotificationHiddingOrHidden()) return;
MediaSessionUMA.recordPlay(
MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() == null) return;
mMediaSessionObserver.getMediaSession().resume();
}
@Override
public void onPause(int actionSource) {
if (isNotificationHiddingOrHidden()) return;
MediaSessionUMA.recordPause(
MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() == null) return;
mMediaSessionObserver.getMediaSession().suspend();
}
@Override
public void onStop(int actionSource) {
if (isNotificationHiddingOrHidden()) return;
MediaSessionUMA.recordStop(
MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
if (mMediaSessionObserver.getMediaSession() != null) {
mMediaSessionObserver.getMediaSession().stop();
}
}
@Override
public void onMediaSessionAction(int action) {
if (!MediaSessionAction.isKnownValue(action)) return;
if (mMediaSessionObserver != null) {
mMediaSessionObserver.getMediaSession().didReceiveAction(action);
}
}
@Override
public void onMediaSessionSeekTo(long pos) {
if (mMediaSessionObserver == null) return;
mMediaSessionObserver.getMediaSession().seekTo(pos);
}
};
private void hideNotificationDelayed() {
if (mTab == null) return;
if (mHideNotificationDelayedTask != null) return;
mHideNotificationDelayedTask = new Runnable() {
@Override
public void run() {
mHideNotificationDelayedTask = null;
hideNotificationInternal();
}
};
mHandler.postDelayed(mHideNotificationDelayedTask, HIDE_NOTIFICATION_DELAY_MILLIS);
mNotificationInfoBuilder = null;
mFavicon = null;
}
private void hideNotificationImmediately() {
if (mTab == null) return;
if (mHideNotificationDelayedTask != null) {
mHandler.removeCallbacks(mHideNotificationDelayedTask);
mHideNotificationDelayedTask = null;
}
hideNotificationInternal();
mNotificationInfoBuilder = null;
}
/**
* This method performs the common steps for hiding the notification. It should only be called
* by {@link #hideNotificationDelayed()} and {@link #hideNotificationImmediately()}.
*/
private void hideNotificationInternal() {
MediaNotificationManager.hide(mTab.getId(), R.id.media_playback_notification);
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
activity.setVolumeControlStream(mPreviousVolumeControlStream);
}
}
private void showNotification() {
assert mNotificationInfoBuilder != null;
if (mHideNotificationDelayedTask != null) {
mHandler.removeCallbacks(mHideNotificationDelayedTask);
mHideNotificationDelayedTask = null;
}
MediaNotificationManager.show(mNotificationInfoBuilder.build());
}
private MediaSessionObserver createMediaSessionObserver(MediaSession mediaSession) {
return new MediaSessionObserver(mediaSession) {
@Override
public void mediaSessionDestroyed() {
hideNotificationImmediately();
cleanupMediaSessionObserver();
}
@Override
public void mediaSessionStateChanged(boolean isControllable, boolean isPaused) {
if (!isControllable) {
hideNotificationDelayed();
return;
}
Intent contentIntent = ChromeIntentUtil.createBringTabToFrontIntent(mTab.getId());
if (contentIntent != null) {
contentIntent.putExtra(MediaNotificationUma.INTENT_EXTRA_NAME,
MediaNotificationUma.Source.MEDIA);
}
if (mFallbackTitle == null) mFallbackTitle = sanitizeMediaTitle(mTab.getTitle());
mCurrentMetadata = getMetadata();
mCurrentMediaImage = getCachedNotificationImage();
mNotificationInfoBuilder =
new MediaNotificationInfo.Builder()
.setMetadata(mCurrentMetadata)
.setPaused(isPaused)
.setOrigin(mOrigin)
.setTabId(mTab.getId())
.setPrivate(mTab.isIncognito())
.setNotificationSmallIcon(R.drawable.audio_playing)
.setNotificationLargeIcon(mCurrentMediaImage)
.setMediaSessionImage(mPageMediaImage)
.setActions(MediaNotificationInfo.ACTION_PLAY_PAUSE
| MediaNotificationInfo.ACTION_SWIPEAWAY)
.setContentIntent(contentIntent)
.setId(R.id.media_playback_notification)
.setListener(mControlsListener)
.setMediaSessionActions(mMediaSessionActions)
.setMediaPosition(mMediaPosition);
// Do not show notification icon till we get the favicon from the LargeIconBridge
// since we do not need to show default icon then change it to favicon. It is ok to
// wait here since the favicon is loaded from local cache in favicon service sql
// database.
// Incognito Tabs need the default icon as they don't show the media icon.
if (mTab.isIncognito() || (mCurrentMediaImage == null && !fetchFaviconImage())) {
mNotificationInfoBuilder.setDefaultNotificationLargeIcon(
R.drawable.audio_playing_square);
}
showNotification();
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
@Override
public void mediaSessionMetadataChanged(MediaMetadata metadata) {
mPageMetadata = metadata;
updateNotificationMetadata();
}
@Override
public void mediaSessionActionsChanged(Set<Integer> actions) {
mMediaSessionActions = actions;
updateNotificationActions();
}
@Override
public void mediaSessionArtworkChanged(List<MediaImage> images) {
mMediaImageManager.downloadImage(images, MediaSessionTabHelper.this);
updateNotificationMetadata();
}
@Override
public void mediaSessionPositionChanged(@Nullable MediaPosition position) {
mMediaPosition = position;
updateNotificationPosition();
}
};
}
private void setWebContents(WebContents webContents) {
MediaSession mediaSession = getMediaSession(webContents);
if (mMediaSessionObserver != null
&& mediaSession == mMediaSessionObserver.getMediaSession()) {
return;
}
cleanupMediaSessionObserver();
mMediaImageManager.setWebContents(webContents);
if (mediaSession != null) {
mMediaSessionObserver = createMediaSessionObserver(mediaSession);
}
}
private void cleanupMediaSessionObserver() {
if (mMediaSessionObserver == null) return;
mMediaSessionObserver.stopObserving();
mMediaSessionObserver = null;
mMediaSessionActions = null;
}
LargeIconBridge mLargeIconBridge;
@VisibleForTesting
final TabObserver mTabObserver = new EmptyTabObserver() {
@Override
public void onContentChanged(Tab tab) {
assert tab == mTab;
setWebContents(tab.getWebContents());
maybeCreateOrUpdateMediaSessionHelper();
}
@Override
public void onFaviconUpdated(Tab tab, Bitmap icon) {
assert tab == mTab;
updateFavicon(icon);
}
@Override
public void onDidFinishNavigation(Tab tab, NavigationHandle navigation) {
assert tab == mTab;
if (!navigation.hasCommitted() || !navigation.isInMainFrame()
|| navigation.isSameDocument()) {
return;
}
mOrigin = UrlFormatter.formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(
GURLUtils.getOrigin(mTab.getUrlString()));
mFavicon = null;
mPageMediaImage = null;
mPageMetadata = null;
// |mCurrentMetadata| selects either |mPageMetadata| or |mFallbackTitle|. As there is no
// guarantee {@link #onTitleUpdated()} will be called before or after this method,
// |mFallbackTitle| is not reset in this callback, i.e. relying solely on
// {@link #onTitleUpdated()}. The following assignment is to keep |mCurrentMetadata| up
// to date as |mPageMetadata| may have changed.
mCurrentMetadata = getMetadata();
mMediaSessionActions = null;
if (isNotificationHiddingOrHidden()) return;
mNotificationInfoBuilder.setOrigin(mOrigin);
mNotificationInfoBuilder.setNotificationLargeIcon(mFavicon);
mNotificationInfoBuilder.setMediaSessionImage(mPageMediaImage);
mNotificationInfoBuilder.setMetadata(mCurrentMetadata);
mNotificationInfoBuilder.setMediaSessionActions(mMediaSessionActions);
showNotification();
}
@Override
public void onTitleUpdated(Tab tab) {
assert tab == mTab;
String newFallbackTitle = sanitizeMediaTitle(tab.getTitle());
if (!TextUtils.equals(mFallbackTitle, newFallbackTitle)) {
mFallbackTitle = newFallbackTitle;
updateNotificationMetadata();
}
}
@Override
public void onShown(Tab tab, @TabSelectionType int type) {
assert tab == mTab;
MediaNotificationManager.activateAndroidMediaSession(
tab.getId(), R.id.media_playback_notification);
mMediaSessionHelper.updateFavicon(icon);
}
@Override
public void onDestroyed(Tab tab) {
assert mTab == tab;
cleanupMediaSessionObserver();
hideNotificationImmediately();
if (mMediaSessionHelper != null) mMediaSessionHelper.destroy();
mTab.removeObserver(this);
mTab = null;
if (mLargeIconBridge != null) {
......@@ -382,16 +61,15 @@ public class MediaSessionTabHelper implements MediaImageCallback {
MediaSessionTabHelper(Tab tab) {
mTab = tab;
mTab.addObserver(mTabObserver);
mMediaImageManager =
new MediaImageManager(MediaNotificationManager.MINIMAL_MEDIA_IMAGE_SIZE_PX,
MediaNotificationManager.getIdealMediaImageSize());
if (mTab.getWebContents() != null) setWebContents(tab.getWebContents());
maybeCreateOrUpdateMediaSessionHelper();
}
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
mPreviousVolumeControlStream = activity.getVolumeControlStream();
private void maybeCreateOrUpdateMediaSessionHelper() {
if (mMediaSessionHelper != null) {
mMediaSessionHelper.setWebContents(mTab.getWebContents());
} else if (mTab.getWebContents() != null) {
mMediaSessionHelper = new MediaSessionHelper(mTab.getWebContents(), this);
}
mHandler = new Handler();
}
/**
......@@ -402,193 +80,50 @@ public class MediaSessionTabHelper implements MediaImageCallback {
new MediaSessionTabHelper(tab);
}
/**
* Removes all the leading/trailing white spaces and the quite common unicode play character.
* It improves the visibility of the title in the notification.
*
* @param title The original tab title, e.g. " ▶ Foo - Bar "
* @return The sanitized tab title, e.g. "Foo - Bar"
*/
private String sanitizeMediaTitle(String title) {
title = title.trim();
return title.startsWith(UNICODE_PLAY_CHARACTER) ? title.substring(1).trim() : title;
}
/**
* Converts the {@link MediaNotificationListener} action source enum into the
* {@link MediaSessionUMA} one to ensure matching the histogram values.
* @param source the source id, must be one of the ACTION_SOURCE_* constants defined in the
* {@link MediaNotificationListener} interface.
* @return the corresponding histogram value.
*/
public static @MediaSessionUMA.MediaSessionActionSource int convertMediaActionSourceToUMA(
int source) {
if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION) {
return MediaSessionUMA.MediaSessionActionSource.MEDIA_NOTIFICATION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION) {
return MediaSessionUMA.MediaSessionActionSource.MEDIA_SESSION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_HEADSET_UNPLUG) {
return MediaSessionUMA.MediaSessionActionSource.HEADSET_UNPLUG;
}
assert false;
return MediaSessionUMA.MediaSessionActionSource.NUM_ENTRIES;
}
private Activity getActivityFromTab(Tab tab) {
WindowAndroid windowAndroid = tab.getWindowAndroid();
if (windowAndroid == null) return null;
return windowAndroid.getActivity().get();
}
/**
* Updates the best favicon if the given icon is better and the favicon is shown in
* notification.
*/
private void updateFavicon(Bitmap icon) {
if (icon == null) return;
mMaybeHasFavicon = true;
// Store the favicon only if notification is being shown. Otherwise the favicon is
// obtained from large icon bridge when needed.
if (isNotificationHiddingOrHidden() || mPageMediaImage != null) return;
// Disable favicons in notifications for low memory devices on O
// where the notification icon is optional.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && SysUtils.isLowEndDevice()) return;
if (!MediaNotificationManager.isBitmapSuitableAsMediaImage(icon)) return;
if (mFavicon != null && (icon.getWidth() < mFavicon.getWidth()
|| icon.getHeight() < mFavicon.getHeight())) {
return;
}
mFavicon = MediaNotificationManager.downscaleIconToIdealSize(icon);
updateNotificationImage(mFavicon);
}
/**
* Updates the metadata in media notification. This method should be called whenever
* |mPageMetadata| or |mFallbackTitle| is changed.
*/
private void updateNotificationMetadata() {
if (isNotificationHiddingOrHidden()) return;
MediaMetadata newMetadata = getMetadata();
if (mCurrentMetadata.equals(newMetadata)) return;
mCurrentMetadata = newMetadata;
mNotificationInfoBuilder.setMetadata(mCurrentMetadata);
showNotification();
}
/**
* @return The up-to-date MediaSession metadata. Returns the cached object like |mPageMetadata|
* or |mCurrentMetadata| if it reflects the current state. Otherwise will return a new
* {@link MediaMetadata} object.
*/
private MediaMetadata getMetadata() {
String title = mFallbackTitle;
String artist = "";
String album = "";
if (mPageMetadata != null) {
if (!TextUtils.isEmpty(mPageMetadata.getTitle())) return mPageMetadata;
artist = mPageMetadata.getArtist();
album = mPageMetadata.getAlbum();
}
if (mCurrentMetadata != null && TextUtils.equals(title, mCurrentMetadata.getTitle())
&& TextUtils.equals(artist, mCurrentMetadata.getArtist())
&& TextUtils.equals(album, mCurrentMetadata.getAlbum())) {
return mCurrentMetadata;
}
return new MediaMetadata(title, artist, album);
}
private void updateNotificationActions() {
if (isNotificationHiddingOrHidden()) return;
mNotificationInfoBuilder.setMediaSessionActions(mMediaSessionActions);
showNotification();
}
private void updateNotificationPosition() {
if (isNotificationHiddingOrHidden()) return;
mNotificationInfoBuilder.setMediaPosition(mMediaPosition);
showNotification();
}
@Override
public void onImageDownloaded(Bitmap image) {
mPageMediaImage = MediaNotificationManager.downscaleIconToIdealSize(image);
mFavicon = null;
updateNotificationImage(mPageMediaImage);
public Intent createBringTabToFrontIntent() {
return ChromeIntentUtil.createBringTabToFrontIntent(mTab.getId());
}
private void updateNotificationImage(Bitmap newMediaImage) {
if (mCurrentMediaImage == newMediaImage) return;
mCurrentMediaImage = newMediaImage;
if (isNotificationHiddingOrHidden()) return;
mNotificationInfoBuilder.setNotificationLargeIcon(mCurrentMediaImage);
mNotificationInfoBuilder.setMediaSessionImage(mPageMediaImage);
showNotification();
}
private Bitmap getCachedNotificationImage() {
if (mPageMediaImage != null) return mPageMediaImage;
if (mFavicon != null) return mFavicon;
return null;
}
/**
* Fetch favicon image and update the notification when available.
* @return if the favicon will be updated.
*/
private boolean fetchFaviconImage() {
// The page does not have a favicon yet to fetch since onFaviconUpdated was never called.
// Don't waste time trying to find it.
if (!mMaybeHasFavicon) return false;
if (mTab == null) return false;
@Override
public boolean fetchLargeFaviconImage() {
WebContents webContents = mTab.getWebContents();
if (webContents == null) return false;
String pageUrl = webContents.getLastCommittedUrl();
int size = MediaNotificationManager.MINIMAL_MEDIA_IMAGE_SIZE_PX;
int size = MediaNotificationImageUtils.MINIMAL_MEDIA_IMAGE_SIZE_PX;
if (mLargeIconBridge == null) {
mLargeIconBridge = new LargeIconBridge(Profile.fromWebContents(mTab.getWebContents()));
mLargeIconBridge = new LargeIconBridge(Profile.fromWebContents(webContents));
}
LargeIconBridge.LargeIconCallback callback = new LargeIconBridge.LargeIconCallback() {
@Override
public void onLargeIconAvailable(
Bitmap icon, int fallbackColor, boolean isFallbackColorDefault, int iconType) {
if (isNotificationHiddingOrHidden()) return;
if (icon == null) {
// If we do not have any favicon then make sure we show default sound icon. This
// icon is used by notification manager only if we do not show any icon.
mNotificationInfoBuilder.setDefaultNotificationLargeIcon(
R.drawable.audio_playing_square);
showNotification();
} else {
updateFavicon(icon);
}
mMediaSessionHelper.setLargeIcon(icon);
}
};
return mLargeIconBridge.getLargeIconForStringUrl(pageUrl, size, callback);
}
private boolean isNotificationHiddingOrHidden() {
return mNotificationInfoBuilder == null;
@Override
public MediaNotificationInfo.Builder createMediaNotificationInfoBuilder() {
return new MediaNotificationInfo.Builder()
.setTabId(mTab.getId())
.setId(R.id.media_playback_notification);
}
@Override
public void showMediaNotification(MediaNotificationInfo notificationInfo) {
MediaNotificationManager.show(notificationInfo);
}
private MediaSession getMediaSession(WebContents contents) {
return (sOverriddenMediaSession != null) ? sOverriddenMediaSession
: MediaSession.fromWebContents(contents);
@Override
public void hideMediaNotification() {
MediaNotificationManager.hide(mTab.getId(), R.id.media_playback_notification);
}
@Override
public void activateAndroidMediaSession() {
MediaNotificationManager.activateAndroidMediaSession(
mTab.getId(), R.id.media_playback_notification);
}
}
......@@ -157,7 +157,7 @@ public class MediaNotificationFaviconTest extends MediaNotificationManagerTestBa
}
private Bitmap getDisplayedIcon() {
return mTabHolder.mMediaSessionTabHelper.mFavicon;
return mTabHolder.mMediaSessionTabHelper.mMediaSessionHelper.mFavicon;
}
@Override
......
......@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.media.ui;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.graphics.Bitmap;
......@@ -24,6 +25,7 @@ import org.chromium.media_session.mojom.MediaSessionAction;
import org.chromium.net.GURLUtils;
import org.chromium.net.GURLUtilsJni;
import org.chromium.services.media_session.MediaMetadata;
import org.chromium.url.GURL;
import java.util.Set;
import java.util.stream.Collectors;
......@@ -73,11 +75,9 @@ public class MediaNotificationTestTabHolder {
when(mTab.getWebContents()).thenReturn(mWebContents);
when(mTab.getId()).thenReturn(tabId);
when(mTab.isIncognito()).thenReturn(false);
when(mTab.getTitle()).thenAnswer(invocation -> mTitle);
when(mTab.getUrlString()).thenAnswer(invocation -> mUrl);
when(mWebContents.isIncognito()).thenReturn(false);
MediaSessionTabHelper.sOverriddenMediaSession = mMediaSession;
MediaSessionHelper.sOverriddenMediaSession = mMediaSession;
mMediaSessionTabHelper = new MediaSessionTabHelper(mTab);
mMediaSessionTabHelper.mLargeIconBridge = new TestLargeIconBridge();
......@@ -91,7 +91,7 @@ public class MediaNotificationTestTabHolder {
public void simulateTitleUpdated(String title) {
mTitle = title;
mMediaSessionTabHelper.mTabObserver.onTitleUpdated(mTab);
mMediaSessionTabHelper.mMediaSessionHelper.mWebContentsObserver.titleWasSet(title);
}
public void simulateFaviconUpdated(Bitmap icon) {
......@@ -99,29 +99,40 @@ public class MediaNotificationTestTabHolder {
}
public void simulateMediaSessionStateChanged(boolean isControllable, boolean isSuspended) {
mMediaSessionTabHelper.mMediaSessionObserver.mediaSessionStateChanged(
mMediaSessionTabHelper.mMediaSessionHelper.mMediaSessionObserver.mediaSessionStateChanged(
isControllable, isSuspended);
}
public void simulateMediaSessionMetadataChanged(MediaMetadata metadata) {
mMediaSessionTabHelper.mMediaSessionObserver.mediaSessionMetadataChanged(metadata);
mMediaSessionTabHelper.mMediaSessionHelper.mMediaSessionObserver
.mediaSessionMetadataChanged(metadata);
}
public void simulateMediaSessionActionsChanged(Set<Integer> actions) {
mMediaSessionTabHelper.mMediaSessionObserver.mediaSessionActionsChanged(actions);
mMediaSessionTabHelper.mMediaSessionHelper.mMediaSessionObserver.mediaSessionActionsChanged(
actions);
}
public void simulateNavigation(String url, boolean isSameDocument) {
mUrl = url;
// The following hoop jumping is necessary because loading real GURLs fails under junit.
GURL gurl = mock(GURL.class);
when(mWebContents.getVisibleUrl()).thenAnswer(invocation -> gurl);
GURL gurlOrigin = mock(GURL.class);
when(gurl.getOrigin()).thenAnswer(invocation -> gurlOrigin);
when(gurlOrigin.getSpec()).thenAnswer(invocation -> url);
NavigationHandle navigation = new NavigationHandle(0 /* navigationHandleProxy */, url,
true /* isInMainFrame */, isSameDocument, false /* isRendererInitiated */);
mMediaSessionTabHelper.mTabObserver.onDidStartNavigation(mTab, navigation);
mMediaSessionTabHelper.mMediaSessionHelper.mWebContentsObserver.didStartNavigation(
navigation);
navigation.didFinish(url, false /* isErrorPage */, true /* hasCommitted */,
false /* isFragmentNavigation */, false /* isDownload */,
false /* isValidSearchFormUrl */, 0 /* pageTransition */, 0 /* errorCode */,
200 /* httpStatusCode */);
mMediaSessionTabHelper.mTabObserver.onDidFinishNavigation(mTab, navigation);
mMediaSessionTabHelper.mMediaSessionHelper.mWebContentsObserver.didFinishNavigation(
navigation);
}
}
......@@ -40,7 +40,7 @@ public class MediaNotificationTitleUpdatedTest extends MediaNotificationManagerT
private static final int TAB_ID_2 = 2;
private static final int THROTTLE_MILLIS = MediaNotificationManager.Throttler.THROTTLE_MILLIS;
private static final int HIDE_NOTIFICATION_DELAY_MILLIS =
MediaSessionTabHelper.HIDE_NOTIFICATION_DELAY_MILLIS;
MediaSessionHelper.HIDE_NOTIFICATION_DELAY_MILLIS;
private MediaNotificationTestTabHolder mTabHolder;
......
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