Commit fc8716ea authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

WebLayer MediaRouter: add services and notification IDs.

The services will be foregrounded and the notifications will be visible
when a cast session is ongoing. The service/notification ID design
matches that of MediaSession. The notification IDs are defined in client
xml, so //components/media_router code needs to ask the embedder for
these values.

Casting is still not functional in WebLayer even after this change as
a CastOptionsProvider is required.

Bug: 1057100
Change-Id: I4d6fa44d1073a0a26c618c501e58251f118379c1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2467197Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarThomas Guilbert <tguilbert@chromium.org>
Auto-Submit: Evan Stade <estade@chromium.org>
Commit-Queue: Evan Stade <estade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#817314}
parent 25f747b7
...@@ -26,6 +26,12 @@ ...@@ -26,6 +26,12 @@
<!-- Media playback notification --> <!-- Media playback notification -->
<item type="id" name="media_playback_notification" /> <item type="id" name="media_playback_notification" />
<!-- Remote Playback API notification -->
<item type="id" name="remote_playback_notification" />
<!-- Presentation API notification -->
<item type="id" name="presentation_notification" />
<!-- Sync UI constants --> <!-- Sync UI constants -->
<item type="id" name="passphrase_type_list" /> <item type="id" name="passphrase_type_list" />
......
...@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager; ...@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager;
import org.chromium.base.ApplicationStatus; import org.chromium.base.ApplicationStatus;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.document.ChromeIntentUtil; import org.chromium.chrome.browser.document.ChromeIntentUtil;
import org.chromium.chrome.browser.media.ui.ChromeMediaNotificationManager; import org.chromium.chrome.browser.media.ui.ChromeMediaNotificationManager;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
...@@ -41,6 +42,16 @@ public class ChromeMediaRouterClient extends MediaRouterClient { ...@@ -41,6 +42,16 @@ public class ChromeMediaRouterClient extends MediaRouterClient {
ChromeMediaNotificationManager.show(notificationInfo); ChromeMediaNotificationManager.show(notificationInfo);
} }
@Override
public int getPresentationNotificationId() {
return R.id.presentation_notification;
}
@Override
public int getRemotingNotificationId() {
return R.id.remote_playback_notification;
}
@Override @Override
public FragmentManager getSupportFragmentManager(WebContents initiator) { public FragmentManager getSupportFragmentManager(WebContents initiator) {
FragmentActivity currentActivity = FragmentActivity currentActivity =
......
...@@ -187,7 +187,7 @@ class ChromeMediaNotificationControllerDelegate implements MediaNotificationCont ...@@ -187,7 +187,7 @@ class ChromeMediaNotificationControllerDelegate implements MediaNotificationCont
* This class is used internally but has to be public to be able to launch the service. * This class is used internally but has to be public to be able to launch the service.
*/ */
public static final class CastListenerServiceImpl extends ListenerServiceImpl { public static final class CastListenerServiceImpl extends ListenerServiceImpl {
static final int NOTIFICATION_ID = R.id.remote_notification; static final int NOTIFICATION_ID = R.id.remote_playback_notification;
public CastListenerServiceImpl() { public CastListenerServiceImpl() {
super(NOTIFICATION_ID); super(NOTIFICATION_ID);
......
...@@ -168,7 +168,6 @@ android_resources("java_resources") { ...@@ -168,7 +168,6 @@ android_resources("java_resources") {
"java/res/drawable/ic_cast_dark_chrome.xml", "java/res/drawable/ic_cast_dark_chrome.xml",
"java/res/layout/caf_controller_media_route_button.xml", "java/res/layout/caf_controller_media_route_button.xml",
"java/res/layout/expanded_cast_controller.xml", "java/res/layout/expanded_cast_controller.xml",
"java/res/values/ids.xml",
"java/res/values/styles.xml", "java/res/values/styles.xml",
] ]
deps = [ deps = [
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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.
-->
<resources>
<!-- RemotePlayback API notification -->
<item type="id" name="remote_notification" />
<!-- Presentation API notification -->
<item type="id" name="presentation_notification" />
</resources>
...@@ -49,6 +49,12 @@ public abstract class MediaRouterClient { ...@@ -49,6 +49,12 @@ public abstract class MediaRouterClient {
*/ */
public abstract void showNotification(MediaNotificationInfo notificationInfo); public abstract void showNotification(MediaNotificationInfo notificationInfo);
/** Returns the ID to be used for Presentation API notifications. */
public abstract int getPresentationNotificationId();
/** Returns the ID to be used for Remote Playback API notifications. */
public abstract int getRemotingNotificationId();
/** /**
* @param initiator the web contents that initiated the request. * @param initiator the web contents that initiated the request.
* @return a {@link FragmentManager} suitable for displaying a media router {@link * @return a {@link FragmentManager} suitable for displaying a media router {@link
......
...@@ -31,6 +31,16 @@ public class TestMediaRouterClient extends MediaRouterClient { ...@@ -31,6 +31,16 @@ public class TestMediaRouterClient extends MediaRouterClient {
@Override @Override
public void showNotification(MediaNotificationInfo notificationInfo) {} public void showNotification(MediaNotificationInfo notificationInfo) {}
@Override
public int getPresentationNotificationId() {
return 2;
}
@Override
public int getRemotingNotificationId() {
return 3;
}
@Override @Override
public FragmentManager getSupportFragmentManager(WebContents initiator) { public FragmentManager getSupportFragmentManager(WebContents initiator) {
return null; return null;
......
...@@ -7,7 +7,7 @@ package org.chromium.components.media_router.caf; ...@@ -7,7 +7,7 @@ package org.chromium.components.media_router.caf;
import android.content.Intent; import android.content.Intent;
import org.chromium.components.browser_ui.media.MediaNotificationUma; import org.chromium.components.browser_ui.media.MediaNotificationUma;
import org.chromium.components.media_router.R; import org.chromium.components.media_router.MediaRouterClient;
/** NotificationController implementation for presentation. */ /** NotificationController implementation for presentation. */
public class CafNotificationController extends BaseNotificationController { public class CafNotificationController extends BaseNotificationController {
...@@ -28,6 +28,6 @@ public class CafNotificationController extends BaseNotificationController { ...@@ -28,6 +28,6 @@ public class CafNotificationController extends BaseNotificationController {
@Override @Override
public int getNotificationId() { public int getNotificationId() {
return R.id.presentation_notification; return MediaRouterClient.getInstance().getPresentationNotificationId();
} }
} }
...@@ -8,7 +8,7 @@ import android.content.Intent; ...@@ -8,7 +8,7 @@ import android.content.Intent;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.components.browser_ui.media.MediaNotificationUma; import org.chromium.components.browser_ui.media.MediaNotificationUma;
import org.chromium.components.media_router.R; import org.chromium.components.media_router.MediaRouterClient;
import org.chromium.components.media_router.caf.BaseNotificationController; import org.chromium.components.media_router.caf.BaseNotificationController;
import org.chromium.components.media_router.caf.BaseSessionController; import org.chromium.components.media_router.caf.BaseSessionController;
...@@ -30,6 +30,6 @@ public class RemotingNotificationController extends BaseNotificationController { ...@@ -30,6 +30,6 @@ public class RemotingNotificationController extends BaseNotificationController {
@Override @Override
public int getNotificationId() { public int getNotificationId() {
return R.id.remote_notification; return MediaRouterClient.getInstance().getRemotingNotificationId();
} }
} }
...@@ -638,7 +638,7 @@ content::ControllerPresentationServiceDelegate* ...@@ -638,7 +638,7 @@ content::ControllerPresentationServiceDelegate*
ContentBrowserClientImpl::GetControllerPresentationServiceDelegate( ContentBrowserClientImpl::GetControllerPresentationServiceDelegate(
content::WebContents* web_contents) { content::WebContents* web_contents) {
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
if (WebLayerFactoryImplAndroid::GetClientMajorVersion() < 87) if (WebLayerFactoryImplAndroid::GetClientMajorVersion() < 88)
return nullptr; return nullptr;
if (base::FeatureList::IsEnabled(features::kMediaRouter)) { if (base::FeatureList::IsEnabled(features::kMediaRouter)) {
......
...@@ -150,6 +150,7 @@ android_library("java") { ...@@ -150,6 +150,7 @@ android_library("java") {
"org/chromium/weblayer_private/media/MediaRouteDialogFragmentImpl.java", "org/chromium/weblayer_private/media/MediaRouteDialogFragmentImpl.java",
"org/chromium/weblayer_private/media/MediaRouterClientImpl.java", "org/chromium/weblayer_private/media/MediaRouterClientImpl.java",
"org/chromium/weblayer_private/media/MediaSessionManager.java", "org/chromium/weblayer_private/media/MediaSessionManager.java",
"org/chromium/weblayer_private/media/MediaSessionNotificationHelper.java",
"org/chromium/weblayer_private/media/MediaStreamManager.java", "org/chromium/weblayer_private/media/MediaStreamManager.java",
"org/chromium/weblayer_private/metrics/MetricsServiceClient.java", "org/chromium/weblayer_private/metrics/MetricsServiceClient.java",
"org/chromium/weblayer_private/metrics/UmaUtils.java", "org/chromium/weblayer_private/metrics/UmaUtils.java",
...@@ -362,6 +363,7 @@ android_library("interfaces_java") { ...@@ -362,6 +363,7 @@ android_library("interfaces_java") {
"org/chromium/weblayer_private/interfaces/NavigationState.java", "org/chromium/weblayer_private/interfaces/NavigationState.java",
"org/chromium/weblayer_private/interfaces/NewTabType.java", "org/chromium/weblayer_private/interfaces/NewTabType.java",
"org/chromium/weblayer_private/interfaces/ObjectWrapper.java", "org/chromium/weblayer_private/interfaces/ObjectWrapper.java",
"org/chromium/weblayer_private/interfaces/RemoteMediaServiceConstants.java",
"org/chromium/weblayer_private/interfaces/ScrollNotificationType.java", "org/chromium/weblayer_private/interfaces/ScrollNotificationType.java",
"org/chromium/weblayer_private/interfaces/SettingType.java", "org/chromium/weblayer_private/interfaces/SettingType.java",
"org/chromium/weblayer_private/interfaces/SiteSettingsFragmentArgs.java", "org/chromium/weblayer_private/interfaces/SiteSettingsFragmentArgs.java",
......
...@@ -79,6 +79,7 @@ import org.chromium.weblayer_private.interfaces.IWebLayerClient; ...@@ -79,6 +79,7 @@ import org.chromium.weblayer_private.interfaces.IWebLayerClient;
import org.chromium.weblayer_private.interfaces.ObjectWrapper; import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import org.chromium.weblayer_private.media.MediaRouteDialogFragmentImpl; import org.chromium.weblayer_private.media.MediaRouteDialogFragmentImpl;
import org.chromium.weblayer_private.media.MediaRouterClientImpl;
import org.chromium.weblayer_private.media.MediaSessionManager; import org.chromium.weblayer_private.media.MediaSessionManager;
import org.chromium.weblayer_private.media.MediaStreamManager; import org.chromium.weblayer_private.media.MediaStreamManager;
import org.chromium.weblayer_private.metrics.MetricsServiceClient; import org.chromium.weblayer_private.metrics.MetricsServiceClient;
...@@ -404,6 +405,19 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -404,6 +405,19 @@ public final class WebLayerImpl extends IWebLayer.Stub {
MediaSessionManager.serviceDestroyed(); MediaSessionManager.serviceDestroyed();
} }
@Override
public void onRemoteMediaServiceStarted(IObjectWrapper sessionService, Intent intent) {
StrictModeWorkaround.apply();
MediaRouterClientImpl.serviceStarted(
ObjectWrapper.unwrap(sessionService, Service.class), intent);
}
@Override
public void onRemoteMediaServiceDestroyed(int id) {
StrictModeWorkaround.apply();
MediaRouterClientImpl.serviceDestroyed(id);
}
@Override @Override
public IBinder initializeImageDecoder(IObjectWrapper appContext, IObjectWrapper remoteContext) { public IBinder initializeImageDecoder(IObjectWrapper appContext, IObjectWrapper remoteContext) {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
...@@ -513,6 +527,42 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -513,6 +527,42 @@ public final class WebLayerImpl extends IWebLayer.Stub {
} }
} }
public static Intent createRemoteMediaServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createRemoteMediaServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getPresentationApiNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getPresentationApiNotificationId();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getRemotePlaybackApiNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getRemotePlaybackApiNotificationId();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static String getClientApplicationName() { public static String getClientApplicationName() {
Context context = ContextUtils.getApplicationContext(); Context context = ContextUtils.getApplicationContext();
return new StringBuilder() return new StringBuilder()
......
...@@ -104,4 +104,8 @@ interface IWebLayer { ...@@ -104,4 +104,8 @@ interface IWebLayer {
IObjectWrapper getApplicationContext() = 20; IObjectWrapper getApplicationContext() = 20;
IMediaRouteDialogFragment createMediaRouteDialogFragmentImpl( IMediaRouteDialogFragment createMediaRouteDialogFragmentImpl(
in IRemoteFragmentClient remoteFragmentClient) = 21; in IRemoteFragmentClient remoteFragmentClient) = 21;
// Added in Version 88.
void onRemoteMediaServiceStarted(in IObjectWrapper sessionService, in Intent intent) = 22;
void onRemoteMediaServiceDestroyed(int id) = 23;
} }
...@@ -18,4 +18,7 @@ interface IWebLayerClient { ...@@ -18,4 +18,7 @@ interface IWebLayerClient {
long getClassLoaderCreationTime() = 4; long getClassLoaderCreationTime() = 4;
long getContextCreationTime() = 5; long getContextCreationTime() = 5;
long getWebLayerLoaderCreationTime() = 6; long getWebLayerLoaderCreationTime() = 6;
Intent createRemoteMediaServiceIntent() = 7;
int getPresentationApiNotificationId() = 8;
int getRemotePlaybackApiNotificationId() = 9;
} }
// Copyright 2020 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.weblayer_private.interfaces;
/** Keys for remote media service intent extras. */
public interface RemoteMediaServiceConstants {
String NOTIFICATION_ID_KEY = "remote_media_service_notification_id_key";
}
...@@ -4,23 +4,46 @@ ...@@ -4,23 +4,46 @@
package org.chromium.weblayer_private.media; package org.chromium.weblayer_private.media;
import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.components.browser_ui.media.MediaNotificationController;
import org.chromium.components.browser_ui.media.MediaNotificationInfo; import org.chromium.components.browser_ui.media.MediaNotificationInfo;
import org.chromium.components.browser_ui.media.MediaNotificationManager;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import org.chromium.components.media_router.MediaRouterClient; import org.chromium.components.media_router.MediaRouterClient;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
import org.chromium.weblayer_private.IntentUtils; import org.chromium.weblayer_private.IntentUtils;
import org.chromium.weblayer_private.TabImpl; import org.chromium.weblayer_private.TabImpl;
import org.chromium.weblayer_private.WebLayerImpl;
import org.chromium.weblayer_private.interfaces.RemoteMediaServiceConstants;
/** Provides WebLayer-specific behavior for Media Router. */ /** Provides WebLayer-specific behavior for Media Router. */
@JNINamespace("weblayer") @JNINamespace("weblayer")
public class MediaRouterClientImpl extends MediaRouterClient { public class MediaRouterClientImpl extends MediaRouterClient {
static int sPresentationNotificationId;
static int sRemotingNotificationId;
private MediaRouterClientImpl() {} private MediaRouterClientImpl() {}
public static void serviceStarted(Service service, Intent intent) {
int notificationId = intent.getIntExtra(RemoteMediaServiceConstants.NOTIFICATION_ID_KEY, 0);
if (notificationId == 0) {
throw new RuntimeException("Invalid RemoteMediaService notification id");
}
MediaSessionNotificationHelper.serviceStarted(service, intent, notificationId);
}
public static void serviceDestroyed(int notificationId) {
MediaSessionNotificationHelper.serviceDestroyed(notificationId);
}
@Override @Override
public int getTabId(WebContents webContents) { public int getTabId(WebContents webContents) {
TabImpl tab = TabImpl.fromWebContents(webContents); TabImpl tab = TabImpl.fromWebContents(webContents);
...@@ -34,7 +57,19 @@ public class MediaRouterClientImpl extends MediaRouterClient { ...@@ -34,7 +57,19 @@ public class MediaRouterClientImpl extends MediaRouterClient {
@Override @Override
public void showNotification(MediaNotificationInfo notificationInfo) { public void showNotification(MediaNotificationInfo notificationInfo) {
// TODO: implement. MediaNotificationManager.show(notificationInfo, () -> {
return new MediaRouterNotificationControllerDelegate(notificationInfo.id);
});
}
@Override
public int getPresentationNotificationId() {
return getPresentationNotificationIdFromClient();
}
@Override
public int getRemotingNotificationId() {
return getRemotingNotificationIdFromClient();
} }
@Override @Override
...@@ -51,4 +86,62 @@ public class MediaRouterClientImpl extends MediaRouterClient { ...@@ -51,4 +86,62 @@ public class MediaRouterClientImpl extends MediaRouterClient {
MediaRouterClient.setInstance(new MediaRouterClientImpl()); MediaRouterClient.setInstance(new MediaRouterClientImpl());
} }
private static class MediaRouterNotificationControllerDelegate
implements MediaNotificationController.Delegate {
// The ID distinguishes between Presentation and Remoting services/notifications.
private final int mNotificationId;
MediaRouterNotificationControllerDelegate(int notificationId) {
mNotificationId = notificationId;
}
@Override
public Intent createServiceIntent() {
return WebLayerImpl.createRemoteMediaServiceIntent().putExtra(
RemoteMediaServiceConstants.NOTIFICATION_ID_KEY, mNotificationId);
}
@Override
public String getAppName() {
return WebLayerImpl.getClientApplicationName();
}
@Override
public String getNotificationGroupName() {
if (mNotificationId == getPresentationNotificationIdFromClient()) {
return "org.chromium.weblayer.PresentationApi";
}
assert mNotificationId == getRemotingNotificationIdFromClient();
return "org.chromium.weblayer.RemotePlaybackApi";
}
@Override
public NotificationWrapperBuilder createNotificationWrapperBuilder() {
return MediaSessionNotificationHelper.createNotificationWrapperBuilder(mNotificationId);
}
@Override
public void onMediaSessionUpdated(MediaSessionCompat session) {
// TODO(estade): implement.
}
@Override
public void logNotificationShown(NotificationWrapper notification) {}
}
private static int getPresentationNotificationIdFromClient() {
if (sPresentationNotificationId == 0) {
sPresentationNotificationId = WebLayerImpl.getPresentationApiNotificationId();
}
return sPresentationNotificationId;
}
private static int getRemotingNotificationIdFromClient() {
if (sRemotingNotificationId == 0) {
sRemotingNotificationId = WebLayerImpl.getRemotePlaybackApiNotificationId();
}
return sRemotingNotificationId;
}
} }
...@@ -12,14 +12,10 @@ import org.chromium.components.browser_ui.media.MediaNotificationController; ...@@ -12,14 +12,10 @@ import org.chromium.components.browser_ui.media.MediaNotificationController;
import org.chromium.components.browser_ui.media.MediaNotificationInfo; import org.chromium.components.browser_ui.media.MediaNotificationInfo;
import org.chromium.components.browser_ui.media.MediaNotificationManager; import org.chromium.components.browser_ui.media.MediaNotificationManager;
import org.chromium.components.browser_ui.media.MediaSessionHelper; import org.chromium.components.browser_ui.media.MediaSessionHelper;
import org.chromium.components.browser_ui.notifications.ForegroundServiceUtils;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapper; import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder; import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import org.chromium.weblayer_private.IntentUtils; import org.chromium.weblayer_private.IntentUtils;
import org.chromium.weblayer_private.WebLayerImpl; import org.chromium.weblayer_private.WebLayerImpl;
import org.chromium.weblayer_private.WebLayerNotificationChannels;
import org.chromium.weblayer_private.WebLayerNotificationWrapperBuilder;
/** /**
* A glue class for MediaSession. * A glue class for MediaSession.
...@@ -31,24 +27,11 @@ public class MediaSessionManager { ...@@ -31,24 +27,11 @@ public class MediaSessionManager {
private static int sNotificationId; private static int sNotificationId;
public static void serviceStarted(Service service, Intent intent) { public static void serviceStarted(Service service, Intent intent) {
MediaNotificationController controller = getController(); MediaSessionNotificationHelper.serviceStarted(service, intent, getNotificationId());
if (controller != null && controller.processIntent(service, intent)) return;
// The service has been started with startForegroundService() but the
// notification hasn't been shown. See similar logic in {@link
// ChromeMediaNotificationControllerDelegate}.
MediaNotificationController.finishStartingForegroundServiceOnO(
service, createNotificationWrapperBuilder().buildNotificationWrapper());
// Call stopForeground to guarantee Android unset the foreground bit.
ForegroundServiceUtils.getInstance().stopForeground(
service, Service.STOP_FOREGROUND_REMOVE);
service.stopSelf();
} }
public static void serviceDestroyed() { public static void serviceDestroyed() {
MediaNotificationController controller = getController(); MediaSessionNotificationHelper.serviceDestroyed(getNotificationId());
if (controller != null) controller.onServiceDestroyed();
MediaNotificationManager.clear(getNotificationId());
} }
public static MediaSessionHelper.Delegate createMediaSessionHelperDelegate(int tabId) { public static MediaSessionHelper.Delegate createMediaSessionHelperDelegate(int tabId) {
...@@ -60,7 +43,7 @@ public class MediaSessionManager { ...@@ -60,7 +43,7 @@ public class MediaSessionManager {
@Override @Override
public boolean fetchLargeFaviconImage() { public boolean fetchLargeFaviconImage() {
// TODO(crbug.com/1076463): WebLayer doesn't support favicons. // TODO(crbug.com/1137625): implement.
return false; return false;
} }
...@@ -108,7 +91,8 @@ public class MediaSessionManager { ...@@ -108,7 +91,8 @@ public class MediaSessionManager {
@Override @Override
public NotificationWrapperBuilder createNotificationWrapperBuilder() { public NotificationWrapperBuilder createNotificationWrapperBuilder() {
return MediaSessionManager.createNotificationWrapperBuilder(); return MediaSessionNotificationHelper.createNotificationWrapperBuilder(
getNotificationId());
} }
@Override @Override
...@@ -120,22 +104,8 @@ public class MediaSessionManager { ...@@ -120,22 +104,8 @@ public class MediaSessionManager {
public void logNotificationShown(NotificationWrapper notification) {} public void logNotificationShown(NotificationWrapper notification) {}
} }
private static NotificationWrapperBuilder createNotificationWrapperBuilder() {
// Only the null tag will work as expected, because {@link Service#startForeground()} only
// takes an ID and no tag. If we pass a tag here, then the notification that's used to
// display a paused state (no foreground service) will not be identified as the same one
// that's used with the foreground service.
return WebLayerNotificationWrapperBuilder.create(
WebLayerNotificationChannels.ChannelId.MEDIA_PLAYBACK,
new NotificationMetadata(0, null /*notificationTag*/, getNotificationId()));
}
private static int getNotificationId() { private static int getNotificationId() {
if (sNotificationId == 0) sNotificationId = WebLayerImpl.getMediaSessionNotificationId(); if (sNotificationId == 0) sNotificationId = WebLayerImpl.getMediaSessionNotificationId();
return sNotificationId; return sNotificationId;
} }
private static MediaNotificationController getController() {
return MediaNotificationManager.getController(getNotificationId());
}
} }
// Copyright 2020 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.weblayer_private.media;
import android.app.Service;
import android.content.Intent;
import org.chromium.components.browser_ui.media.MediaNotificationController;
import org.chromium.components.browser_ui.media.MediaNotificationManager;
import org.chromium.components.browser_ui.notifications.ForegroundServiceUtils;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import org.chromium.weblayer_private.WebLayerNotificationChannels;
import org.chromium.weblayer_private.WebLayerNotificationWrapperBuilder;
/**
* A helper class for management of MediaSession (local device), Presentation API and Remote
* Playback API (casting) notifications and foreground services.
*/
class MediaSessionNotificationHelper {
static void serviceStarted(Service service, Intent intent, int notificationId) {
MediaNotificationController controller =
MediaNotificationManager.getController(notificationId);
if (controller != null && controller.processIntent(service, intent)) return;
// The service has been started with startForegroundService() but the
// notification hasn't been shown. See similar logic in {@link
// ChromeMediaNotificationControllerDelegate}.
MediaNotificationController.finishStartingForegroundServiceOnO(service,
createNotificationWrapperBuilder(notificationId).buildNotificationWrapper());
// Call stopForeground to guarantee Android unset the foreground bit.
ForegroundServiceUtils.getInstance().stopForeground(
service, Service.STOP_FOREGROUND_REMOVE);
service.stopSelf();
}
static void serviceDestroyed(int notificationId) {
MediaNotificationController controller =
MediaNotificationManager.getController(notificationId);
if (controller != null) controller.onServiceDestroyed();
MediaNotificationManager.clear(notificationId);
}
static NotificationWrapperBuilder createNotificationWrapperBuilder(int notificationId) {
// Only the null tag will work as expected, because {@link Service#startForeground()} only
// takes an ID and no tag. If we pass a tag here, then the notification that's used to
// display a paused state (no foreground service) will not be identified as the same one
// that's used with the foreground service.
return WebLayerNotificationWrapperBuilder.create(
WebLayerNotificationChannels.ChannelId.MEDIA_PLAYBACK,
new NotificationMetadata(0, null /*notificationTag*/, notificationId));
}
}
...@@ -85,6 +85,13 @@ ...@@ -85,6 +85,13 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name="org.chromium.weblayer.RemoteMediaService"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- Service for decoding images in a sandboxed process. --> <!-- Service for decoding images in a sandboxed process. -->
<service <service
android:name="org.chromium.weblayer.ImageDecoderService" android:name="org.chromium.weblayer.ImageDecoderService"
......
...@@ -70,6 +70,7 @@ android_library("java") { ...@@ -70,6 +70,7 @@ android_library("java") {
"org/chromium/weblayer/LoadError.java", "org/chromium/weblayer/LoadError.java",
"org/chromium/weblayer/MediaCaptureCallback.java", "org/chromium/weblayer/MediaCaptureCallback.java",
"org/chromium/weblayer/MediaCaptureController.java", "org/chromium/weblayer/MediaCaptureController.java",
"org/chromium/weblayer/MediaPlaybackBaseService.java",
"org/chromium/weblayer/MediaRouteDialogFragment.java", "org/chromium/weblayer/MediaRouteDialogFragment.java",
"org/chromium/weblayer/MediaSessionService.java", "org/chromium/weblayer/MediaSessionService.java",
"org/chromium/weblayer/NavigateParams.java", "org/chromium/weblayer/NavigateParams.java",
...@@ -82,6 +83,7 @@ android_library("java") { ...@@ -82,6 +83,7 @@ android_library("java") {
"org/chromium/weblayer/ObserverList.java", "org/chromium/weblayer/ObserverList.java",
"org/chromium/weblayer/Profile.java", "org/chromium/weblayer/Profile.java",
"org/chromium/weblayer/RemoteFragment.java", "org/chromium/weblayer/RemoteFragment.java",
"org/chromium/weblayer/RemoteMediaService.java",
"org/chromium/weblayer/ScrollNotificationType.java", "org/chromium/weblayer/ScrollNotificationType.java",
"org/chromium/weblayer/ScrollOffsetCallback.java", "org/chromium/weblayer/ScrollOffsetCallback.java",
"org/chromium/weblayer/SettingType.java", "org/chromium/weblayer/SettingType.java",
......
// Copyright 2020 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.weblayer;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Base class that provides common functionality for media playback services that are associated
* with an active media session and Android notification.
*/
abstract class MediaPlaybackBaseService extends Service {
// True when the start command has been forwarded to the impl.
boolean mStarted;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
if (!WebLayer.hasWebLayerInitializationStarted()) {
stopSelf();
return;
}
init();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
WebLayer webLayer = getWebLayer();
if (webLayer == null) {
stopSelf();
} else {
try {
forwardStartCommandToImpl(webLayer, intent);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
mStarted = true;
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (!mStarted) return;
try {
forwardDestroyToImpl();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/** Called to do initialization when the service is created. */
void init() {}
/**
* Called to forward {@link onStartCommand()} to the WebLayer implementation.
*
* @param webLayer the implementation.
* @param intent the intent that started the service.
*/
abstract void forwardStartCommandToImpl(@NonNull WebLayer webLayer, Intent intent)
throws RemoteException;
/**
* Called to forward {@link onDestroy()} to the WebLayer implementation.
*
* This will only be called if {@link forwardStartCommandToImpl()} was previously called, and
* there should always be a loaded {@link WebLayer} available via {@link getWebLayer()}.
*/
abstract void forwardDestroyToImpl() throws RemoteException;
/** Returns the loaded {@link WebLayer}, or null if none is loaded. */
@Nullable
WebLayer getWebLayer() {
WebLayer webLayer;
try {
webLayer = WebLayer.getLoadedWebLayer(getApplication());
} catch (UnsupportedVersionException e) {
throw new RuntimeException(e);
}
return webLayer;
}
}
...@@ -9,9 +9,10 @@ import android.content.Context; ...@@ -9,9 +9,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import androidx.annotation.NonNull;
import org.chromium.weblayer_private.interfaces.ObjectWrapper; import org.chromium.weblayer_private.interfaces.ObjectWrapper;
/** /**
...@@ -21,24 +22,20 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper; ...@@ -21,24 +22,20 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper;
* foreground when the MediaSession is active. * foreground when the MediaSession is active.
* @since 85 * @since 85
*/ */
public class MediaSessionService extends Service { public class MediaSessionService extends MediaPlaybackBaseService {
// A helper to automatically pause the media session when a user removes headphones. // A helper to automatically pause the media session when a user removes headphones.
private BroadcastReceiver mAudioBecomingNoisyReceiver; private BroadcastReceiver mAudioBecomingNoisyReceiver;
@Override @Override
public IBinder onBind(Intent intent) { public void onDestroy() {
return null; super.onDestroy();
if (mAudioBecomingNoisyReceiver != null) {
unregisterReceiver(mAudioBecomingNoisyReceiver);
}
} }
@Override @Override
public void onCreate() { void init() {
super.onCreate();
if (!WebLayer.hasWebLayerInitializationStarted()) {
stopSelf();
return;
}
mAudioBecomingNoisyReceiver = new BroadcastReceiver() { mAudioBecomingNoisyReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
...@@ -57,43 +54,13 @@ public class MediaSessionService extends Service { ...@@ -57,43 +54,13 @@ public class MediaSessionService extends Service {
} }
@Override @Override
public void onDestroy() { void forwardStartCommandToImpl(@NonNull WebLayer webLayer, Intent intent)
super.onDestroy(); throws RemoteException {
webLayer.getImpl().onMediaSessionServiceStarted(ObjectWrapper.wrap(this), intent);
if (mAudioBecomingNoisyReceiver == null) return;
try {
getWebLayer().getImpl().onMediaSessionServiceDestroyed();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
unregisterReceiver(mAudioBecomingNoisyReceiver);
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { void forwardDestroyToImpl() throws RemoteException {
try { getWebLayer().getImpl().onMediaSessionServiceDestroyed();
WebLayer webLayer = getWebLayer();
if (webLayer == null) {
stopSelf();
} else {
webLayer.getImpl().onMediaSessionServiceStarted(ObjectWrapper.wrap(this), intent);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
}
return START_NOT_STICKY;
}
private WebLayer getWebLayer() {
WebLayer webLayer;
try {
webLayer = WebLayer.getLoadedWebLayer(getApplication());
} catch (UnsupportedVersionException e) {
throw new RuntimeException(e);
}
return webLayer;
} }
} }
// Copyright 2020 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.weblayer;
import android.content.Intent;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.RemoteMediaServiceConstants;
/**
* A foreground {@link Service} for Presentation API and Remote Playback API.
*
* Like {@link MediaSessionService}, this class is associated with a notification for an ongoing
* media session. The difference is that the media for this service is played back on a remote
* device, i.e. casting.
*
* @since 88
*/
public class RemoteMediaService extends MediaPlaybackBaseService {
private int mId;
@Override
void forwardStartCommandToImpl(@NonNull WebLayer webLayer, Intent intent)
throws RemoteException {
mId = intent.getIntExtra(RemoteMediaServiceConstants.NOTIFICATION_ID_KEY, 0);
if (mId == 0) throw new RuntimeException("Invalid RemoteMediaService notification id");
webLayer.getImpl().onRemoteMediaServiceStarted(ObjectWrapper.wrap(this), intent);
}
@Override
void forwardDestroyToImpl() throws RemoteException {
getWebLayer().getImpl().onRemoteMediaServiceDestroyed(mId);
}
}
...@@ -749,6 +749,26 @@ public class WebLayer { ...@@ -749,6 +749,26 @@ public class WebLayer {
public long getWebLayerLoaderCreationTime() { public long getWebLayerLoaderCreationTime() {
return sWebLayerLoaderCreationTime; return sWebLayerLoaderCreationTime;
} }
@Override
public Intent createRemoteMediaServiceIntent() {
StrictModeWorkaround.apply();
return new Intent(WebLayer.getAppContext(), RemoteMediaService.class);
}
@Override
public int getPresentationApiNotificationId() {
StrictModeWorkaround.apply();
// The id is part of the public library to avoid conflicts.
return R.id.weblayer_presentation_api_notification;
}
@Override
public int getRemotePlaybackApiNotificationId() {
StrictModeWorkaround.apply();
// The id is part of the public library to avoid conflicts.
return R.id.weblayer_remote_playback_api_notification;
}
} }
@VerifiesOnO @VerifiesOnO
......
...@@ -5,4 +5,6 @@ ...@@ -5,4 +5,6 @@
<resources> <resources>
<item type="id" name="weblayer_media_session_notification" /> <item type="id" name="weblayer_media_session_notification" />
<item type="id" name="weblayer_presentation_api_notification" />
<item type="id" name="weblayer_remote_playback_api_notification" />
</resources> </resources>
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