// 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.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.MediaRouteChooserDialogFragment;
import android.support.v7.app.MediaRouteControllerDialogFragment;
import android.support.v7.app.MediaRouteDialogFactory;
import com.google.android.gms.cast.CastMediaControlIntent;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.media.remote.MediaRouteController.MediaStateListener;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
import org.chromium.ui.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* The singleton responsible managing the global resources for remote media playback (cast)
*/
public class RemoteMediaPlayerController implements MediaRouteController.UiListener {
// Singleton instance of the class. May only be accessed from UI thread.
private static RemoteMediaPlayerController sInstance;
private static final String TAG = "MediaFling";
private static final String DEFAULT_CASTING_MESSAGE = "Casting to Chromecast";
private CastNotificationControl mNotificationControl;
private Context mCastContextApplicationContext;
// The Activity that was in the foreground when the video was cast.
private WeakReference<Activity> mChromeVideoActivity;
private List<MediaRouteController> mMediaRouteControllers;
// points to mDefaultRouteSelector, mYouTubeRouteSelector or null
private MediaRouteController mCurrentRouteController;
// This is a key for meta-data in the package manifest.
private static final String REMOTE_MEDIA_PLAYERS_KEY =
"org.chromium.content.browser.REMOTE_MEDIA_PLAYERS";
/**
* The private constructor to make sure the object is only created by the instance() method.
*/
private RemoteMediaPlayerController() {
mChromeVideoActivity = new WeakReference<Activity>(null);
mMediaRouteControllers = new ArrayList<MediaRouteController>();
}
/**
* @return The poster image for the currently playing remote video, null if there's none.
*/
public Bitmap getPoster() {
if (mCurrentRouteController == null) return null;
return mCurrentRouteController.getPoster();
}
/**
* The singleton instance access method for native objects. Must be called on the UI thread
* only.
*/
public static RemoteMediaPlayerController instance() {
ThreadUtils.assertOnUiThread();
if (sInstance == null) sInstance = new RemoteMediaPlayerController();
if (sInstance.mChromeVideoActivity.get() == null) sInstance.linkToBrowserActivity();
return sInstance;
}
/**
* Gets the MediaRouteController for a video, creating it if necessary.
* @param frameUrl The Url of the frame containing the video
* @return the MediaRouteController, or null if none.
*/
public MediaRouteController getMediaRouteController(String sourceUrl, String frameUrl) {
for (MediaRouteController controller: mMediaRouteControllers) {
if (controller.canPlayMedia(sourceUrl, frameUrl)) {
return controller;
}
}
return null;
}
/**
* Gets the default MediaRouteController, creating it if necessary.
* @return the default MediaRouteController.
*/
public List<MediaRouteController> getMediaRouteControllers() {
return mMediaRouteControllers;
}
/**
* Links this object to the Activity that owns the video, if it exists.
*
*/
private void linkToBrowserActivity() {
Activity currentActivity = ApplicationStatus.getLastTrackedFocusedActivity();
if (currentActivity != null) {
mChromeVideoActivity = new WeakReference<Activity>(currentActivity);
mCastContextApplicationContext = currentActivity.getApplicationContext();
createMediaRouteControllers(currentActivity);
}
}
/**
* Create the mediaRouteControllers
* @param context - the current Android Context
*/
public void createMediaRouteControllers(Context context) {
// We only need to do this once
if (!mMediaRouteControllers.isEmpty()) return;
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(
context.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
String classNameString = bundle.getString(REMOTE_MEDIA_PLAYERS_KEY);
if (classNameString != null) {
String[] classNames = classNameString.split(",");
for (String className : classNames) {
Log.d(TAG, "Adding remote media route controller %s", className.trim());
Class<?> mediaRouteControllerClass = Class.forName(className.trim());
Object mediaRouteController = mediaRouteControllerClass.newInstance();
assert mediaRouteController instanceof MediaRouteController;
mMediaRouteControllers.add((MediaRouteController) mediaRouteController);
}
}
} catch (NameNotFoundException | ClassNotFoundException | SecurityException
| InstantiationException | IllegalAccessException | IllegalArgumentException e) {
// Should never happen, implies corrupt AndroidManifest
Log.e(TAG, "Couldn't instatiate MediaRouteControllers", e);
assert false;
}
}
private void onStateReset(MediaRouteController controller) {
if (!controller.initialize()) return;
mNotificationControl = CastNotificationControl.getOrCreate(
mChromeVideoActivity.get(), controller);
mNotificationControl.setPosterBitmap(getPoster());
controller.prepareMediaRoute();
controller.addUiListener(this);
}
/**
* Called when a lower layer requests that a video be cast. This will typically be a request
* from Blink when the cast button is pressed on the default video controls.
* @param player the player for which cast is being requested
* @param frameUrl the URL of the frame containing the video, needed for YouTube videos
*/
public void requestRemotePlayback(
MediaRouteController.MediaStateListener player, MediaRouteController controller) {
Activity currentActivity = ApplicationStatus.getLastTrackedFocusedActivity();
mChromeVideoActivity = new WeakReference<Activity>(currentActivity);
if (mCurrentRouteController != null && controller != mCurrentRouteController) {
mCurrentRouteController.release();
}
onStateReset(controller);
showMediaRouteDialog(player, controller, currentActivity);
}
/**
* Called when a lower layer requests control of a video that is being cast.
* @param player The player for which remote playback control is being requested.
*/
public void requestRemotePlaybackControl(MediaRouteController.MediaStateListener player) {
// Player should match currently remotely played item, but there
// can be a race between various
// ways that the a video can stop playing remotely. Check that the
// player is current, and ignore if not.
if (mCurrentRouteController == null) return;
if (mCurrentRouteController.getMediaStateListener() != player) return;
showMediaRouteControlDialog(ApplicationStatus.getLastTrackedFocusedActivity());
}
private void showMediaRouteDialog(MediaStateListener player, MediaRouteController controller,
Activity activity) {
FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
if (fm == null) {
throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
}
MediaRouteDialogFactory factory = new MediaRouteChooserDialogFactory(player, controller,
activity);
if (fm.findFragmentByTag(
"android.support.v7.mediarouter:MediaRouteChooserDialogFragment") != null) {
Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
return;
}
MediaRouteChooserDialogFragment f = factory.onCreateChooserDialogFragment();
f.setRouteSelector(controller.buildMediaRouteSelector());
f.show(fm, "android.support.v7.mediarouter:MediaRouteChooserDialogFragment");
}
private void showMediaRouteControlDialog(Activity activity) {
FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
if (fm == null) {
throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
}
MediaRouteDialogFactory factory = new MediaRouteControllerDialogFactory();
if (fm.findFragmentByTag(
"android.support.v7.mediarouter:MediaRouteControllerDialogFragment") != null) {
Log.w(TAG, "showDialog(): Route controller dialog already showing!");
return;
}
MediaRouteControllerDialogFragment f = factory.onCreateControllerDialogFragment();
f.show(fm, "android.support.v7.mediarouter:MediaRouteControllerDialogFragment");
}
/**
* @return the currently playing MediaRouteController
*/
public MediaRouteController getCurrentlyPlayingMediaRouteController() {
return mCurrentRouteController;
}
/**
* Set the current MediaRouteController
* @param controller the controller
*/
public void setCurrentMediaRouteController(MediaRouteController controller) {
mCurrentRouteController = controller;
}
private CastNotificationControl getNotificationControl() {
return mNotificationControl;
}
@Override
public void onPrepared(MediaRouteController mediaRouteController) {
}
@Override
public void onPlaybackStateChanged(PlayerState newState) {
}
@Override
public void onError(int error, String errorMessage) {
if (error == CastMediaControlIntent.ERROR_CODE_SESSION_START_FAILED) {
showMessageToast(errorMessage);
}
}
@Override
public void onDurationUpdated(long durationMillis) {}
@Override
public void onPositionChanged(long positionMillis) {}
@Override
public void onTitleChanged(String title) {}
@Override
public void onRouteSelected(String routeName, MediaRouteController mediaRouteController) {
if (mCurrentRouteController != mediaRouteController) {
mCurrentRouteController = mediaRouteController;
resetPlayingVideo();
}
}
/**
* Gets some text to tell the user that the video is being cast.
* @param routeName The name of the route on which the video is being cast.
* @return A String to be shown to the user.
*/
public String getCastingMessage(String routeName) {
String castingMessage = DEFAULT_CASTING_MESSAGE;
if (mCastContextApplicationContext != null) {
castingMessage = mCastContextApplicationContext.getString(
R.string.cast_casting_video, routeName);
}
return castingMessage;
}
// Note that, after switching MediaRouteControllers onRouteUnselected may be called for
// the old media route controller, so this should not do anything to
// mCurrentRouteController
@Override
public void onRouteUnselected(MediaRouteController mediaRouteController) {
if (mediaRouteController == mCurrentRouteController) {
mCurrentRouteController = null;
}
}
private void showMessageToast(String message) {
Toast toast = Toast.makeText(mCastContextApplicationContext, message, Toast.LENGTH_SHORT);
toast.show();
}
private void resetPlayingVideo() {
if (mNotificationControl != null) {
mNotificationControl.setRouteController(mCurrentRouteController);
}
}
@VisibleForTesting
static RemoteMediaPlayerController getIfExists() {
return sInstance;
}
}