Index: chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java b/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java |
deleted file mode 100644 |
index 055ec4aa3ffa782841fc0748428c0c82dfa7bd4e..0000000000000000000000000000000000000000 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/remote/NotificationTransportControl.java |
+++ /dev/null |
@@ -1,519 +0,0 @@ |
-// Copyright 2013 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.app.Notification; |
-import android.app.PendingIntent; |
-import android.app.Service; |
-import android.content.Context; |
-import android.content.Intent; |
-import android.content.res.Resources; |
-import android.graphics.Bitmap; |
-import android.os.Handler; |
-import android.os.IBinder; |
-import android.support.v4.app.NotificationCompat; |
-import android.util.DisplayMetrics; |
-import android.view.View; |
-import android.widget.RemoteViews; |
- |
-import org.chromium.base.VisibleForTesting; |
-import org.chromium.chrome.R; |
-import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState; |
- |
-import java.util.Set; |
- |
-import javax.annotation.Nullable; |
- |
-/** |
- * A class for notifications that provide information and optional transport controls for a given |
- * remote control. Internally implements a Service for transforming notification Intents into |
- * {@link TransportControl.Listener} calls for all registered listeners. |
- */ |
-public class NotificationTransportControl |
- extends TransportControl implements MediaRouteController.UiListener { |
- /** |
- * Service used to transform intent requests triggered from the notification into |
- * {@code Listener} callbacks. Ideally this class should be protected, but public is required to |
- * create as a service. |
- */ |
- public static class ListenerService extends Service { |
- private static final String ACTION_PREFIX = ListenerService.class.getName() + "."; |
- |
- // Constants used by intent actions |
- public static final int ACTION_ID_PLAY = 0; |
- public static final int ACTION_ID_PAUSE = 1; |
- public static final int ACTION_ID_SEEK = 2; |
- public static final int ACTION_ID_STOP = 3; |
- public static final int ACTION_ID_SELECT = 4; |
- |
- // Intent parameters |
- public static final String SEEK_POSITION = "SEEK_POSITION"; |
- |
- // Must be kept in sync with the ACTION_ID_XXX constants above |
- private static final String[] ACTION_VERBS = {"PLAY", "PAUSE", "SEEK", "STOP", "SELECT"}; |
- |
- private PendingIntent[] mPendingIntents; |
- |
- private Notification mNotification; |
- |
- @VisibleForTesting |
- PendingIntent getPendingIntent(int id) { |
- return mPendingIntents[id]; |
- } |
- |
- @Override |
- public IBinder onBind(Intent intent) { |
- return null; |
- } |
- |
- @Override |
- public void onCreate() { |
- // If Android recreates the service the state can be wrong, see |
- // https://crbug.com/555266 |
- if (sInstance == null || sInstance.mService != null) return; |
- super.onCreate(); |
- |
- sInstance.mService = this; |
- |
- // Create all the PendingIntents |
- int actionCount = ACTION_VERBS.length; |
- mPendingIntents = new PendingIntent[actionCount]; |
- for (int i = 0; i < actionCount; ++i) { |
- Intent intent = new Intent(this, ListenerService.class); |
- intent.setAction(ACTION_PREFIX + ACTION_VERBS[i]); |
- mPendingIntents[i] = PendingIntent.getService(this, 0, intent, |
- PendingIntent.FLAG_CANCEL_CURRENT); |
- } |
- } |
- |
- @Override |
- public void onDestroy() { |
- if (sInstance == null || sInstance.mService == null) return; |
- stop(); |
- sInstance.mService = null; |
- } |
- |
- @Override |
- public void onTaskRemoved(Intent rootIntent) { |
- stop(); |
- } |
- |
- @Override |
- public int onStartCommand(Intent intent, int flags, int startId) { |
- if (sInstance == null) { |
- stopSelf(); |
- return START_NOT_STICKY; |
- } |
- |
- if (intent != null) { |
- String action = intent.getAction(); |
- if (action != null && action.startsWith(ACTION_PREFIX)) { |
- Set<Listener> listeners = sInstance.getListeners(); |
- |
- // Strip the prefix for matching the verb |
- action = action.substring(ACTION_PREFIX.length()); |
- if (ACTION_VERBS[ACTION_ID_PLAY].equals(action)) { |
- for (Listener listener : listeners) { |
- listener.onPlay(); |
- } |
- } else if (ACTION_VERBS[ACTION_ID_PAUSE].equals(action)) { |
- for (Listener listener : listeners) { |
- listener.onPause(); |
- } |
- } else if (ACTION_VERBS[ACTION_ID_SEEK].equals(action)) { |
- int seekPosition = intent.getIntExtra(SEEK_POSITION, 0); |
- for (Listener listener : listeners) { |
- listener.onSeek(seekPosition); |
- } |
- } else if (ACTION_VERBS[ACTION_ID_STOP].equals(action)) { |
- stop(); |
- } else if (ACTION_VERBS[ACTION_ID_SELECT].equals(action)) { |
- ExpandedControllerActivity.startActivity(this); |
- } |
- } |
- } |
- |
- updateNotification(); |
- return START_NOT_STICKY; |
- } |
- |
- private void stop() { |
- for (Listener listener : sInstance.getListeners()) listener.onStop(); |
- stopSelf(); |
- } |
- |
- private void createNotification() { |
- NotificationCompat.Builder notificationBuilder = |
- new NotificationCompat.Builder(this) |
- .setDefaults(0) |
- .setSmallIcon(R.drawable.ic_notification_media_route) |
- .setLocalOnly(true) |
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) |
- .setOnlyAlertOnce(true) |
- .setOngoing(true) |
- .setContent(createContentView()) |
- .setContentIntent(getPendingIntent(ListenerService.ACTION_ID_SELECT)) |
- .setDeleteIntent(getPendingIntent(ListenerService.ACTION_ID_STOP)); |
- mNotification = notificationBuilder.build(); |
- } |
- |
- private void updateNotification() { |
- RemoteVideoInfo videoInfo = sInstance.getVideoInfo(); |
- if (!sInstance.isShowing() || videoInfo == null |
- || videoInfo.state == PlayerState.STOPPED |
- || videoInfo.state == PlayerState.FINISHED |
- || videoInfo.state == PlayerState.INVALIDATED) { |
- stopForeground(true); |
- mNotification = null; |
- } else { |
- if (mNotification == null) createNotification(); |
- RemoteViews contentView = createContentView(); |
- contentView.setTextViewText(R.id.title, sInstance.getScreenName()); |
- contentView.setTextViewText(R.id.status, getStatus()); |
- if (sInstance.mIcon != null) { |
- contentView.setImageViewBitmap(R.id.icon, sInstance.mIcon); |
- } else { |
- contentView.setImageViewResource( |
- R.id.icon, R.drawable.ic_notification_media_route); |
- } |
- |
- boolean showPlayPause = false; |
- boolean showProgress = false; |
- switch (videoInfo.state) { |
- case PLAYING: |
- showProgress = true; |
- showPlayPause = true; |
- setProgressBar(videoInfo, contentView); |
- contentView.setImageViewResource( |
- R.id.playpause, R.drawable.ic_vidcontrol_pause); |
- contentView.setContentDescription( |
- R.id.playpause, sInstance.mPauseDescription); |
- contentView.setOnClickPendingIntent( |
- R.id.playpause, getPendingIntent(ListenerService.ACTION_ID_PAUSE)); |
- scheduleProgressUpdate(); |
- break; |
- case PAUSED: |
- showProgress = true; |
- showPlayPause = true; |
- setProgressBar(videoInfo, contentView); |
- contentView.setImageViewResource( |
- R.id.playpause, R.drawable.ic_vidcontrol_play); |
- contentView.setContentDescription( |
- R.id.playpause, sInstance.mPlayDescription); |
- contentView.setOnClickPendingIntent( |
- R.id.playpause, getPendingIntent(ListenerService.ACTION_ID_PLAY)); |
- break; |
- case LOADING: |
- showProgress = true; |
- contentView.setProgressBar(R.id.progress, 0, 0, true); |
- break; |
- case ERROR: |
- showProgress = true; |
- break; |
- default: |
- break; |
- } |
- |
- contentView.setViewVisibility( |
- R.id.playpause, showPlayPause ? View.VISIBLE : View.INVISIBLE); |
- // We use GONE instead of INVISIBLE for this because the notification looks funny |
- // with a large gap in the middle if we have no duration. Setting it to GONE forces |
- // the layout to squeeze tighter to the middle. |
- contentView.setViewVisibility(R.id.progress, |
- (showProgress && videoInfo.durationMillis > 0) ? View.VISIBLE : View.GONE); |
- contentView.setViewVisibility( |
- R.id.stop, showPlayPause ? View.VISIBLE : View.INVISIBLE); |
- |
- mNotification.contentView = contentView; |
- |
- startForeground(R.id.remote_notification, mNotification); |
- } |
- } |
- |
- private void setProgressBar(RemoteVideoInfo videoInfo, RemoteViews contentView) { |
- long durationMillis = videoInfo.durationMillis; |
- long currentTimeMillis = videoInfo.currentTimeMillis; |
- // Handle ridiculously long videos (25 days+). |
- if (durationMillis > Integer.MAX_VALUE) { |
- long factor = durationMillis / Integer.MAX_VALUE; |
- durationMillis = Integer.MAX_VALUE; |
- currentTimeMillis = currentTimeMillis / factor; |
- } |
- contentView.setProgressBar(R.id.progress, (int) durationMillis, (int) currentTimeMillis, |
- false); |
- } |
- |
- private RemoteViews createContentView() { |
- RemoteViews contentView = |
- new RemoteViews(getPackageName(), R.layout.remote_notification_bar); |
- contentView.setOnClickPendingIntent( |
- R.id.stop, getPendingIntent(ListenerService.ACTION_ID_STOP)); |
- |
- return contentView; |
- } |
- |
- private String getStatus() { |
- if (sInstance.hasError()) return sInstance.getError(); |
- RemoteVideoInfo videoInfo = sInstance.getVideoInfo(); |
- // TODO(bclayton): Is there something better to display here? |
- if (videoInfo == null) return ""; |
- String videoTitle = videoInfo.title; |
- switch (videoInfo.state) { |
- case PLAYING: |
- return videoTitle != null |
- ? getString(R.string.cast_notification_playing_for_video, videoTitle) |
- : getString(R.string.cast_notification_playing); |
- case LOADING: |
- return videoTitle != null |
- ? getString(R.string.cast_notification_loading_for_video, videoTitle) |
- : getString(R.string.cast_notification_loading); |
- case PAUSED: |
- return videoTitle != null |
- ? getString(R.string.cast_notification_paused_for_video, videoTitle) |
- : getString(R.string.cast_notification_paused); |
- case STOPPED: |
- return getString(R.string.cast_notification_stopped); |
- case FINISHED: |
- case INVALIDATED: |
- return videoTitle != null |
- ? getString(R.string.cast_notification_finished_for_video, videoTitle) |
- : getString(R.string.cast_notification_finished); |
- case ERROR: |
- default: |
- return videoInfo.errorMessage; |
- } |
- } |
- |
- private void scheduleProgressUpdate() { |
- sInstance.mHandler.postDelayed(new Runnable() { |
- @Override |
- public void run() { |
- sInstance.onPositionChanged(sInstance.mMediaRouteController.getPosition()); |
- } |
- }, sInstance.mProgressUpdateInterval); |
- } |
- } |
- |
- private static NotificationTransportControl sInstance = null; |
- private static final Object LOCK = new Object(); |
- |
- private static final int MINIMUM_PROGRESS_UPDATE_INTERVAL_MS = 1000; |
- |
- private boolean mShowing = false; |
- |
- private final String mPlayDescription; |
- |
- private final String mPauseDescription; |
- |
- private final Context mContext; |
- |
- private MediaRouteController mMediaRouteController; |
- |
- // ListenerService running for the notification. |
- private ListenerService mService; |
- |
- private Bitmap mIcon; |
- |
- private Handler mHandler; |
- |
- private int mProgressUpdateInterval = MINIMUM_PROGRESS_UPDATE_INTERVAL_MS; |
- |
- /** |
- * Returns the singleton NotificationTransportControl object. |
- * |
- * @param context The Context that the notification service needs to be created in. |
- * @param mrc The MediaRouteController object to use. |
- * @return A {@code NotificationTransportControl} object that uses the given |
- * MediaRouteController object. |
- */ |
- public static NotificationTransportControl getOrCreate(Context context, |
- @Nullable MediaRouteController mrc) { |
- synchronized (LOCK) { |
- if (sInstance == null) { |
- sInstance = new NotificationTransportControl(context); |
- sInstance.setVideoInfo( |
- new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null)); |
- } |
- |
- sInstance.setRouteController(mrc); |
- return sInstance; |
- } |
- } |
- |
- @VisibleForTesting |
- static NotificationTransportControl getIfExists() { |
- return sInstance; |
- } |
- |
- /** |
- * Scale the specified bitmap to the desired with and height while preserving aspect ratio. |
- */ |
- private Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { |
- if (bitmap == null) { |
- return null; |
- } |
- |
- float scaleX = 1.0f; |
- float scaleY = 1.0f; |
- if (bitmap.getWidth() > maxWidth) { |
- scaleX = maxWidth / (float) bitmap.getWidth(); |
- } |
- if (bitmap.getHeight() > maxHeight) { |
- scaleY = maxHeight / (float) bitmap.getHeight(); |
- } |
- float scale = Math.min(scaleX, scaleY); |
- int width = (int) (bitmap.getWidth() * scale); |
- int height = (int) (bitmap.getHeight() * scale); |
- return Bitmap.createScaledBitmap(bitmap, width, height, false); |
- } |
- |
- private NotificationTransportControl(Context context) { |
- this.mContext = context; |
- mHandler = new Handler(context.getMainLooper()); |
- mPlayDescription = context.getResources().getString(R.string.accessibility_play); |
- mPauseDescription = context.getResources().getString(R.string.accessibility_pause); |
- } |
- |
- @Override |
- public void hide() { |
- mShowing = false; |
- mContext.stopService(new Intent(mContext, ListenerService.class)); |
- } |
- |
- /** |
- * @return true if the notification is currently visible to the user. |
- */ |
- @VisibleForTesting |
- boolean isShowing() { |
- return mShowing; |
- } |
- |
- @Override |
- public void onDurationUpdated(long durationMillis) { |
- // Set the progress update interval based on the screen height/width, since there's no point |
- // in updating the progress bar more frequently than what the user can see. |
- // getDisplayMetrics() is dependent on the current orientation, so we need to get the max |
- // of both height and width so that both portrait and landscape notifications are covered. |
- DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); |
- float density = metrics.density; |
- float dpHeight = metrics.heightPixels / density; |
- float dpWidth = metrics.widthPixels / density; |
- |
- float maxDimen = Math.max(dpHeight, dpWidth); |
- |
- mProgressUpdateInterval = Math.max(MINIMUM_PROGRESS_UPDATE_INTERVAL_MS, |
- Math.round(durationMillis / maxDimen)); |
- |
- RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo()); |
- videoInfo.durationMillis = durationMillis; |
- setVideoInfo(videoInfo); |
- } |
- |
- @Override |
- public void onError(int error, String errorMessage) { |
- // Stop the session for all errors |
- hide(); |
- } |
- |
- @Override |
- public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) { |
- RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo()); |
- videoInfo.state = newState; |
- setVideoInfo(videoInfo); |
- } |
- |
- @Override |
- public void onPositionChanged(long positionMillis) { |
- RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo()); |
- videoInfo.currentTimeMillis = positionMillis; |
- setVideoInfo(videoInfo); |
- } |
- |
- @Override |
- public void onPrepared(MediaRouteController mediaRouteController) { |
- } |
- |
- @Override |
- public void onRouteSelected(String name, MediaRouteController mediaRouteController) { |
- setScreenName(name); |
- } |
- |
- @Override |
- public void onRouteUnselected(MediaRouteController mediaRouteController) { |
- hide(); |
- } |
- |
- @Override |
- public void onTitleChanged(String title) { |
- RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo()); |
- videoInfo.title = title; |
- setVideoInfo(videoInfo); |
- } |
- |
- @Override |
- public void setRouteController(MediaRouteController controller) { |
- if (mMediaRouteController != null) mMediaRouteController.removeUiListener(this); |
- mMediaRouteController = controller; |
- if (controller != null) controller.addUiListener(this); |
- } |
- |
- @Override |
- public void show(PlayerState initialState) { |
- mShowing = true; |
- RemoteVideoInfo newVideoInfo = new RemoteVideoInfo(mVideoInfo); |
- newVideoInfo.state = initialState; |
- setVideoInfo(newVideoInfo); |
- } |
- |
- @VisibleForTesting |
- Notification getNotification() { |
- // TODO(aberent). This is only used by the tests, which should actually simply be using |
- // isShowing(); but the tests are still downstream, so this needs to be changed in stages. |
- if (getService() == null) return null; |
- return getService().mNotification; |
- } |
- |
- @VisibleForTesting |
- final ListenerService getService() { |
- // TODO(aberent). This is only used by the tests, which should actually simply be using |
- // isShowing(); but the tests are still downstream, so this needs to be changed in stages. |
- if (!isShowing()) return null; |
- return mService; |
- } |
- |
- @Override |
- protected void onErrorChanged() { |
- mContext.startService(new Intent(mContext, ListenerService.class)); |
- } |
- |
- @Override |
- protected void onPosterBitmapChanged() { |
- Bitmap posterBitmap = getPosterBitmap(); |
- mIcon = scaleBitmapForIcon(posterBitmap); |
- super.onPosterBitmapChanged(); |
- } |
- |
- @Override |
- protected void onScreenNameChanged() { |
- mContext.startService(new Intent(mContext, ListenerService.class)); |
- } |
- |
- @Override |
- protected void onVideoInfoChanged() { |
- mContext.startService(new Intent(mContext, ListenerService.class)); |
- } |
- |
- /** |
- * Scale the specified bitmap to properly fit as a notification icon. If the argument is null |
- * the function returns null. |
- */ |
- private Bitmap scaleBitmapForIcon(Bitmap bitmap) { |
- Resources res = mContext.getResources(); |
- float maxWidth = res.getDimension(R.dimen.remote_notification_logo_max_width); |
- float maxHeight = res.getDimension(R.dimen.remote_notification_logo_max_height); |
- return scaleBitmap(bitmap, (int) maxWidth, (int) maxHeight); |
- } |
- |
-} |