[go: up one dir, main page]

blob: 94fe204afd2236ad6c559a4b8bfac9074c48f21a [file] [log] [blame]
// 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.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.IBinder;
import android.provider.Browser;
import android.support.v4.app.NotificationCompat;
import android.widget.RemoteViews;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler.TabOpenType;
import org.chromium.chrome.browser.metrics.MediaSessionUMA;
/**
* A class for notifications that provide information and optional media controls for a given media.
* Internally implements a Service for transforming notification Intents into
* {@link MediaPlaybackListener} calls for all registered listeners.
*/
public class NotificationMediaPlaybackControls {
private static final Object LOCK = new Object();
private static NotificationMediaPlaybackControls sInstance;
/**
* Service used to transform intent requests triggered from the notification into
* {@code Listener} callbacks. Ideally this class should be private, but public is required to
* create as a service.
*/
public static class ListenerService extends Service {
private static final String ACTION_PLAY =
"NotificationMediaPlaybackControls.ListenerService.PLAY";
private static final String ACTION_PAUSE =
"NotificationMediaPlaybackControls.ListenerService.PAUSE";
private PendingIntent getPendingIntent(String action) {
Intent intent = new Intent(this, ListenerService.class);
intent.setAction(action);
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
// This would only happen if we have been recreated by the OS after Chrome has died.
// In this case, there can be no media playback happening so we don't have to show
// the notification.
if (sInstance == null) return;
onServiceStarted(this);
}
@Override
public void onDestroy() {
super.onDestroy();
if (sInstance == null) return;
onServiceDestroyed();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null
|| sInstance == null
|| sInstance.mMediaNotificationInfo == null
|| sInstance.mMediaNotificationInfo.listener == null) {
stopSelf();
return START_NOT_STICKY;
}
String action = intent.getAction();
if (ACTION_PLAY.equals(action)) {
sInstance.mMediaNotificationInfo.listener.onPlay();
sInstance.onPlaybackStateChanged(false);
MediaSessionUMA.recordPlay(
MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_MEDIA_NOTIFICATION);
} else if (ACTION_PAUSE.equals(action)) {
sInstance.mMediaNotificationInfo.listener.onPause();
sInstance.onPlaybackStateChanged(true);
MediaSessionUMA.recordPause(
MediaSessionUMA.MEDIA_SESSION_ACTION_SOURCE_MEDIA_NOTIFICATION);
}
return START_NOT_STICKY;
}
}
/**
* Shows the notification with media controls with the specified media info. Replaces/updates
* the current notification if already showing. Does nothing if |mediaNotificationInfo| hasn't
* changed from the last one.
*
* @param applicationContext context to create the notification with
* @param mediaNotificationInfo information to show in the notification
*/
public static void show(Context applicationContext,
MediaNotificationInfo mediaNotificationInfo) {
synchronized (LOCK) {
if (sInstance == null) {
sInstance = new NotificationMediaPlaybackControls(applicationContext);
}
}
sInstance.showNotification(mediaNotificationInfo);
}
/**
* Hides the notification for the specified tabId.
*
* @param tabId the id of the tab that showed the notification or invalid tab id.
*/
public static void hide(int tabId) {
if (sInstance == null) return;
sInstance.hideNotification(tabId);
}
/**
* Hides any notification if shown by this service.
*/
public static void clear() {
if (sInstance == null) return;
sInstance.clearNotification();
}
/**
* Registers the started {@link Service} with the singleton and creates the notification.
*
* @param service the service that was started
*/
private static void onServiceStarted(ListenerService service) {
assert sInstance != null;
assert sInstance.mService == null;
sInstance.mService = service;
sInstance.updateNotification();
}
/**
* Handles the destruction
*/
private static void onServiceDestroyed() {
assert sInstance != null;
assert sInstance.mService != null;
sInstance.mNotificationBuilder = null;
sInstance.mService = null;
}
private final Context mContext;
// ListenerService running for the notification. Only non-null when showing.
private ListenerService mService;
private final String mPlayDescription;
private final String mPauseDescription;
private NotificationCompat.Builder mNotificationBuilder;
private Bitmap mNotificationIconBitmap;
private MediaNotificationInfo mMediaNotificationInfo;
private NotificationMediaPlaybackControls(Context context) {
mContext = context;
mPlayDescription = context.getResources().getString(R.string.accessibility_play);
mPauseDescription = context.getResources().getString(R.string.accessibility_pause);
}
private void showNotification(MediaNotificationInfo mediaNotificationInfo) {
mContext.startService(new Intent(mContext, ListenerService.class));
assert mediaNotificationInfo != null;
if (mediaNotificationInfo.equals(mMediaNotificationInfo)) return;
mMediaNotificationInfo = mediaNotificationInfo;
updateNotification();
}
private void clearNotification() {
mMediaNotificationInfo = null;
mContext.stopService(new Intent(mContext, ListenerService.class));
}
private void hideNotification(int tabId) {
if (mMediaNotificationInfo == null || tabId != mMediaNotificationInfo.tabId) return;
clearNotification();
}
private void onPlaybackStateChanged(boolean isPaused) {
assert mMediaNotificationInfo != null;
mMediaNotificationInfo = new MediaNotificationInfo(
mMediaNotificationInfo.title,
isPaused,
mMediaNotificationInfo.origin,
mMediaNotificationInfo.tabId,
mMediaNotificationInfo.isPrivate,
mMediaNotificationInfo.listener);
updateNotification();
}
private RemoteViews createContentView() {
RemoteViews contentView =
new RemoteViews(mContext.getPackageName(), R.layout.playback_notification_bar);
return contentView;
}
private String getStatus() {
if (mMediaNotificationInfo.origin != null) {
return mContext.getString(R.string.media_notification_link_text,
mMediaNotificationInfo.origin);
}
return mContext.getString(R.string.media_notification_text_no_link);
}
private String getTitle() {
String mediaTitle = mMediaNotificationInfo.title;
if (mMediaNotificationInfo.isPaused) {
return mContext.getString(
R.string.media_playback_notification_paused_for_media, mediaTitle);
}
return mContext.getString(
R.string.media_playback_notification_playing_for_media, mediaTitle);
}
private PendingIntent createContentIntent() {
int tabId = mMediaNotificationInfo.tabId;
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
intent.putExtra(TabOpenType.BRING_TAB_TO_FRONT.name(), tabId);
intent.setPackage(mContext.getPackageName());
return PendingIntent.getActivity(mContext, tabId, intent, 0);
}
private void updateNotification() {
if (mService == null) return;
if (mMediaNotificationInfo == null) {
// Notification was hidden before we could update it.
assert mNotificationBuilder == null;
return;
}
// Android doesn't badge the icons for RemoteViews automatically when
// running the app under the Work profile.
if (mNotificationIconBitmap == null) {
Drawable notificationIconDrawable = ApiCompatibilityUtils.getUserBadgedIcon(
mContext, R.drawable.audio_playing);
mNotificationIconBitmap = drawableToBitmap(notificationIconDrawable);
}
if (mNotificationBuilder == null) {
mNotificationBuilder = new NotificationCompat.Builder(mContext)
.setSmallIcon(R.drawable.audio_playing)
.setAutoCancel(false)
.setOngoing(true);
}
mNotificationBuilder.setContentIntent(createContentIntent());
RemoteViews contentView = createContentView();
contentView.setTextViewText(R.id.title, getTitle());
contentView.setTextViewText(R.id.status, getStatus());
if (mNotificationIconBitmap != null) {
contentView.setImageViewBitmap(R.id.icon, mNotificationIconBitmap);
} else {
contentView.setImageViewResource(R.id.icon, R.drawable.audio_playing);
}
if (mMediaNotificationInfo.isPaused) {
contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_play);
contentView.setContentDescription(R.id.playpause, mPlayDescription);
contentView.setOnClickPendingIntent(R.id.playpause,
mService.getPendingIntent(ListenerService.ACTION_PLAY));
} else {
contentView.setImageViewResource(R.id.playpause, R.drawable.ic_vidcontrol_pause);
contentView.setContentDescription(R.id.playpause, mPauseDescription);
contentView.setOnClickPendingIntent(R.id.playpause,
mService.getPendingIntent(ListenerService.ACTION_PAUSE));
}
mNotificationBuilder.setContent(contentView);
mNotificationBuilder.setVisibility(
mMediaNotificationInfo.isPrivate ? NotificationCompat.VISIBILITY_PRIVATE
: NotificationCompat.VISIBILITY_PUBLIC);
mService.startForeground(R.id.media_playback_notification, mNotificationBuilder.build());
}
private Bitmap drawableToBitmap(Drawable drawable) {
if (!(drawable instanceof BitmapDrawable)) return null;
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
return bitmapDrawable.getBitmap();
}
}