// Copyright 2016 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.remote; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.media.AudioManager; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.R; import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState; import org.chromium.chrome.browser.media.ui.MediaNotificationInfo; import org.chromium.chrome.browser.media.ui.MediaNotificationListener; import org.chromium.chrome.browser.media.ui.MediaNotificationManager; import org.chromium.chrome.browser.metrics.MediaNotificationUma; import org.chromium.chrome.browser.tab.Tab; import org.chromium.content_public.common.MediaMetadata; import javax.annotation.Nullable; /** * Notification control controls the cast notification and lock screen, using * {@link MediaNotificationManager} */ public class CastNotificationControl implements MediaRouteController.UiListener, MediaNotificationListener, AudioManager.OnAudioFocusChangeListener { private static CastNotificationControl sInstance; private Bitmap mPosterBitmap; protected MediaRouteController mMediaRouteController = null; private MediaNotificationInfo.Builder mNotificationBuilder; private Context mContext; private PlayerState mState; private String mTitle = ""; private AudioManager mAudioManager; private boolean mIsShowing = false; private static final Object LOCK = new Object(); private CastNotificationControl(Context context) { mContext = context; mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); } /** * Get the unique NotificationControl, creating it if necessary. * @param context The context of the activity * @param mediaRouteController The current mediaRouteController, if any. * @return a {@code LockScreenTransportControl} based on the platform's SDK API or null if the * current platform's SDK API is not supported. */ public static CastNotificationControl getOrCreate(Context context, @Nullable MediaRouteController mediaRouteController) { synchronized (LOCK) { if (sInstance == null) { sInstance = new CastNotificationControl(context); } sInstance.setRouteController(mediaRouteController); return sInstance; } } /** * @return the poster bitmap previously assigned with {@link #setPosterBitmap(Bitmap)}, or * {@code null} if the poster bitmap has not yet been assigned. */ public final Bitmap getPosterBitmap() { return mPosterBitmap; } /** * Sets the poster bitmap to display on the TransportControl. */ public final void setPosterBitmap(Bitmap posterBitmap) { if (mPosterBitmap == posterBitmap || (mPosterBitmap != null && mPosterBitmap.sameAs(posterBitmap))) { return; } mPosterBitmap = posterBitmap; if (mNotificationBuilder == null || mMediaRouteController == null) return; mNotificationBuilder.setLargeIcon(mMediaRouteController.getPoster()); updateNotification(); } public void hide() { mIsShowing = false; MediaNotificationManager.hide(Tab.INVALID_TAB_ID, R.id.remote_notification); mAudioManager.abandonAudioFocus(this); mMediaRouteController.removeUiListener(this); } public void show(PlayerState initialState) { mMediaRouteController.addUiListener(this); // TODO(aberent): investigate why this is necessary, and whether we are handling // it correctly. Also add code to restore it when Chrome is resumed. mAudioManager.requestAudioFocus(this, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.AUDIOFOCUS_GAIN); Intent contentIntent = new Intent(mContext, ExpandedControllerActivity.class); contentIntent.putExtra(MediaNotificationUma.INTENT_EXTRA_NAME, MediaNotificationUma.SOURCE_MEDIA_FLING); mNotificationBuilder = new MediaNotificationInfo.Builder() .setPaused(false) .setPrivate(false) .setIcon(R.drawable.ic_notification_media_route) .setContentIntent(contentIntent) .setLargeIcon(mMediaRouteController.getPoster()) .setDefaultLargeIcon(R.drawable.cast_playing_square) .setId(R.id.remote_notification) .setListener(this); mState = initialState; updateNotification(); mIsShowing = true; } public void setRouteController(MediaRouteController controller) { if (mMediaRouteController != null) mMediaRouteController.removeUiListener(this); mMediaRouteController = controller; if (controller != null) controller.addUiListener(this); } private void updateNotification() { // Nothing shown yet, nothing to update. if (mNotificationBuilder == null) return; mNotificationBuilder.setMetadata(new MediaMetadata(mTitle, "", "")); if (mState == PlayerState.PAUSED || mState == PlayerState.PLAYING) { mNotificationBuilder.setPaused(mState != PlayerState.PLAYING); mNotificationBuilder.setActions(MediaNotificationInfo.ACTION_STOP | MediaNotificationInfo.ACTION_PLAY_PAUSE); MediaNotificationManager.show(mContext, mNotificationBuilder.build()); } else if (mState == PlayerState.LOADING) { mNotificationBuilder.setActions(MediaNotificationInfo.ACTION_STOP); MediaNotificationManager.show(mContext, mNotificationBuilder.build()); } else { hide(); } } // TODO(aberent) at the moment this is only called from a test, but it should be called if the // poster changes. public void onPosterBitmapChanged() { if (mNotificationBuilder == null || mMediaRouteController == null) return; mNotificationBuilder.setLargeIcon(mMediaRouteController.getPoster()); updateNotification(); } // MediaRouteController.UiListener implementation. @Override public void onPlaybackStateChanged(PlayerState newState) { if (!mIsShowing && (newState == PlayerState.PLAYING || newState == PlayerState.LOADING || newState == PlayerState.PAUSED)) { show(newState); return; } if (mState == newState || mState == PlayerState.PAUSED && newState == PlayerState.LOADING && mIsShowing) { return; } mState = newState; updateNotification(); } @Override public void onRouteSelected(String name, MediaRouteController mediaRouteController) { } @Override public void onRouteUnselected(MediaRouteController mediaRouteController) { hide(); } @Override public void onPrepared(MediaRouteController mediaRouteController) { } @Override public void onError(int errorType, String message) { } @Override public void onDurationUpdated(long durationMillis) { } @Override public void onPositionChanged(long positionMillis) { } @Override public void onTitleChanged(String title) { if (title == null || title.equals(mTitle)) return; mTitle = title; updateNotification(); } // MediaNotificationListener methods @Override public void onPlay(int actionSource) { mMediaRouteController.resume(); } @Override public void onPause(int actionSource) { mMediaRouteController.pause(); } @Override public void onStop(int actionSource) { mMediaRouteController.release(); } // AudioManager.OnAudioFocusChangeListener methods @Override public void onAudioFocusChange(int focusChange) { // Do nothing. The remote player should keep playing. } @VisibleForTesting static CastNotificationControl getForTests() { return sInstance; } @VisibleForTesting boolean isShowingForTests() { return mIsShowing; } }