// 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.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.FragmentActivity; import android.support.v4.media.TransportMediator; import android.support.v4.media.TransportPerformer; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.ImageView; import android.widget.TextView; import com.google.android.gms.cast.CastMediaControlIntent; import org.chromium.chrome.R; import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState; import org.chromium.chrome.browser.metrics.MediaNotificationUma; import org.chromium.third_party.android.media.MediaController; /** * The activity that's opened by clicking the video flinging (casting) notification. * * TODO(aberent): Refactor to merge some common logic with {@link CastNotificationControl}. */ public class ExpandedControllerActivity extends FragmentActivity implements MediaRouteController.UiListener { private static final int PROGRESS_UPDATE_PERIOD_IN_MS = 1000; // The alpha value for the poster/placeholder image, an integer between 0 and 256 (opaque). private static final int POSTER_IMAGE_ALPHA = 200; private Handler mHandler; // We don't use the standard android.media.MediaController, but a custom one. // See the class itself for details. private MediaController mMediaController; private FullscreenMediaRouteButton mMediaRouteButton; private MediaRouteController mMediaRouteController; private RemoteVideoInfo mVideoInfo; private String mScreenName; private TransportMediator mTransportMediator; /** * Handle actions from on-screen media controls. */ private TransportPerformer mTransportPerformer = new TransportPerformer() { @Override public void onStart() { if (mMediaRouteController == null) return; mMediaRouteController.resume(); RecordCastAction.recordFullscreenControlsAction( RecordCastAction.FULLSCREEN_CONTROLS_RESUME, mMediaRouteController.getMediaStateListener() != null); } @Override public void onStop() { if (mMediaRouteController == null) return; onPause(); mMediaRouteController.release(); } @Override public void onPause() { if (mMediaRouteController == null) return; mMediaRouteController.pause(); RecordCastAction.recordFullscreenControlsAction( RecordCastAction.FULLSCREEN_CONTROLS_PAUSE, mMediaRouteController.getMediaStateListener() != null); } @Override public long onGetDuration() { if (mMediaRouteController == null) return 0; return mMediaRouteController.getDuration(); } @Override public long onGetCurrentPosition() { if (mMediaRouteController == null) return 0; return mMediaRouteController.getPosition(); } @Override public void onSeekTo(long pos) { if (mMediaRouteController == null) return; mMediaRouteController.seekTo(pos); RecordCastAction.recordFullscreenControlsAction( RecordCastAction.FULLSCREEN_CONTROLS_SEEK, mMediaRouteController.getMediaStateListener() != null); } @Override public boolean onIsPlaying() { if (mMediaRouteController == null) return false; return mMediaRouteController.isPlaying(); } @Override public int onGetTransportControlFlags() { int flags = TransportMediator.FLAG_KEY_MEDIA_REWIND | TransportMediator.FLAG_KEY_MEDIA_FAST_FORWARD; if (mMediaRouteController != null && mMediaRouteController.isPlaying()) { flags |= TransportMediator.FLAG_KEY_MEDIA_PAUSE; } else { flags |= TransportMediator.FLAG_KEY_MEDIA_PLAY; } return flags; } }; private Runnable mProgressUpdater = new Runnable() { @Override public void run() { if (mMediaRouteController.isPlaying()) { mMediaController.updateProgress(); mHandler.postDelayed(this, PROGRESS_UPDATE_PERIOD_IN_MS); } else { mHandler.removeCallbacks(this); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MediaNotificationUma.recordClickSource(getIntent()); mMediaRouteController = RemoteMediaPlayerController.instance().getCurrentlyPlayingMediaRouteController(); if (mMediaRouteController == null || mMediaRouteController.routeIsDefaultRoute()) { // We don't want to do anything for the default (local) route finish(); return; } // Make the activity full screen. requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // requestWindowFeature must be called before adding content. setContentView(R.layout.expanded_cast_controller); mHandler = new Handler(); ViewGroup rootView = (ViewGroup) findViewById(android.R.id.content); rootView.setBackgroundColor(Color.BLACK); mMediaRouteController.addUiListener(this); // Create transport controller to control video, giving the callback // interface to receive actions from. mTransportMediator = new TransportMediator(this, mTransportPerformer); // Create and initialize the media control UI. mMediaController = (MediaController) findViewById(R.id.cast_media_controller); mMediaController.setMediaPlayer(mTransportMediator); View button = getLayoutInflater().inflate(R.layout.cast_controller_media_route_button, rootView, false); if (button instanceof FullscreenMediaRouteButton) { mMediaRouteButton = (FullscreenMediaRouteButton) button; rootView.addView(mMediaRouteButton); mMediaRouteButton.bringToFront(); mMediaRouteButton.initialize(mMediaRouteController); } else { mMediaRouteButton = null; } // Initialize the video info. setVideoInfo(new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null)); mMediaController.refresh(); scheduleProgressUpdate(); } @Override protected void onResume() { super.onResume(); if (mVideoInfo.state == PlayerState.FINISHED) finish(); if (mMediaRouteController == null) return; // Lifetime of the media element is bound to that of the {@link MediaStateListener} // of the {@link MediaRouteController}. RecordCastAction.recordFullscreenControlsShown( mMediaRouteController.getMediaStateListener() != null); mMediaRouteController.prepareMediaRoute(); ImageView iv = (ImageView) findViewById(R.id.cast_background_image); if (iv == null) return; Bitmap posterBitmap = mMediaRouteController.getPoster(); if (posterBitmap != null) iv.setImageBitmap(posterBitmap); iv.setImageAlpha(POSTER_IMAGE_ALPHA); } @Override protected void onDestroy() { cleanup(); super.onDestroy(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); if ((keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP) || mVideoInfo.state == PlayerState.FINISHED) { return super.dispatchKeyEvent(event); } return handleVolumeKeyEvent(mMediaRouteController, event); } private void cleanup() { if (mHandler != null) mHandler.removeCallbacks(mProgressUpdater); if (mMediaRouteController != null) mMediaRouteController.removeUiListener(this); mMediaRouteController = null; mProgressUpdater = null; } /** * Sets the remote's video information to display. */ private final void setVideoInfo(RemoteVideoInfo videoInfo) { if ((mVideoInfo == null) ? (videoInfo == null) : mVideoInfo.equals(videoInfo)) return; mVideoInfo = videoInfo; onVideoInfoChanged(); } private void scheduleProgressUpdate() { mHandler.removeCallbacks(mProgressUpdater); if (mMediaRouteController.isPlaying()) { mHandler.post(mProgressUpdater); } } /** * Sets the name to display for the device. */ private void setScreenName(String screenName) { if (TextUtils.equals(mScreenName, screenName)) return; mScreenName = screenName; onScreenNameChanged(); } private void onVideoInfoChanged() { updateUi(); } private void onScreenNameChanged() { updateUi(); } private void updateUi() { if (mMediaController == null || mMediaRouteController == null) return; String deviceName = mMediaRouteController.getRouteName(); String castText = ""; if (deviceName != null) { castText = getResources().getString(R.string.cast_casting_video, deviceName); } TextView castTextView = (TextView) findViewById(R.id.cast_screen_title); castTextView.setText(castText); mMediaController.refresh(); } @Override public void onRouteSelected(String name, MediaRouteController mediaRouteController) { setScreenName(name); } @Override public void onRouteUnselected(MediaRouteController mediaRouteController) { finish(); } @Override public void onPrepared(MediaRouteController mediaRouteController) { // No implementation. } @Override public void onError(int error, String message) { if (error == CastMediaControlIntent.ERROR_CODE_SESSION_START_FAILED) finish(); } @Override public void onPlaybackStateChanged(PlayerState newState) { RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo); videoInfo.state = newState; setVideoInfo(videoInfo); scheduleProgressUpdate(); if (newState == PlayerState.FINISHED || newState == PlayerState.INVALIDATED) { // If we are switching to a finished state, stop the notifications. finish(); } } @Override public void onDurationUpdated(long durationMillis) { RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo); videoInfo.durationMillis = durationMillis; setVideoInfo(videoInfo); } @Override public void onPositionChanged(long positionMillis) { RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo); videoInfo.currentTimeMillis = positionMillis; setVideoInfo(videoInfo); } @Override public void onTitleChanged(String title) { RemoteVideoInfo videoInfo = new RemoteVideoInfo(mVideoInfo); videoInfo.title = title; setVideoInfo(videoInfo); } /** * Modify remote volume by handling volume keys. * * @param controller The remote controller through which the volume will be modified. * @param event The key event. Its keycode needs to be either {@code KEYCODE_VOLUME_DOWN} or * {@code KEYCODE_VOLUME_UP} otherwise this method will return false. * @return True if the event is handled. */ private boolean handleVolumeKeyEvent(MediaRouteController controller, KeyEvent event) { if (!controller.isBeingCast()) return false; int action = event.getAction(); int keyCode = event.getKeyCode(); // Intercept the volume keys to affect only remote volume. switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: if (action == KeyEvent.ACTION_DOWN) controller.setRemoteVolume(-1); return true; case KeyEvent.KEYCODE_VOLUME_UP: if (action == KeyEvent.ACTION_DOWN) controller.setRemoteVolume(1); return true; default: return false; } } /** * Launches the ExpandedControllerActivity as a new task. * * @param context the Context to start this activity within. */ public static void startActivity(Context context) { if (context == null) return; Intent intent = new Intent(context, ExpandedControllerActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }