Commit 76097722 authored by aberent's avatar aberent Committed by Commit bot

Upstream Chrome for Android Cast.

This upstreams all of Chrome for Android's cast code with the exception of:
1. The YouTube support code.
2. The id of the Chrome cast receiver.

The YouTube support code and the id of Chrome cast receiver have to remain
proprietory due to licencing restrictions. The tests will be upstreamed
seperately, but need further work to make them independent of other
downstream code.

BUG=315088

Review URL: https://codereview.chromium.org/928643003

Cr-Commit-Position: refs/heads/master@{#322569}
parent 96a3b4c5
...@@ -18,6 +18,7 @@ android_resources("chrome_java_resources") { ...@@ -18,6 +18,7 @@ android_resources("chrome_java_resources") {
"//content/public/android:content_java_resources", "//content/public/android:content_java_resources",
"//chrome/app:java_strings_grd", "//chrome/app:java_strings_grd",
"//third_party/android_data_chart:android_data_chart_java_resources", "//third_party/android_data_chart:android_data_chart_java_resources",
"//third_party/android_media:android_media_resources",
"//third_party/android_tools:android_support_v7_appcompat_resources", "//third_party/android_tools:android_support_v7_appcompat_resources",
] ]
custom_package = "org.chromium.chrome" custom_package = "org.chromium.chrome"
...@@ -92,12 +93,15 @@ android_library("chrome_java") { ...@@ -92,12 +93,15 @@ android_library("chrome_java") {
"//printing:printing_java", "//printing:printing_java",
"//sync/android:sync_java", "//sync/android:sync_java",
"//third_party/android_data_chart:android_data_chart_java", "//third_party/android_data_chart:android_data_chart_java",
"//third_party/android_media:android_media_java",
"//third_party/android_protobuf:protobuf_nano_javalib", "//third_party/android_protobuf:protobuf_nano_javalib",
"//third_party/android_tools:android_support_v13_java", "//third_party/android_tools:android_support_v13_java",
"//third_party/android_tools:android_support_v7_appcompat_java", "//third_party/android_tools:android_support_v7_appcompat_java",
"//third_party/android_tools:android_support_v7_mediarouter_java",
"//third_party/cacheinvalidation:cacheinvalidation_javalib", "//third_party/cacheinvalidation:cacheinvalidation_javalib",
"//third_party/cacheinvalidation:cacheinvalidation_proto_java", "//third_party/cacheinvalidation:cacheinvalidation_proto_java",
"//third_party/jsr-305:jsr_305_javalib", "//third_party/jsr-305:jsr_305_javalib",
"//media/base/android:media_java",
"//ui/android:ui_java", "//ui/android:ui_java",
"//ui/android:ui_java_resources", "//ui/android:ui_java_resources",
google_play_services_library, google_play_services_library,
......
include_rules = [ include_rules = [
"+components/invalidation", "+components/invalidation",
"+jni", "+jni",
"+media/base/android/java",
] ]
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2014 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="@color/light_active_color" />
<size
android:width="40dp"
android:height="40dp"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2014 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="@color/light_active_color" />
<size
android:width="40dp"
android:height="40dp"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:state_enabled="true"
android:drawable="@drawable/ic_cast_dark_on" />
<item android:state_activated="true" android:state_enabled="true"
android:drawable="@drawable/ic_cast_dark_on" />
<item android:state_enabled="true"
android:drawable="@drawable/ic_cast_dark_off" />
</selector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<org.chromium.chrome.browser.media.remote.FullscreenMediaRouteButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cast_controller_media_route_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
android:layout_gravity = "start|top"
style="@style/CastMediaRouteButton" />
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/cast_frame_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:minHeight="200dp">
<ImageView
android:id="@+id/cast_background_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:alpha="0.7"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:contentDescription="@null"/>
<TextView
android:id="@+id/cast_screen_title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/cast_media_controller_text" />
<org.chromium.third_party.android.media.MediaController
android:id="@+id/cast_media_controller"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
</org.chromium.third_party.android.media.MediaController>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 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. -->
<!--
Notification layout for remote controls.
___________________________________________________________
| | | |
| | [Living Room TV] | _ |
| ICON | =====0============================= | || |_| |
| | Playing "[Web Page Title]" | |
|________|______________________________________|_________|
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<FrameLayout
android:layout_width="@android:dimen/notification_large_icon_width"
android:layout_height="@android:dimen/notification_large_icon_height" >
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="centerInside"
android:src="@drawable/notification_icon_bg" />
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="center"
android:src="@drawable/ic_notification_media_route" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:ellipsize="end"
android:singleLine="true"
style="@style/RemoteNotificationTitle"/>
<!-- android:visibility is set to 'gone' by default since we don't want to show it as long
as the duration of the video is unknown. The duration can be unknown in the case of
live streaming videos or YouTube. -->
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
style="@style/RemoteNotificationProgressBar"/>
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:ellipsize="end"
android:singleLine="true"
style="@style/RemoteNotificationText"/>
</LinearLayout>
<ImageButton
android:id="@+id/playpause"
android:src="@drawable/ic_vidcontrol_play"
android:layout_width="40dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:gravity="center"
android:padding="8dp"
android:scaleType="center"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@null"/>
<ImageButton
android:id="@+id/stop"
android:src="@drawable/ic_vidcontrol_stop"
android:layout_width="40dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:padding="8dp"
android:scaleType="center"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/accessibility_stop"/>
</LinearLayout>
\ No newline at end of file
...@@ -224,5 +224,23 @@ ...@@ -224,5 +224,23 @@
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </style>
<!-- Cast -->
<style
name="RemoteNotificationTitle"
parent="@android:style/TextAppearance.StatusBar.EventContent.Title"/>
<style
name="RemoteNotificationText"
parent="@android:style/TextAppearance.StatusBar.EventContent"/>
<style
name="RemoteNotificationProgressBar"
parent="@android:style/Widget.Holo.ProgressBar.Horizontal"/>
<style name="CastMediaRouteButton" parent="@style/Widget.MediaRouter.MediaRouteButton">
<item name="android:background">@null</item>
<item name="android:paddingTop">0dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="android:paddingStart">0dp</item>
<item name="android:paddingEnd">0dp</item>
<item name="externalRouteEnabledDrawable">@drawable/ic_cast_dark_chrome</item>
</style>
</resources> </resources>
...@@ -58,5 +58,15 @@ ...@@ -58,5 +58,15 @@
<style name="ButtonCompatBorderless" parent="ButtonCompat"> <style name="ButtonCompatBorderless" parent="ButtonCompat">
<item name="android:background">@drawable/button_borderless_compat</item> <item name="android:background">@drawable/button_borderless_compat</item>
</style> </style>
<!-- Cast notification -->
<style name="RemoteNotificationTitle"
parent="@android:style/TextAppearance.Material.Notification.Title">
<item name="android:layout_marginBottom">4dp</item>
</style>
<style name="RemoteNotificationText"
parent="@android:style/TextAppearance.Material.Notification.Line2"/>
<style name="RemoteNotificationProgressBar" parent="@android:style/Widget.ProgressBar.Horizontal">
<item name="android:minHeight">5dp</item>
</style>
</resources> </resources>
...@@ -126,4 +126,8 @@ ...@@ -126,4 +126,8 @@
<!-- Autofill card unmasking prompt dimensions --> <!-- Autofill card unmasking prompt dimensions -->
<dimen name="autofill_card_unmask_tooltip_horizontal_padding">16dp</dimen> <dimen name="autofill_card_unmask_tooltip_horizontal_padding">16dp</dimen>
<dimen name="autofill_card_unmask_tooltip_vertical_padding">4dp</dimen> <dimen name="autofill_card_unmask_tooltip_vertical_padding">4dp</dimen>
<!-- Cast related constants -->
<dimen name="remote_notification_logo_max_width">@android:dimen/notification_large_icon_width</dimen>
<dimen name="remote_notification_logo_max_height">64dp</dimen>
</resources> </resources>
...@@ -39,4 +39,7 @@ ...@@ -39,4 +39,7 @@
<item type="id" name="menu_id_help_privacy" /> <item type="id" name="menu_id_help_privacy" />
<item type="id" name="menu_id_contextual_search_learn" /> <item type="id" name="menu_id_contextual_search_learn" />
<!-- Cast notification -->
<item type="id" name="remote_notification" />
</resources> </resources>
// Copyright 2014 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.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.MediaRouteChooserDialog;
import android.support.v7.app.MediaRouteChooserDialogFragment;
import android.support.v7.app.MediaRouteControllerDialog;
import android.support.v7.app.MediaRouteControllerDialogFragment;
import android.support.v7.app.MediaRouteDialogFactory;
import android.view.View;
import android.widget.FrameLayout;
/**
* The Chrome implementation of the dialog factory so custom behavior can
* be injected for the disconnect button.
*/
public class ChromeMediaRouteDialogFactory extends MediaRouteDialogFactory {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static class SystemVisibilitySaver {
private int mSystemVisibility;
private boolean mRestoreSystemVisibility;
void saveSystemVisibility(Activity activity) {
// The Android APIs don't exist on old versions.
// TODO(aberent) this can go once the minSdkVersion has been updated in the manifests.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return;
// If we are in fullscreen we may have also have hidden the system UI. This
// is overridden when we display the dialog. Save the system UI visibility
// state so we can restore it.
FrameLayout decor = (FrameLayout) activity.getWindow().getDecorView();
mSystemVisibility = decor.getSystemUiVisibility();
mRestoreSystemVisibility = (
(mSystemVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0);
}
void restoreSystemVisibility(Activity activity) {
if (mRestoreSystemVisibility) {
FrameLayout decor = (FrameLayout) activity.getWindow().getDecorView();
// In some cases we come out of fullscreen before closing this dialog. In these
// cases we don't want to restore the system UI visibility state.
int systemVisibility = decor.getSystemUiVisibility();
if ((systemVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
decor.setSystemUiVisibility(mSystemVisibility);
}
}
}
}
@Override
public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
return new MediaRouteControllerDialogFragment() {
final SystemVisibilitySaver mVisibilitySaver = new SystemVisibilitySaver();
@Override
public Dialog onCreateDialog(Bundle saved) {
mVisibilitySaver.saveSystemVisibility(getActivity());
return new MediaRouteControllerDialog(getActivity());
}
@Override
public void onStop() {
super.onStop();
mVisibilitySaver.restoreSystemVisibility(getActivity());
}
};
}
@Override
public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
return new MediaRouteChooserDialogFragment() {
final SystemVisibilitySaver mVisibilitySaver = new SystemVisibilitySaver();
@Override
public MediaRouteChooserDialog onCreateChooserDialog(
Context context, Bundle savedInstanceState) {
mVisibilitySaver.saveSystemVisibility(getActivity());
return new MediaRouteChooserDialog(context);
}
@Override
public void onStop() {
super.onStop();
mVisibilitySaver.restoreSystemVisibility(getActivity());
}
};
}
}
// Copyright 2012 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.support.v7.app.MediaRouteButton;
import android.util.AttributeSet;
import android.view.View;
/**
* Cast button that wraps around a MediaRouteButton. We show the button only if there are available
* cast devices.
*/
public class FullscreenMediaRouteButton extends MediaRouteButton {
// Are we in the time window when the button should become visible if there're routes?
private boolean mVisibilityRequested;
/**
* The constructor invoked when inflating the button.
*/
public FullscreenMediaRouteButton(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mVisibilityRequested = false;
}
/**
* Set the necessary state for the button to work.
*/
/**
* Set the necessary state for the button to work
* @param controller the MediaRouteController controlling the route
*/
public void initialize(MediaRouteController controller) {
setRouteSelector(controller.buildMediaRouteSelector());
setDialogFactory(new ChromeMediaRouteDialogFactory());
}
@Override
public void setEnabled(boolean enabled) {
if (!RemoteMediaPlayerController.isRemotePlaybackEnabled()) return;
// TODO(aberent) not sure if this is still used, and in particular if mVisibilityRequest
// is still used.
// We need to check if the button was in the same state before to avoid doing anything,
// but we also need to update the current state for {@link #setButtonVisibility} to work.
boolean wasEnabled = isEnabled();
super.setEnabled(enabled);
if (wasEnabled == enabled) return;
if (enabled && mVisibilityRequested) {
setButtonVisibility(View.VISIBLE);
} else {
setVisibility(View.GONE);
}
}
private void setButtonVisibility(int visibility) {
// If the button is being set to visible, first make sure that it can even cast
// to anything before making it actually visible.
if (visibility == View.VISIBLE) {
if (isEnabled()) {
setVisibility(View.VISIBLE);
} else {
setVisibility(View.GONE);
}
} else {
setVisibility(visibility);
}
}
}
// Copyright 2014 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.KeyEvent;
import org.chromium.base.CommandLine;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.ChromeSwitches;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
import java.util.Set;
import javax.annotation.Nullable;
/**
* An abstract base class and factory for {@link TransportControl}s that are displayed on the lock
* screen.
*/
public abstract class LockScreenTransportControl
extends TransportControl implements MediaRouteController.UiListener {
private static final String TAG = "LockScreenTransportControl";
private static LockScreenTransportControl sInstance;
private MediaRouteController mMediaRouteController = null;
private static final Object LOCK = new Object();
private static boolean sDebug;
// Needed to get around findbugs complaints.
private static void setSDebug() {
sDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
}
protected LockScreenTransportControl() {
setSDebug();
}
/**
* {@link BroadcastReceiver} that receives the media button events from the lock screen and
* forwards the messages on to the {@link TransportControl}'s listeners.
*
* Ideally this class should be private, but public is required to create as a
* BroadcastReceiver.
*/
public static class MediaButtonIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (sDebug) Log.d(TAG, "Received intent: " + intent);
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
LockScreenTransportControl control = LockScreenTransportControl.getIfExists();
if (control == null) {
Log.w(TAG, "Event received when no LockScreenTransportControl exists");
return;
}
Set<Listener> listeners = control.getListeners();
// Ignore ACTION_DOWN. We'll get an ACTION_UP soon enough!
if (event.getAction() == KeyEvent.ACTION_DOWN) return;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
for (Listener listener : listeners) {
if (control.isPlaying()) {
listener.onPause();
} else {
listener.onPlay();
}
}
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
for (Listener listener : listeners)
listener.onStop();
break;
default:
Log.w(TAG, "Unrecognized event: " + event);
}
}
}
}
/**
* Get the unique LockScreenTransportControl, 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 LockScreenTransportControl getOrCreate(Context context,
@Nullable MediaRouteController mediaRouteController) {
Log.d(TAG, "getOrCreate called");
synchronized (LOCK) {
if (sInstance == null) {
// TODO(aberent) Investigate disabling the lock screen for Android L. It is
// supposedly deprecated, but the still seems to be the only way of controlling the
// wallpaper (which we set to the poster of the current video) when the phone is
// locked. Also, once the minSdkVersion is updated in the manifest, get rid of the
// code for older SDK versions.
if (!enabled()) {
return null;
} else if (android.os.Build.VERSION.SDK_INT < 16) {
sInstance = new LockScreenTransportControlV14(context);
} else if (android.os.Build.VERSION.SDK_INT < 18) {
sInstance = new LockScreenTransportControlV16(context);
} else {
sInstance = new LockScreenTransportControlV18(context);
}
}
sInstance.setVideoInfo(
new RemoteVideoInfo(null, 0, RemoteVideoInfo.PlayerState.STOPPED, 0, null));
sInstance.mMediaRouteController = mediaRouteController;
return sInstance;
}
}
protected MediaRouteController getMediaRouteController() {
return mMediaRouteController;
}
/**
* TODO(aberent) From PlayMovies code. Either remove this and get V14/15 working or combine V14
* and V16 versions only and change getOrCreate to only support V16 or later.
*
* @return true if lock screen transport controls should be used on this device.
*/
private static boolean enabled() {
// Lock screen controls don't work well prior to JB, see b/9101584
return android.os.Build.VERSION.SDK_INT >= 16;
}
/**
* Internal function for callbacks that need to get the current lock screen statically, but
* don't want to create a new one.
*
* @return the current lock screen, if any.
*/
@VisibleForTesting
static LockScreenTransportControl getIfExists() {
return sInstance;
}
@Override
public void hide() {
onLockScreenPlaybackStateChanged(null, PlayerState.STOPPED);
mMediaRouteController.removeUiListener(this);
}
@Override
public void show(PlayerState initialState) {
mMediaRouteController.addUiListener(this);
onLockScreenPlaybackStateChanged(null, initialState);
}
@Override
public void setRouteController(MediaRouteController controller) {
synchronized (LOCK) {
if (sInstance != null) sInstance.mMediaRouteController = controller;
}
}
@Override
public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
onLockScreenPlaybackStateChanged(oldState, newState);
}
protected abstract void onLockScreenPlaybackStateChanged(PlayerState oldState,
PlayerState newState);
protected abstract boolean isPlaying();
}
// Copyright 2014 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.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.util.Log;
import org.chromium.base.CommandLine;
import org.chromium.chrome.ChromeSwitches;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
/**
* An implementation of {@link LockScreenTransportControl} targeting platforms with an API of 14 or
* above.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
// TODO(aberent) This whole class is based upon RemoteControlClient, which is deprecated in
// the Android L SDK. It, however, still seems to be the only way of controlling the
// lock screen wallpaper. We need to investigate whether there is an alternative. See
// LockScreenTransportControl.java.
@SuppressWarnings("deprecation")
public class LockScreenTransportControlV14 extends LockScreenTransportControl {
private static final String TAG = "LockScreenTransportControlV14";
private static boolean sDebug;
private final AudioManager mAudioManager;
private final PendingIntent mMediaPendingIntent;
private final ComponentName mMediaEventReceiver;
private final AudioFocusListener mAudioFocusListener;
private android.media.RemoteControlClient mRemoteControlClient;
private boolean mIsPlaying;
protected LockScreenTransportControlV14(Context context) {
sDebug = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_CAST_DEBUG_LOGS);
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mMediaEventReceiver = new ComponentName(context.getPackageName(),
MediaButtonIntentReceiver.class.getName());
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setComponent(mMediaEventReceiver);
mMediaPendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), 0,
mediaButtonIntent, 0);
mAudioFocusListener = new AudioFocusListener();
}
@Override
public void onErrorChanged() {
if (hasError()) updatePlaybackState(android.media.RemoteControlClient.PLAYSTATE_ERROR);
}
@Override
public void onLockScreenPlaybackStateChanged(PlayerState oldState, PlayerState newState) {
if (sDebug) Log.d(TAG, "onLockScreenPlaybackStateChanged - new state: " + newState);
int playbackState = android.media.RemoteControlClient.PLAYSTATE_STOPPED;
boolean shouldBeRegistered = false;
if (newState != null) {
mIsPlaying = false;
shouldBeRegistered = true;
switch (newState) {
case PAUSED:
playbackState = android.media.RemoteControlClient.PLAYSTATE_PAUSED;
break;
case ERROR:
playbackState = android.media.RemoteControlClient.PLAYSTATE_ERROR;
break;
case PLAYING:
playbackState = android.media.RemoteControlClient.PLAYSTATE_PLAYING;
mIsPlaying = true;
break;
case LOADING:
playbackState = android.media.RemoteControlClient.PLAYSTATE_BUFFERING;
break;
default:
shouldBeRegistered = false;
break;
}
}
boolean registered = (mRemoteControlClient != null);
if (registered != shouldBeRegistered) {
if (shouldBeRegistered) {
register();
onVideoInfoChanged();
onPosterBitmapChanged();
} else {
unregister();
}
}
updatePlaybackState(playbackState);
}
@Override
public void onVideoInfoChanged() {
if (mRemoteControlClient == null) return;
RemoteVideoInfo videoInfo = getVideoInfo();
String title = null;
long duration = 0;
if (videoInfo != null) {
title = videoInfo.title;
duration = videoInfo.durationMillis;
}
android.media.RemoteControlClient.MetadataEditor editor = mRemoteControlClient.editMetadata(
true);
editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, title);
editor.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration);
updateBitmap(editor);
editor.apply();
}
@Override
public void onPosterBitmapChanged() {
if (mRemoteControlClient == null) return;
android.media.RemoteControlClient.MetadataEditor editor = mRemoteControlClient.editMetadata(
false);
updateBitmap(editor);
editor.apply();
}
private void updateBitmap(android.media.RemoteControlClient.MetadataEditor editor) {
// RemoteControlClient likes to recycle bitmaps that have been passed to it through
// BITMAP_KEY_ARTWORK. We can't go recycling bitmaps like this since they are also used by
// {@link ExpandedControllerActivity} and their life cycle is controller by
// {@link RemoteMediaPlayerController}. See crbug.com/356612
Bitmap src = getPosterBitmap();
Bitmap copy = src != null ? src.copy(src.getConfig(), true) : null;
editor.putBitmap(android.media.RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, copy);
}
protected final android.media.RemoteControlClient getRemoteControlClient() {
return mRemoteControlClient;
}
protected void register() {
if (sDebug) Log.d(TAG, "register called");
mRemoteControlClient = new android.media.RemoteControlClient(mMediaPendingIntent);
mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.USE_DEFAULT_STREAM_TYPE,
AudioManager.AUDIOFOCUS_GAIN);
mAudioManager.registerMediaButtonEventReceiver(mMediaEventReceiver);
mAudioManager.registerRemoteControlClient(mRemoteControlClient);
mRemoteControlClient.setTransportControlFlags(getTransportControlFlags());
}
protected void unregister() {
if (sDebug) Log.d(TAG, "unregister called");
mRemoteControlClient.editMetadata(true).apply();
mRemoteControlClient.setTransportControlFlags(0);
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mAudioManager.unregisterMediaButtonEventReceiver(mMediaEventReceiver);
mAudioManager.unregisterRemoteControlClient(mRemoteControlClient);
mRemoteControlClient = null;
}
protected void updatePlaybackState(int state) {
if (mRemoteControlClient != null) mRemoteControlClient.setPlaybackState(state);
}
protected int getTransportControlFlags() {
return android.media.RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE
| android.media.RemoteControlClient.FLAG_KEY_MEDIA_STOP;
}
private static class AudioFocusListener implements OnAudioFocusChangeListener {
@Override
public void onAudioFocusChange(int focusChange) {
// Do nothing, the listener is only used to later abandon audio focus.
}
}
// TODO(aberent): Investigate moving some or all of the MediaRouterController.Listener
// implementations to TransportControl. See http://crbug/354490.
@Override
public void onRouteSelected(String name, MediaRouteController mediaRouteController) {
setScreenName(name);
}
@Override
public void onRouteUnselected(MediaRouteController mediaRouteController) {
hide();
}
@Override
public void onPrepared(MediaRouteController mediaRouteController) {
}
@Override
public void onError(int error, String errorMessage) {
// Stop the session for all errors
hide();
}
@Override
public void onDurationUpdated(int durationMillis) {
RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
videoInfo.durationMillis = durationMillis;
setVideoInfo(videoInfo);
}
@Override
public void onPositionChanged(int positionMillis) {
RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
videoInfo.currentTimeMillis = positionMillis;
setVideoInfo(videoInfo);
}
@Override
public void onTitleChanged(String title) {
RemoteVideoInfo videoInfo = new RemoteVideoInfo(getVideoInfo());
videoInfo.title = title;
setVideoInfo(videoInfo);
}
@Override
protected boolean isPlaying() {
return mIsPlaying;
}
}
// Copyright 2014 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.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.v7.media.MediaRouter;
/**
* An implementation of {@link LockScreenTransportControl} targeting platforms with an API greater
* than 15. Extends {@link LockScreenTransportControlV14}, adding support for remote volume control.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
class LockScreenTransportControlV16 extends LockScreenTransportControlV14 {
private final MediaRouter mMediaRouter;
LockScreenTransportControlV16(Context context) {
super(context);
mMediaRouter = MediaRouter.getInstance(context);
}
@Override
protected void register() {
super.register();
mMediaRouter.addRemoteControlClient(getRemoteControlClient());
}
@Override
protected void unregister() {
mMediaRouter.removeRemoteControlClient(getRemoteControlClient());
super.unregister();
}
}
// Copyright 2014 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.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
/**
* An implementation of {@link LockScreenTransportControl} targeting platforms with an API greater
* than 17. Extends {@link LockScreenTransportControlV16}, adding support for seeking.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
// This whole class is based upon RemoteControlClient, which is deprecated (and non-functional) in
// the Android L SDK. Once the L SDK is released we will add code in LockSceenTransportControl to
// prevent the use of this class on Android L. See {@link LockScreenTransportControl}.
@SuppressWarnings("deprecation")
class LockScreenTransportControlV18 extends LockScreenTransportControlV16 {
private final PlaybackPositionUpdateListener mPlaybackPositionUpdateListener;
private final GetPlaybackPositionUpdateListener mGetPlaybackPositionUpdateListener;
LockScreenTransportControlV18(Context context) {
super(context);
mPlaybackPositionUpdateListener = new PlaybackPositionUpdateListener();
mGetPlaybackPositionUpdateListener = new GetPlaybackPositionUpdateListener();
}
@Override
protected void register() {
super.register();
getRemoteControlClient().setPlaybackPositionUpdateListener(mPlaybackPositionUpdateListener);
getRemoteControlClient().setOnGetPlaybackPositionListener(
mGetPlaybackPositionUpdateListener);
}
@Override
protected void unregister() {
getRemoteControlClient().setOnGetPlaybackPositionListener(null);
getRemoteControlClient().setPlaybackPositionUpdateListener(null);
super.unregister();
}
@Override
protected void updatePlaybackState(int state) {
RemoteVideoInfo videoInfo = getVideoInfo();
if (videoInfo != null && getRemoteControlClient() != null) {
getRemoteControlClient().setPlaybackState(state, videoInfo.currentTimeMillis, 1.0f);
} else {
super.updatePlaybackState(state);
}
}
@Override
protected int getTransportControlFlags() {
return super.getTransportControlFlags()
| android.media.RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE;
}
private class GetPlaybackPositionUpdateListener implements
android.media.RemoteControlClient.OnGetPlaybackPositionListener {
@Override
public long onGetPlaybackPosition() {
RemoteVideoInfo videoInfo = getVideoInfo();
return videoInfo == null ? 0 : videoInfo.currentTimeMillis;
}
}
private class PlaybackPositionUpdateListener implements
android.media.RemoteControlClient.OnPlaybackPositionUpdateListener {
@Override
public void onPlaybackPositionUpdate(long position) {
for (Listener listener : getListeners())
listener.onSeek((int) position);
}
}
}
// Copyright 2014 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.graphics.Bitmap;
import android.net.Uri;
import android.support.v7.media.MediaRouteSelector;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
/**
* Each MediaRouteController controls the routes to devices which support remote playback of
* particular categories of Media elements (e.g. all YouTube media elements, all media elements
* with simple http source URLs). The MediaRouteController is responsible for configuring
* and controlling remote playback of the media elements it supports.
*/
public interface MediaRouteController extends TransportControl.Listener {
/**
* Listener for events that are relevant to the state of the media and the media controls
*/
public interface MediaStateListener {
/**
* Called when the first route becomes available, or the last route
* is removed.
* @param available whether routes are available.
*/
public void onRouteAvailabilityChanged(boolean available);
/**
* Called when an error is detected by the media route controller
*/
public void onError();
/**
* Called when a seek completes on the current route
*/
public void onSeekCompleted();
/**
* Called when the current route is ready to be used
*/
public void onPrepared();
/**
* Called when a new route has been selected for Cast
* @param name the name of the route
*/
public void onRouteSelected(String name);
/**
* Called when the current route is unselected
*/
public void onRouteUnselected();
/**
* Called when the playback state changes (e.g. from Playing to Paused)
* @param newState the new playback state
*/
public void onPlaybackStateChanged(PlayerState newState);
/**
* @return the title of the video
*/
public String getTitle();
/**
* @return the poster bitmap
*/
public Bitmap getPosterBitmap();
}
/**
* Listener for events that are relevant to the Browser UI.
*/
public interface UiListener {
/**
* Called when a new route is selected
* @param name the name of the new route
* @param mediaRouteController the controller that selected the route
*/
void onRouteSelected(String name, MediaRouteController mediaRouteController);
/**
* Called when the current route is unselected
* @param mediaRouteController the controller that had the route.
*/
void onRouteUnselected(MediaRouteController mediaRouteController);
/**
* Called when the current route is ready to be used
* @param mediaRouteController the controller that has the route.
*/
void onPrepared(MediaRouteController mediaRouteController);
/**
* Called when an error is detected by the controller
* @param errorType One of the error types from CastMediaControlIntent
* @param message The message for the error
*/
void onError(int errorType, String message);
/**
* Called when the Playback state has changed (e.g. from playing to paused)
* @param oldState the old state
* @param newState the new state
*/
void onPlaybackStateChanged(PlayerState oldState, PlayerState newState);
/**
* Called when the duration of the currently playing video changes.
* @param durationMillis the new duration in ms.
*/
void onDurationUpdated(int durationMillis);
/**
* Called when the media route controller receives new information about the
* current position in the video.
* @param positionMillis the current position in the video in ms.
*/
void onPositionChanged(int positionMillis);
/**
* Called if the title of the video changes
* @param title the new title
*/
void onTitleChanged(String title);
}
/**
* Scan routes, and set up the MediaRouter object. This is called at every time we need to reset
* the state. Because of that, this function is idempotent. If that changes in the future, where
* this function gets called needs to be re-evaluated.
*
* @return false if device doesn't support cast, true otherwise.
*/
public boolean initialize();
/**
* Can this mediaRouteController handle a media element?
* @param sourceUrl the source
* @param frameUrl
* @return true if it can, false if it can't.
*/
public boolean canPlayMedia(String sourceUrl, String frameUrl);
/**
* @return A new MediaRouteSelector filtering the remote playback devices from all the routes.
*/
public MediaRouteSelector buildMediaRouteSelector();
/**
* @return Whether there're remote playback devices available.
*/
public boolean isRemotePlaybackAvailable();
/**
* @return Whether the currently selected device supports remote playback
*/
public boolean currentRouteSupportsRemotePlayback();
/**
* Checks if we want to reconnect, and if so starts trying to do so. Otherwise clears out the
* persistent request to reconnect.
*/
public boolean reconnectAnyExistingRoute();
/**
* Sets the video URL when it becomes known.
*
* This is the original video URL but if there's URL redirection, it will change as resolved by
* {@link MediaUrlResolver}.
*
* @param uri The video URL.
*/
public void setDataSource(Uri uri, String cookies);
/**
* Setup this object to discover new routes and register the necessary players.
*/
public void prepareMediaRoute();
/**
* Add a Listener that will listen to events from this object
*
* @param listener the Listener that will receive the events
*/
public void addUiListener(UiListener listener);
/**
* Removes a Listener from this object
*
* @param listener the Listener to remove
*/
public void removeUiListener(UiListener listener);
/**
* @return The currently selected route's friendly name, or null if there is none selected
*/
public String getRouteName();
/**
* @return true if this is currently using the default route, false if not.
*/
public boolean routeIsDefaultRoute();
/**
* Called to prepare the remote playback asyncronously. onPrepared() of the current remote media
* player object is called when the player is ready.
*
* @param startPositionMillis indicates where in the stream to start playing
*/
public void prepareAsync(String frameUrl, long startPositionMillis);
/**
* Sets the remote volume of the current route.
*
* @param delta The delta value in arbitrary "Android Volume Units".
*/
public void setRemoteVolume(int delta);
/**
* Resume paused playback of the current video.
*/
public void resume();
/**
* Pauses the currently playing video if any.
*/
public void pause();
/**
* Returns the current remote playback position. Estimates the current position by using the
* last known position and the current time.
*
* TODO(avayvod): Send periodic status update requests to update the position once in several
* seconds or so.
*
* @return The current position of the remote playback in milliseconds.
*/
public int getPosition();
/**
* @return The stream duration in milliseconds.
*/
public int getDuration();
/**
* @return Whether the video is currently being played.
*/
public boolean isPlaying();
/**
* @return Whether the video is being cast (any of playing/paused/loading/stopped).
*/
public boolean isBeingCast();
/**
* Initiates a seek request for the remote playback device to the specified position.
*
* @param msec The position to seek to, in milliseconds.
*/
public void seekTo(int msec);
/**
* Stop the current remote playback completely and release all resources.
*/
public void release();
/**
* @param player - the current player using this media route controller.
*/
public void setMediaStateListener(MediaStateListener listener);
/**
* @return the current VideoStateListener
*/
public MediaStateListener getMediaStateListener();
/**
* @return true if the video is new
*/
public boolean shouldResetState(MediaStateListener newListener);
@VisibleForTesting
public PlayerState getPlayerState();
/**
* Add a media state listener
* @param listener
*/
public void removeMediaStateListener(MediaStateListener listener);
/**
* Remove an existing media state listener
* @param listener
*/
public void addMediaStateListener(MediaStateListener listener);
/**
* Get the poster for the video, if any
* @return the poster bitmap, or Null.
*/
public Bitmap getPoster();
}
// 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.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.chromium.chrome.browser.ChromiumApplication;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
/**
* Resolves the final URL if it's a redirect. Works asynchronously, uses HTTP
* HEAD request to determine if the URL is redirected.
*/
public class MediaUrlResolver extends AsyncTask<Void, Void, MediaUrlResolver.Result> {
/**
* The interface to get the initial URI with cookies from and pass the final
* URI to.
*/
public interface Delegate {
/**
* @return the original URL to resolve.
*/
Uri getUri();
/**
* @return the cookies to fetch the URL with.
*/
String getCookies();
/**
* Passes the resolved URL to the delegate.
*
* @param uri the resolved URL.
*/
void setUri(Uri uri, Header[] headers);
}
protected static final class Result {
private final String mUri;
private final Header[] mRelevantHeaders;
public Result(String uri, Header[] relevantHeaders) {
mUri = uri;
mRelevantHeaders =
relevantHeaders != null
? Arrays.copyOf(relevantHeaders, relevantHeaders.length)
: null;
}
public String getUri() {
return mUri;
}
public Header[] getRelevantHeaders() {
return mRelevantHeaders != null
? Arrays.copyOf(mRelevantHeaders, mRelevantHeaders.length)
: null;
}
}
private static final String TAG = "MediaUrlResolver";
private static final String CORS_HEADER_NAME = "Access-Control-Allow-Origin";
private static final String COOKIES_HEADER_NAME = "Cookies";
private static final String USER_AGENT_HEADER_NAME = "User-Agent";
private static final String RANGE_HEADER_NAME = "Range";
// We don't want to necessarily fetch the whole video but we don't want to miss the CORS header.
// Assume that 64k should be more than enough to keep all the headers.
private static final String RANGE_HEADER_VALUE = "bytes: 0-65536";
private final Context mContext;
private final Delegate mDelegate;
/**
* The constructor
* @param context the context to use to resolve the URL
* @param delegate The customer for this URL resolver.
*/
public MediaUrlResolver(Context context, Delegate delegate) {
mContext = context;
mDelegate = delegate;
}
@Override
protected MediaUrlResolver.Result doInBackground(Void... params) {
Uri uri = mDelegate.getUri();
String url = uri.toString();
Header[] relevantHeaders = null;
String cookies = mDelegate.getCookies();
String userAgent = ChromiumApplication.getBrowserUserAgent();
// URL may already be partially percent encoded; double percent encoding will break
// things, so decode it before sanitizing it.
String sanitizedUrl = sanitizeUrl(Uri.decode(url));
// If we failed to sanitize the URL (e.g. because the host name contains underscores) then
// HttpURLConnection won't work,so we can't follow redirections. Just try to use it as is.
// TODO (aberent): Find out if there is a way of following redirections that is not so
// strict on the URL format.
if (!sanitizedUrl.equals("")) {
HttpURLConnection urlConnection = null;
try {
URL requestUrl = new URL(sanitizedUrl);
urlConnection = (HttpURLConnection) requestUrl.openConnection();
if (!TextUtils.isEmpty(cookies)) {
urlConnection.setRequestProperty(COOKIES_HEADER_NAME, cookies);
}
urlConnection.setRequestProperty(USER_AGENT_HEADER_NAME, userAgent);
urlConnection.setRequestProperty(RANGE_HEADER_NAME, RANGE_HEADER_VALUE);
// This triggers resolving the URL and receiving the headers.
urlConnection.getHeaderFields();
url = urlConnection.getURL().toString();
String corsHeader = urlConnection.getHeaderField(CORS_HEADER_NAME);
if (corsHeader != null) {
relevantHeaders = new Header[1];
relevantHeaders[0] = new BasicHeader(CORS_HEADER_NAME, corsHeader);
}
} catch (IOException e) {
Log.e(TAG, "Failed to fetch the final URI", e);
url = "";
}
if (urlConnection != null) urlConnection.disconnect();
}
return new MediaUrlResolver.Result(url, relevantHeaders);
}
@Override
protected void onPostExecute(MediaUrlResolver.Result result) {
String url = result.getUri();
Uri uri = "".equals(url) ? Uri.EMPTY : Uri.parse(url);
mDelegate.setUri(uri, result.getRelevantHeaders());
}
private String sanitizeUrl(String unsafeUrl) {
URL url;
URI uri;
try {
url = new URL(unsafeUrl);
uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(),
url.getPath(), url.getQuery(), url.getRef());
return uri.toURL().toString();
} catch (URISyntaxException syntaxException) {
Log.w(TAG, "URISyntaxException " + syntaxException);
} catch (MalformedURLException malformedUrlException) {
Log.w(TAG, "MalformedURLException " + malformedUrlException);
}
return "";
}
}
\ No newline at end of file
// Copyright 2014 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 org.chromium.base.JNINamespace;
import org.chromium.base.library_loader.LibraryLoader;
/**
* Record statistics on interesting cast events and actions.
*/
@JNINamespace("remote_media")
public class RecordCastAction {
// UMA histogram values for the device types the user could select.
// Keep in sync with the enum in uma_record_action.cc
public static final int DEVICE_TYPE_CAST_GENERIC = 0;
public static final int DEVICE_TYPE_CAST_YOUTUBE = 1;
public static final int DEVICE_TYPE_NON_CAST_YOUTUBE = 2;
public static final int DEVICE_TYPE_COUNT = 3;
/**
* Record the type of cast receiver we to which we are casting.
* @param playerType the type of cast receiver.
*/
public static void remotePlaybackDeviceSelected(int playerType) {
assert playerType >= 0
&& playerType < RecordCastAction.DEVICE_TYPE_COUNT;
if (LibraryLoader.isInitialized()) nativeRecordRemotePlaybackDeviceSelected(playerType);
}
/**
* Record that a remote playback was requested. This is intended to record all playback
* requests, whether they were user initiated or was an auto-playback resulting from the user
* selecting the device initially.
*/
public static void castPlayRequested() {
if (LibraryLoader.isInitialized()) nativeRecordCastPlayRequested();
}
/**
* Record the result of the cast playback request.
*
* @param castSucceeded true if the playback succeeded, false if there was an error
*/
public static void castDefaultPlayerResult(boolean castSucceeded) {
if (LibraryLoader.isInitialized()) nativeRecordCastDefaultPlayerResult(castSucceeded);
}
/**
* Record the result of casting a YouTube video.
*
* @param castSucceeded true if the playback succeeded, false if there was an error
*/
public static void castYouTubePlayerResult(boolean castSucceeded) {
if (LibraryLoader.isInitialized()) nativeRecordCastYouTubePlayerResult(castSucceeded);
}
/**
* Record the amount of time remaining on the video when the remote playback stops.
*
* @param videoLengthMs the total length of the video in milliseconds
* @param timeRemainingMs the remaining time in the video in milliseconds
*/
public static void castEndedTimeRemaining(int videoLengthMs, int timeRemainingMs) {
if (LibraryLoader.isInitialized()) {
nativeRecordCastEndedTimeRemaining(videoLengthMs, timeRemainingMs);
}
}
/**
* Record the type of the media being cast.
*
* @param mediaType the type of the media being casted, see media/base/container_names.h for
* possible media types.
*/
public static void castMediaType(int mediaType) {
if (LibraryLoader.isInitialized()) nativeRecordCastMediaType(mediaType);
}
// Cast sending
private static native void nativeRecordRemotePlaybackDeviceSelected(int deviceType);
private static native void nativeRecordCastPlayRequested();
private static native void nativeRecordCastDefaultPlayerResult(boolean castSucceeded);
private static native void nativeRecordCastYouTubePlayerResult(boolean castSucceeded);
private static native void nativeRecordCastEndedTimeRemaining(
int videoLengthMs, int timeRemainingMs);
private static native void nativeRecordCastMediaType(int mediaType);
}
// 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.SharedPreferences;
import android.preference.PreferenceManager;
/**
* Maintains the persistent settings that the app needs for casting.
*/
public class RemotePlaybackSettings {
private static final String DEVICE_ID_KEY = "remote_device_id";
private static final String RECONNECT_KEY = "reconnect_remote_device";
private static final String LAST_VIDEO_TIME_REMAINING_KEY = "last_video_time_remaining";
private static final String LAST_VIDEO_TIME_PLAYED_KEY = "last_video_time_played";
private static final String LAST_VIDEO_STATE_KEY = "last_video_state";
private static final String PLAYER_IN_USE_KEY = "player_in_use";
private static final String URI_PLAYING = "uri playing";
private static final String SESSION_ID = "session_id";
public static String getDeviceId(Context context) {
return getPreferences(context).getString(DEVICE_ID_KEY, null);
}
public static void setDeviceId(Context context, String deviceId) {
putValue(getPreferences(context), DEVICE_ID_KEY, deviceId);
}
public static String getSessionId(Context context) {
return getPreferences(context).getString(SESSION_ID, null);
}
public static void setSessionId(Context context, String sessionId) {
putValue(getPreferences(context), SESSION_ID, sessionId);
}
public static boolean getShouldReconnectToRemote(Context context) {
return getPreferences(context).getBoolean(RECONNECT_KEY, false);
}
public static void setShouldReconnectToRemote(Context context, boolean reconnect) {
putValue(getPreferences(context), RECONNECT_KEY, reconnect);
}
public static long getRemainingTime(Context context) {
return getPreferences(context).getLong(LAST_VIDEO_TIME_REMAINING_KEY, 0);
}
public static void setRemainingTime(Context context, long time) {
putValue(getPreferences(context), LAST_VIDEO_TIME_REMAINING_KEY, time);
}
public static long getLastPlayedTime(Context context) {
return getPreferences(context).getLong(LAST_VIDEO_TIME_PLAYED_KEY, 0);
}
public static void setLastPlayedTime(Context context, long time) {
putValue(getPreferences(context), LAST_VIDEO_TIME_PLAYED_KEY, time);
}
public static String getLastVideoState(Context context) {
return getPreferences(context).getString(LAST_VIDEO_STATE_KEY, null);
}
public static void setLastVideoState(Context context, String title) {
putValue(getPreferences(context), LAST_VIDEO_STATE_KEY, title);
}
public static String getPlayerInUse(Context context) {
return getPreferences(context).getString(PLAYER_IN_USE_KEY, null);
}
public static void setPlayerInUse(Context context, String player) {
putValue(getPreferences(context), PLAYER_IN_USE_KEY, player);
}
public static String getUriPlaying(Context context) {
return getPreferences(context).getString(URI_PLAYING, null);
}
public static void setUriPlaying(Context context, String url) {
putValue(getPreferences(context), URI_PLAYING, url);
}
private static void putValue(SharedPreferences preferences, String key, String value) {
SharedPreferences.Editor editor = preferences.edit();
editor.putString(key, value);
editor.apply();
}
private static void putValue(SharedPreferences preferences, String key, boolean value) {
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(key, value);
editor.apply();
}
private static void putValue(SharedPreferences preferences, String key, long value) {
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(key, value);
editor.apply();
}
private static SharedPreferences getPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
}
// 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.text.TextUtils;
/**
* Exposes information about the current video to the external clients.
*/
public class RemoteVideoInfo {
/**
* This lists all the states that the remote video can be in.
*/
public enum PlayerState {
/** The remote player is currently stopped. */
STOPPED,
/** The remote player is buffering this video. */
LOADING,
/** The remote player is playing this video. */
PLAYING,
/** The remote player is paused. */
PAUSED,
/** The remote player is in an error state. */
ERROR,
/** The remote player has been replaced by another player (so the current session has
* finished) */
INVALIDATED,
/** The remote video has completed playing. */
FINISHED
}
/**
* The title of the video
*/
public String title;
/**
* The duration of the video
*/
public int durationMillis;
/**
* The current state of the video
*/
public PlayerState state;
/**
* The last known position in the video
*/
public int currentTimeMillis;
/**
* The current error message, if any
*/
// TODO(aberent) At present nothing sets this to anything other than Null.
public String errorMessage;
/**
* Create a new RemoteVideoInfo
* @param title
* @param durationMillis
* @param state
* @param currentTimeMillis
* @param errorMessage
*/
public RemoteVideoInfo(String title, int durationMillis, PlayerState state,
int currentTimeMillis, String errorMessage) {
this.title = title;
this.durationMillis = durationMillis;
this.state = state;
this.currentTimeMillis = currentTimeMillis;
this.errorMessage = errorMessage;
}
/**
* Copy a remote video info
* @param other the source.
*/
public RemoteVideoInfo(RemoteVideoInfo other) {
this(other.title, other.durationMillis, other.state, other.currentTimeMillis,
other.errorMessage);
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof RemoteVideoInfo)) {
return false;
}
RemoteVideoInfo other = (RemoteVideoInfo) obj;
return durationMillis == other.durationMillis
&& currentTimeMillis == other.currentTimeMillis
&& state == other.state
&& TextUtils.equals(title, other.title)
&& TextUtils.equals(errorMessage, other.errorMessage);
}
@Override
public int hashCode() {
int result = durationMillis;
result = 31 * result + currentTimeMillis;
result = 31 * result + (title == null ? 0 : title.hashCode());
result = 31 * result + (state == null ? 0 : state.hashCode());
result = 31 * result + (errorMessage == null ? 0 : errorMessage.hashCode());
return result;
}
}
...@@ -1482,6 +1482,54 @@ Drag from top to exit. ...@@ -1482,6 +1482,54 @@ Drag from top to exit.
Your tabs and apps are now in the same place for easier access. You can turn this feature off in <ph name="BEGIN_LINK">&lt;link&gt;</ph>Settings<ph name="END_LINK">&lt;/link&gt;</ph>. Your tabs and apps are now in the same place for easier access. You can turn this feature off in <ph name="BEGIN_LINK">&lt;link&gt;</ph>Settings<ph name="END_LINK">&lt;/link&gt;</ph>.
</message> </message>
<message name="IDS_CAST_NOTIFICATION_STOPPED" desc="AtHome notification text when stopped. [CHAR LIMIT=40]">
Stopped
</message>
<message name="IDS_CAST_NOTIFICATION_FINISHED" desc="AtHome notification text when finished. [CHAR LIMIT=40]">
Finished
</message>
<message name="IDS_CAST_NOTIFICATION_PAUSED" desc="AtHome notification text when paused. [CHAR LIMIT=40]">
Paused
</message>
<!-- Messages for remote media playback (casting) -->
<message name="IDS_CAST_NOTIFICATION_LOADING" desc="AtHome notification text when loading a video. [CHAR LIMIT=40]">
Loading video
</message>
<message name="IDS_CAST_NOTIFICATION_PLAYING" desc="AtHome notification text when playing a video. [CHAR LIMIT=40]">
Playing video
</message>
<message name="IDS_CAST_NOTIFICATION_LOADING_FOR_VIDEO" desc="AtHome notification text when loading a given video. [CHAR LIMIT=40]">
Loading “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>
</message>
<message name="IDS_CAST_NOTIFICATION_PLAYING_FOR_VIDEO" desc="AtHome notification text when playing a given video. [CHAR LIMIT=40]">
Playing “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>
</message>
<message name="IDS_CAST_NOTIFICATION_PAUSED_FOR_VIDEO" desc="AtHome notification text when paused in a given video. [CHAR LIMIT=40]">
Paused “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>
</message>
<message name="IDS_CAST_NOTIFICATION_FINISHED_FOR_VIDEO" desc="AtHome notification text when finished playing a given video. [CHAR LIMIT=40]">
Finished “<ph name="VIDEO_TITLE">%1$s<ex>Psy - Gangnam Style - YouTube</ex></ph>
</message>
<message name="IDS_CAST_CASTING_VIDEO" desc="AtHome text to tell user which screen casting is happening. [CHAR LIMIT=40]">
Casting to <ph name="SCREEN_NAME">%1$s<ex>Living Room TV</ex></ph>
</message>
<message name="IDS_CAST_ERROR_PLAYING_VIDEO" desc="The message shown to the user when playing a video to the Chromecast fails.">
Cannot play video on <ph name="SCREEN_NAME">%1$s<ex>Living Room TV</ex></ph>.
</message>
<message name="IDS_CAST_PERMISSION_ERROR_PLAYING_VIDEO" desc="The message shown to the user when trying to play a video that the domain does not let us cast.">
Unable to cast video due to site restrictions
</message>
<message name="IDS_ACCESSIBILITY_PLAY" desc="Content description for the play button that starts playing the media.">
Play
</message>
<message name="IDS_ACCESSIBILITY_PAUSE" desc="Content description for the pause button that pauses playing the media.">
Pause
</message>
<message name="IDS_ACCESSIBILITY_STOP" desc="Content description for the stop button that stops playing the media.">
Stop
</message>
</messages> </messages>
</release> </release>
</grit> </grit>
...@@ -192,5 +192,30 @@ ...@@ -192,5 +192,30 @@
<meta-data android:name="org.chromium.chrome.browser.SERVICE_TAB_LAUNCHER" <meta-data android:name="org.chromium.chrome.browser.SERVICE_TAB_LAUNCHER"
android:value="org.chromium.chrome.shell.ChromeShellServiceTabLauncher" /> android:value="org.chromium.chrome.shell.ChromeShellServiceTabLauncher" />
<!-- Activity, service, and meta-data to support casting to Chromecast -->
<!-- Expanded controller activity is displayed when the Cast Notification is clicked -->
<activity android:name="org.chromium.chrome.browser.media.remote.ExpandedControllerActivity"
android:theme="@style/MainTheme"
android:label="Chrome.ExpandedControllerActivity"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:noHistory="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
android:excludeFromRecents="true">
</activity>
<service android:name="org.chromium.chrome.browser.media.remote.NotificationTransportControl$ListenerService" />
<!-- Media route controllers to use for remote playback (cast).
This is here, rather than in code, since it varies between upstream and downstream,
yet we need this list of classes in the notification service, which belongs upstream
and doesn't run the downstream Clank startup code. The Cast code will, for each media element,
choose the first MediaRouteController that can play it, so the order of the list can be important.
The most specific MediaRouteControllers should be listed first, followed by more generic ones -->
<meta-data android:name="org.chromium.content.browser.REMOTE_MEDIA_PLAYERS"
android:value="org.chromium.chrome.browser.media.remote.DefaultMediaRouteController"/>
</application> </application>
</manifest> </manifest>
specific_include_rules = {
# Fix layering violation crbug.com/396828
"chrome_main_delegate_android.cc": [
"+content/browser/media/android/browser_media_player_manager.h",
],
}
\ No newline at end of file
...@@ -8,9 +8,23 @@ ...@@ -8,9 +8,23 @@
#include "base/trace_event/trace_event.h" #include "base/trace_event/trace_event.h"
#include "chrome/browser/android/chrome_startup_flags.h" #include "chrome/browser/android/chrome_startup_flags.h"
#include "chrome/browser/android/metrics/uma_utils.h" #include "chrome/browser/android/metrics/uma_utils.h"
#include "chrome/browser/android/metrics/uma_utils.h"
#include "chrome/browser/media/android/remote/remote_media_player_manager.h"
#include "components/startup_metric_utils/startup_metric_utils.h" #include "components/startup_metric_utils/startup_metric_utils.h"
#include "content/browser/media/android/browser_media_player_manager.h"
#include "content/public/browser/browser_main_runner.h" #include "content/public/browser/browser_main_runner.h"
namespace {
content::BrowserMediaPlayerManager* CreateRemoteMediaPlayerManager(
content::RenderFrameHost* render_frame_host,
content::MediaPlayersObserver* audio_monitor) {
return new remote_media::RemoteMediaPlayerManager(render_frame_host,
audio_monitor);
}
} // namespace
// ChromeMainDelegateAndroid is created when the library is loaded. It is always // ChromeMainDelegateAndroid is created when the library is loaded. It is always
// done in the process's main Java thread. But for non browser process, e.g. // done in the process's main Java thread. But for non browser process, e.g.
// renderer process, it is not the native Chrome's main thread. // renderer process, it is not the native Chrome's main thread.
...@@ -19,7 +33,6 @@ ChromeMainDelegateAndroid::ChromeMainDelegateAndroid() { ...@@ -19,7 +33,6 @@ ChromeMainDelegateAndroid::ChromeMainDelegateAndroid() {
ChromeMainDelegateAndroid::~ChromeMainDelegateAndroid() { ChromeMainDelegateAndroid::~ChromeMainDelegateAndroid() {
} }
void ChromeMainDelegateAndroid::SandboxInitialized( void ChromeMainDelegateAndroid::SandboxInitialized(
const std::string& process_type) { const std::string& process_type) {
ChromeMainDelegate::SandboxInitialized(process_type); ChromeMainDelegate::SandboxInitialized(process_type);
...@@ -49,5 +62,9 @@ int ChromeMainDelegateAndroid::RunProcess( ...@@ -49,5 +62,9 @@ int ChromeMainDelegateAndroid::RunProcess(
bool ChromeMainDelegateAndroid::BasicStartupComplete(int* exit_code) { bool ChromeMainDelegateAndroid::BasicStartupComplete(int* exit_code) {
SetChromeSpecificCommandLineFlags(); SetChromeSpecificCommandLineFlags();
content::BrowserMediaPlayerManager::RegisterFactory(
&CreateRemoteMediaPlayerManager);
return ChromeMainDelegate::BasicStartupComplete(exit_code); return ChromeMainDelegate::BasicStartupComplete(exit_code);
} }
...@@ -67,6 +67,8 @@ ...@@ -67,6 +67,8 @@
#include "chrome/browser/history/android/sqlite_cursor.h" #include "chrome/browser/history/android/sqlite_cursor.h"
#include "chrome/browser/invalidation/invalidation_service_factory_android.h" #include "chrome/browser/invalidation/invalidation_service_factory_android.h"
#include "chrome/browser/lifetime/application_lifetime_android.h" #include "chrome/browser/lifetime/application_lifetime_android.h"
#include "chrome/browser/media/android/remote/record_cast_action.h"
#include "chrome/browser/media/android/remote/remote_media_player_bridge.h"
#include "chrome/browser/net/spdyproxy/data_reduction_proxy_settings_android.h" #include "chrome/browser/net/spdyproxy/data_reduction_proxy_settings_android.h"
#include "chrome/browser/notifications/notification_ui_manager_android.h" #include "chrome/browser/notifications/notification_ui_manager_android.h"
#include "chrome/browser/password_manager/credential_android.h" #include "chrome/browser/password_manager/credential_android.h"
...@@ -228,6 +230,9 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = { ...@@ -228,6 +230,9 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = {
{"ProfileDownloader", RegisterProfileDownloader}, {"ProfileDownloader", RegisterProfileDownloader},
{"ProfileSyncService", ProfileSyncServiceAndroid::Register}, {"ProfileSyncService", ProfileSyncServiceAndroid::Register},
{"RecentlyClosedBridge", RecentlyClosedTabsBridge::Register}, {"RecentlyClosedBridge", RecentlyClosedTabsBridge::Register},
{"RecordCastAction", remote_media::RegisterRecordCastAction},
{"RemoteMediaPlayerBridge",
remote_media::RemoteMediaPlayerBridge::RegisterRemoteMediaPlayerBridge},
{"SavePasswordInfoBar", SavePasswordInfoBar::Register}, {"SavePasswordInfoBar", SavePasswordInfoBar::Register},
{"SceneLayer", chrome::android::RegisterSceneLayer}, {"SceneLayer", chrome::android::RegisterSceneLayer},
{"ServiceTabLauncher", ServiceTabLauncher::RegisterServiceTabLauncher}, {"ServiceTabLauncher", ServiceTabLauncher::RegisterServiceTabLauncher},
......
specific_include_rules = {
# TODO(aberent): Fix layering violation crbug.com/396828
"remote_media_player_manager\.": [
"+content/browser/media/android/browser_media_player_manager.h",
],
# TODO(aberent): Fix layering violation crbug.com/396828
"remote_media_player_manager\.cc": [
"+content/common/media/media_player_messages_android.h",
],
# TODO(aberent): Fix layering violation crbug.com/396828
"remote_media_player_bridge\.cc": [
"+content/browser/android/content_view_core_impl.h"
],
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment