// Copyright 2015 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.text.TextUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.metrics.MediaNotificationUma;
import org.chromium.chrome.browser.metrics.MediaSessionUMA;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.common.MediaMetadata;
import org.chromium.ui.base.WindowAndroid;
import java.net.URI;
import java.net.URISyntaxException;
/**
* A tab helper responsible for enabling/disabling media controls and passing
* media actions from the controls to the {@link org.chromium.content.browser.MediaSession}
*/
public class MediaSessionTabHelper {
private static final String TAG = "MediaSession";
private static final String UNICODE_PLAY_CHARACTER = "\u25B6";
private static final int MINIMAL_FAVICON_SIZE = 114;
private Tab mTab;
private Bitmap mFavicon = null;
private String mOrigin = null;
private WebContents mWebContents;
private WebContentsObserver mWebContentsObserver;
private int mPreviousVolumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE;
private MediaNotificationInfo.Builder mNotificationInfoBuilder = null;
private MediaMetadata mFallbackMetadata;
private MediaNotificationListener mControlsListener = new MediaNotificationListener() {
@Override
public void onPlay(int actionSource) {
MediaSessionUMA
.recordPlay(MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
mWebContents.resumeMediaSession();
}
@Override
public void onPause(int actionSource) {
MediaSessionUMA.recordPause(
MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
mWebContents.suspendMediaSession();
}
@Override
public void onStop(int actionSource) {
MediaSessionUMA
.recordStop(MediaSessionTabHelper.convertMediaActionSourceToUMA(actionSource));
mWebContents.stopMediaSession();
}
};
void hideNotification() {
if (mTab == null) {
return;
}
MediaNotificationManager.hide(mTab.getId(), R.id.media_playback_notification);
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
activity.setVolumeControlStream(mPreviousVolumeControlStream);
}
mNotificationInfoBuilder = null;
}
private WebContentsObserver createWebContentsObserver(WebContents webContents) {
return new WebContentsObserver(webContents) {
@Override
public void destroy() {
hideNotification();
super.destroy();
}
@Override
public void mediaSessionStateChanged(boolean isControllable, boolean isPaused,
MediaMetadata metadata) {
if (!isControllable) {
hideNotification();
return;
}
mFallbackMetadata = null;
// The page's title is used as a placeholder if no title is specified in the
// metadata.
if (metadata == null || TextUtils.isEmpty(metadata.getTitle())) {
mFallbackMetadata = new MediaMetadata(
sanitizeMediaTitle(mTab.getTitle()),
metadata == null ? "" : metadata.getArtist(),
metadata == null ? "" : metadata.getAlbum());
metadata = mFallbackMetadata;
}
Intent contentIntent = Tab.createBringTabToFrontIntent(mTab.getId());
if (contentIntent != null) {
contentIntent.putExtra(MediaNotificationUma.INTENT_EXTRA_NAME,
MediaNotificationUma.SOURCE_MEDIA);
}
mNotificationInfoBuilder =
new MediaNotificationInfo.Builder()
.setMetadata(metadata)
.setPaused(isPaused)
.setOrigin(mOrigin)
.setTabId(mTab.getId())
.setPrivate(mTab.isIncognito())
.setIcon(R.drawable.audio_playing)
.setLargeIcon(mFavicon)
.setDefaultLargeIcon(R.drawable.audio_playing_square)
.setActions(MediaNotificationInfo.ACTION_PLAY_PAUSE
| MediaNotificationInfo.ACTION_SWIPEAWAY)
.setContentIntent(contentIntent)
.setId(R.id.media_playback_notification)
.setListener(mControlsListener);
MediaNotificationManager.show(ContextUtils.getApplicationContext(),
mNotificationInfoBuilder.build());
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
};
}
private void setWebContents(WebContents webContents) {
if (mWebContents == webContents) return;
cleanupWebContents();
mWebContents = webContents;
if (mWebContents != null) mWebContentsObserver = createWebContentsObserver(mWebContents);
}
private void cleanupWebContents() {
if (mWebContentsObserver != null) mWebContentsObserver.destroy();
mWebContentsObserver = null;
mWebContents = null;
}
private final TabObserver mTabObserver = new EmptyTabObserver() {
@Override
public void onContentChanged(Tab tab) {
assert tab == mTab;
setWebContents(tab.getWebContents());
}
@Override
public void onFaviconUpdated(Tab tab, Bitmap icon) {
assert tab == mTab;
if (!updateFavicon(icon)) return;
if (mNotificationInfoBuilder == null) return;
mNotificationInfoBuilder.setLargeIcon(mFavicon);
MediaNotificationManager.show(
ContextUtils.getApplicationContext(), mNotificationInfoBuilder.build());
}
@Override
public void onUrlUpdated(Tab tab) {
assert tab == mTab;
String origin = mTab.getUrl();
try {
origin = UrlFormatter.formatUrlForSecurityDisplay(new URI(origin), true);
} catch (URISyntaxException e) {
Log.e(TAG, "Unable to parse the origin from the URL. "
+ "Using the full URL instead.");
}
if (mOrigin != null && mOrigin.equals(origin)) return;
mOrigin = origin;
mFavicon = null;
if (mNotificationInfoBuilder == null) return;
mNotificationInfoBuilder.setOrigin(mOrigin);
mNotificationInfoBuilder.setLargeIcon(mFavicon);
MediaNotificationManager.show(
ContextUtils.getApplicationContext(), mNotificationInfoBuilder.build());
}
@Override
public void onTitleUpdated(Tab tab) {
assert tab == mTab;
if (mNotificationInfoBuilder == null || mFallbackMetadata == null) return;
mFallbackMetadata = new MediaMetadata(mFallbackMetadata);
mFallbackMetadata.setTitle(sanitizeMediaTitle(mTab.getTitle()));
mNotificationInfoBuilder.setMetadata(mFallbackMetadata);
MediaNotificationManager.show(ContextUtils.getApplicationContext(),
mNotificationInfoBuilder.build());
}
@Override
public void onDestroyed(Tab tab) {
assert mTab == tab;
cleanupWebContents();
hideNotification();
mTab.removeObserver(this);
mTab = null;
}
};
private MediaSessionTabHelper(Tab tab) {
mTab = tab;
mTab.addObserver(mTabObserver);
if (mTab.getWebContents() != null) setWebContents(tab.getWebContents());
Activity activity = getActivityFromTab(mTab);
if (activity != null) {
mPreviousVolumeControlStream = activity.getVolumeControlStream();
}
}
/**
* Creates the {@link MediaSessionTabHelper} for the given {@link Tab}.
* @param tab the tab to attach the helper to.
*/
public static void createForTab(Tab tab) {
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 int convertMediaActionSourceToUMA(int source) {
if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_NOTIFICATION) {
return MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_MEDIA_NOTIFICATION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_MEDIA_SESSION) {
return MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_MEDIA_SESSION;
} else if (source == MediaNotificationListener.ACTION_SOURCE_HEADSET_UNPLUG) {
return MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_HEADSET_UNPLUG;
}
assert false;
return MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_MAX;
}
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.
* @return whether the best favicon is updated.
*/
private boolean updateFavicon(Bitmap icon) {
if (icon == null) return false;
if (icon.getWidth() < MINIMAL_FAVICON_SIZE || icon.getHeight() < MINIMAL_FAVICON_SIZE) {
return false;
}
if (mFavicon != null && (icon.getWidth() < mFavicon.getWidth()
|| icon.getHeight() < mFavicon.getHeight())) {
return false;
}
mFavicon = MediaNotificationManager.scaleIconForDisplay(icon);
return true;
}
}