Commit 6cbf85b9 authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

WebLayer: hook up MediaSession.

This adds the glue code for MediaSession to work in WebLayer.

A new Service class is required in the client library to display
the notification. This service proxies lifetime events to WebLayer
implementation, which then glues together componentized code.

Of note:
1. notifications displayed with Service#startForeground cannot have
   a tag, so the ID must be unique across all application notifications.
   There's a default ID for the MediaSession notification, but an API
   is also added to WebLayer to allow setting a different ID, in case
   this one conflicts.
2. favicons are not supported (normally they'd be used as fallbacks for
   iconography/images not provided by the system)
3. Extra handling is required for the action button icons to work,
   see WebLayerNotificationBuilder. On L, we fall back to system icons,
   which look bad because of their low resolution, but are at least
   functional.

      https://googlechrome.github.io/samples/media-session/

Test: automated tests TODO; manual testing on
Bug: 1066263
Change-Id: I5e2ae13a07315e73aae474251cb5c0424804a427
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2220551Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarClark DuVall <cduvall@chromium.org>
Commit-Queue: Evan Stade <estade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#782054}
parent 4be85061
...@@ -139,10 +139,6 @@ bool ContentMainDelegateImpl::BasicStartupComplete(int* exit_code) { ...@@ -139,10 +139,6 @@ bool ContentMainDelegateImpl::BasicStartupComplete(int* exit_code) {
cl->AppendSwitch(::switches::kDisablePresentationAPI); cl->AppendSwitch(::switches::kDisablePresentationAPI);
// TODO(crbug.com/1057100): make remote-playback-api work with WebLayer. // TODO(crbug.com/1057100): make remote-playback-api work with WebLayer.
cl->AppendSwitch(::switches::kDisableRemotePlaybackAPI); cl->AppendSwitch(::switches::kDisableRemotePlaybackAPI);
#if defined(OS_ANDROID)
// TODO(crbug.com/1066263): make MediaSession work with WebLayer.
cl->AppendSwitch(::switches::kDisableMediaSessionAPI);
#endif
DisableFeaturesIfNotSet({ DisableFeaturesIfNotSet({
// TODO(crbug.com/1025619): make web-payments work with WebLayer. // TODO(crbug.com/1025619): make web-payments work with WebLayer.
::features::kWebPayments, ::features::kWebPayments,
......
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
#include "content/public/browser/browser_thread.h" #include "content/public/browser/browser_thread.h"
#include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/render_process_host.h" #include "content/public/browser/render_process_host.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/main_function_params.h" #include "content/public/common/main_function_params.h"
#include "content/public/common/url_constants.h" #include "content/public/common/url_constants.h"
#include "services/service_manager/embedder/result_codes.h" #include "services/service_manager/embedder/result_codes.h"
...@@ -36,16 +35,19 @@ ...@@ -36,16 +35,19 @@
#include "weblayer/public/main.h" #include "weblayer/public/main.h"
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
#include "base/command_line.h"
#include "components/crash/content/browser/child_exit_observer_android.h" #include "components/crash/content/browser/child_exit_observer_android.h"
#include "components/crash/content/browser/child_process_crash_observer_android.h" #include "components/crash/content/browser/child_process_crash_observer_android.h"
#include "components/crash/core/common/crash_key.h" #include "components/crash/core/common/crash_key.h"
#include "components/javascript_dialogs/android/app_modal_dialog_view_android.h" // nogncheck #include "components/javascript_dialogs/android/app_modal_dialog_view_android.h" // nogncheck
#include "components/javascript_dialogs/app_modal_dialog_manager.h" // nogncheck #include "components/javascript_dialogs/app_modal_dialog_manager.h" // nogncheck
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "net/android/network_change_notifier_factory_android.h" #include "net/android/network_change_notifier_factory_android.h"
#include "net/base/network_change_notifier.h" #include "net/base/network_change_notifier.h"
#include "weblayer/browser/android/metrics/uma_utils.h" #include "weblayer/browser/android/metrics/uma_utils.h"
#include "weblayer/browser/java/jni/MojoInterfaceRegistrar_jni.h" #include "weblayer/browser/java/jni/MojoInterfaceRegistrar_jni.h"
#include "weblayer/browser/weblayer_factory_impl_android.h"
#endif #endif
#if defined(USE_X11) #if defined(USE_X11)
...@@ -112,6 +114,15 @@ int BrowserMainPartsImpl::PreCreateThreads() { ...@@ -112,6 +114,15 @@ int BrowserMainPartsImpl::PreCreateThreads() {
std::make_unique<crash_reporter::ChildProcessCrashObserver>()); std::make_unique<crash_reporter::ChildProcessCrashObserver>());
crash_reporter::InitializeCrashKeys(); crash_reporter::InitializeCrashKeys();
// MediaSession was implemented in M85, and requires both implementation and
// client libraries to be at least that new. The version check has to be in
// the browser process, but the command line flag is automatically propagated
// to renderers.
if (WebLayerFactoryImplAndroid::GetClientMajorVersion() < 85) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kDisableMediaSessionAPI);
}
#endif #endif
return service_manager::RESULT_CODE_NORMAL_EXIT; return service_manager::RESULT_CODE_NORMAL_EXIT;
......
...@@ -26,6 +26,7 @@ android_resources("weblayer_resources") { ...@@ -26,6 +26,7 @@ android_resources("weblayer_resources") {
":weblayer_strings_grd", ":weblayer_strings_grd",
"//components/blocked_content/android:java_resources", "//components/blocked_content/android:java_resources",
"//components/browser_ui/http_auth/android:java_resources", "//components/browser_ui/http_auth/android:java_resources",
"//components/browser_ui/media/android:java_resources",
"//components/browser_ui/settings/android:java_resources", "//components/browser_ui/settings/android:java_resources",
"//components/browser_ui/site_settings/android:java_resources", "//components/browser_ui/site_settings/android:java_resources",
"//components/browser_ui/strings/android:browser_ui_strings_grd", "//components/browser_ui/strings/android:browser_ui_strings_grd",
...@@ -104,6 +105,7 @@ android_library("java") { ...@@ -104,6 +105,7 @@ android_library("java") {
"org/chromium/weblayer_private/IntentUtils.java", "org/chromium/weblayer_private/IntentUtils.java",
"org/chromium/weblayer_private/InterceptNavigationDelegateClientImpl.java", "org/chromium/weblayer_private/InterceptNavigationDelegateClientImpl.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/MediaSessionManager.java",
"org/chromium/weblayer_private/MediaStreamManager.java", "org/chromium/weblayer_private/MediaStreamManager.java",
"org/chromium/weblayer_private/MojoInterfaceRegistrar.java", "org/chromium/weblayer_private/MojoInterfaceRegistrar.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationControllerImpl.java",
...@@ -151,6 +153,7 @@ android_library("java") { ...@@ -151,6 +153,7 @@ android_library("java") {
"//components/autofill/android/provider:java", "//components/autofill/android/provider:java",
"//components/browser_ui/client_certificate/android:java", "//components/browser_ui/client_certificate/android:java",
"//components/browser_ui/http_auth/android:java", "//components/browser_ui/http_auth/android:java",
"//components/browser_ui/media/android:java",
"//components/browser_ui/modaldialog/android:java", "//components/browser_ui/modaldialog/android:java",
"//components/browser_ui/notifications/android:java", "//components/browser_ui/notifications/android:java",
"//components/browser_ui/settings/android:java", "//components/browser_ui/settings/android: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_private;
import android.annotation.SuppressLint;
import android.app.Service;
import android.content.Intent;
import android.support.v4.media.session.MediaSessionCompat;
import org.chromium.components.browser_ui.media.MediaNotificationController;
import org.chromium.components.browser_ui.media.MediaNotificationInfo;
import org.chromium.components.browser_ui.media.MediaSessionHelper;
import org.chromium.components.browser_ui.notifications.ChromeNotification;
import org.chromium.components.browser_ui.notifications.ChromeNotificationBuilder;
import org.chromium.components.browser_ui.notifications.ForegroundServiceUtils;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
/**
* A glue class for MediaSession.
* This class defines delegates that provide WebLayer-specific behavior to shared MediaSession code.
* It also manages the lifetime of {@link MediaNotificationController} and the {@link Service}
* associated with the notification.
*/
class MediaSessionManager {
// This is a singleton because there's only at most one MediaSession active at a time.
@SuppressLint("StaticFieldLeak")
static MediaNotificationController sController;
private static int sNotificationId = 0;
static void serviceStarted(Service service, Intent intent) {
if (sController != null && sController.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, createChromeNotificationBuilder().buildChromeNotification());
// Call stopForeground to guarantee Android unset the foreground bit.
ForegroundServiceUtils.getInstance().stopForeground(
service, Service.STOP_FOREGROUND_REMOVE);
service.stopSelf();
}
static void serviceDestroyed() {
if (sController != null) sController.onServiceDestroyed();
sController = null;
}
static MediaSessionHelper.Delegate createMediaSessionHelperDelegate(int tabId) {
return new MediaSessionHelper.Delegate() {
@Override
public Intent createBringTabToFrontIntent() {
return IntentUtils.createBringTabToFrontIntent(tabId);
}
@Override
public boolean fetchLargeFaviconImage() {
// TODO(crbug.com/1076463): WebLayer doesn't support favicons.
return false;
}
@Override
public MediaNotificationInfo.Builder createMediaNotificationInfoBuilder() {
ensureNotificationId();
return new MediaNotificationInfo.Builder().setInstanceId(tabId).setId(
sNotificationId);
}
@Override
public void showMediaNotification(MediaNotificationInfo notificationInfo) {
assert notificationInfo.id == sNotificationId;
if (sController == null) {
sController = new MediaNotificationController(
new WebLayerMediaNotificationControllerDelegate());
}
sController.mThrottler.queueNotification(notificationInfo);
}
@Override
public void hideMediaNotification() {
if (sController != null) sController.hideNotification(tabId);
}
@Override
public void activateAndroidMediaSession() {
if (sController != null) sController.activateAndroidMediaSession(tabId);
}
};
}
private static class WebLayerMediaNotificationControllerDelegate
implements MediaNotificationController.Delegate {
@Override
public Intent createServiceIntent() {
return WebLayerImpl.createMediaSessionServiceIntent();
}
@Override
public String getAppName() {
return WebLayerImpl.getClientApplicationName();
}
@Override
public String getNotificationGroupName() {
return "org.chromium.weblayer.MediaSession";
}
@Override
public ChromeNotificationBuilder createChromeNotificationBuilder() {
return MediaSessionManager.createChromeNotificationBuilder();
}
@Override
public void onMediaSessionUpdated(MediaSessionCompat session) {
// This is only relevant when casting.
}
@Override
public void logNotificationShown(ChromeNotification notification) {}
}
private static ChromeNotificationBuilder createChromeNotificationBuilder() {
ensureNotificationId();
// 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 WebLayerNotificationBuilder.create(
WebLayerNotificationChannels.ChannelId.MEDIA_PLAYBACK,
new NotificationMetadata(0, null /*notificationTag*/, sNotificationId));
}
private static void ensureNotificationId() {
if (sNotificationId == 0) sNotificationId = WebLayerImpl.getMediaSessionNotificationId();
}
}
...@@ -27,6 +27,7 @@ import org.chromium.base.annotations.NativeMethods; ...@@ -27,6 +27,7 @@ import org.chromium.base.annotations.NativeMethods;
import org.chromium.components.autofill.AutofillActionModeCallback; import org.chromium.components.autofill.AutofillActionModeCallback;
import org.chromium.components.autofill.AutofillProvider; import org.chromium.components.autofill.AutofillProvider;
import org.chromium.components.browser_ui.http_auth.LoginPrompt; import org.chromium.components.browser_ui.http_auth.LoginPrompt;
import org.chromium.components.browser_ui.media.MediaSessionHelper;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate; import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate; import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams; import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
...@@ -112,6 +113,7 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer { ...@@ -112,6 +113,7 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
private InterceptNavigationDelegateClientImpl mInterceptNavigationDelegateClient; private InterceptNavigationDelegateClientImpl mInterceptNavigationDelegateClient;
private InterceptNavigationDelegateImpl mInterceptNavigationDelegate; private InterceptNavigationDelegateImpl mInterceptNavigationDelegate;
private InfoBarContainer mInfoBarContainer; private InfoBarContainer mInfoBarContainer;
private MediaSessionHelper mMediaSessionHelper;
private boolean mPostContainerViewInitDone; private boolean mPostContainerViewInitDone;
...@@ -251,6 +253,14 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer { ...@@ -251,6 +253,14 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
}; };
// addObserver() calls to observer when added. // addObserver() calls to observer when added.
WebLayerAccessibilityUtil.get().addObserver(mAccessibilityObserver); WebLayerAccessibilityUtil.get().addObserver(mAccessibilityObserver);
// MediaSession only works if the client is new enough. Sadly, passing
// kDisableMediaSessionAPI does not fully disable the API, so a check is also necessary
// before installing this observer.
if (WebLayerFactoryImpl.getClientMajorVersion() >= 85) {
mMediaSessionHelper = new MediaSessionHelper(
mWebContents, MediaSessionManager.createMediaSessionHelperDelegate(mId));
}
} }
private void doInitAfterSettingContainerView() { private void doInitAfterSettingContainerView() {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.weblayer_private; package org.chromium.weblayer_private;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
...@@ -331,6 +332,19 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -331,6 +332,19 @@ public final class WebLayerImpl extends IWebLayer.Stub {
} }
} }
@Override
public void onMediaSessionServiceStarted(IObjectWrapper sessionService, Intent intent) {
StrictModeWorkaround.apply();
MediaSessionManager.serviceStarted(
ObjectWrapper.unwrap(sessionService, Service.class), intent);
}
@Override
public void onMediaSessionServiceDestroyed() {
StrictModeWorkaround.apply();
MediaSessionManager.serviceDestroyed();
}
@Override @Override
public void enumerateAllProfileNames(IObjectWrapper valueCallback) { public void enumerateAllProfileNames(IObjectWrapper valueCallback) {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
...@@ -383,6 +397,30 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -383,6 +397,30 @@ public final class WebLayerImpl extends IWebLayer.Stub {
} }
} }
public static Intent createMediaSessionServiceIntent() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.createMediaSessionServiceIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public static int getMediaSessionNotificationId() {
if (sClient == null) {
throw new IllegalStateException("WebLayer should have been initialized already.");
}
try {
return sClient.getMediaSessionNotificationId();
} 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()
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
package org.chromium.weblayer_private; package org.chromium.weblayer_private;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.Build; import android.os.Build;
...@@ -40,17 +43,67 @@ final class WebLayerNotificationBuilder extends NotificationBuilder { ...@@ -40,17 +43,67 @@ final class WebLayerNotificationBuilder extends NotificationBuilder {
public ChromeNotificationBuilder setSmallIcon(int icon) { public ChromeNotificationBuilder setSmallIcon(int icon) {
if (WebLayerImpl.isAndroidResource(icon)) { if (WebLayerImpl.isAndroidResource(icon)) {
super.setSmallIcon(icon); super.setSmallIcon(icon);
return this; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
super.setSmallIcon(createIcon(icon));
} else {
// Some fallback is required, or the notification won't appear.
super.setSmallIcon(getFallbackAndroidResource(icon));
} }
return this;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @Override
super.setSmallIcon( @SuppressWarnings("deprecation")
Icon.createWithResource(WebViewFactory.getLoadedPackageInfo().packageName, public ChromeNotificationBuilder addAction(int icon, CharSequence title, PendingIntent intent) {
WebLayerImpl.getResourceIdForSystemUi(icon))); if (WebLayerImpl.isAndroidResource(icon)) {
super.addAction(icon, title, intent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
super.addAction(
new Notification.Action.Builder(createIcon(icon), title, intent).build());
} else { } else {
// Some fallback is required, or the notification won't appear. super.addAction(getFallbackAndroidResource(icon), title, intent);
super.setSmallIcon(android.R.drawable.radiobutton_on_background);
} }
return this; return this;
} }
@TargetApi(Build.VERSION_CODES.M)
private Icon createIcon(int resId) {
return Icon.createWithResource(WebViewFactory.getLoadedPackageInfo().packageName,
WebLayerImpl.getResourceIdForSystemUi(resId));
}
/**
* Finds a reasonable replacement for the given app-defined resource from among stock android
* resources. This is useful when {@link Icon} is not available.
*/
private int getFallbackAndroidResource(int appResourceId) {
if (appResourceId == R.drawable.ic_play_arrow_white_36dp) {
return android.R.drawable.ic_media_play;
}
if (appResourceId == R.drawable.ic_pause_white_36dp) {
return android.R.drawable.ic_media_pause;
}
if (appResourceId == R.drawable.ic_stop_white_36dp) {
// There's no ic_media_stop. This standin is at least a square. In practice this
// shouldn't ever come up as stop is only used in (Chrome) cast notifications.
return android.R.drawable.checkbox_off_background;
}
if (appResourceId == R.drawable.ic_skip_previous_white_36dp) {
return android.R.drawable.ic_media_previous;
}
if (appResourceId == R.drawable.ic_skip_next_white_36dp) {
return android.R.drawable.ic_media_next;
}
if (appResourceId == R.drawable.ic_fast_forward_white_36dp) {
return android.R.drawable.ic_media_ff;
}
if (appResourceId == R.drawable.ic_fast_rewind_white_36dp) {
return android.R.drawable.ic_media_rew;
}
if (appResourceId == R.drawable.audio_playing) {
return android.R.drawable.ic_lock_silent_mode_off;
}
return android.R.drawable.radiobutton_on_background;
}
} }
...@@ -96,4 +96,8 @@ interface IWebLayer { ...@@ -96,4 +96,8 @@ interface IWebLayer {
ISiteSettingsFragment createSiteSettingsFragmentImpl( ISiteSettingsFragment createSiteSettingsFragmentImpl(
in IRemoteFragmentClient remoteFragmentClient, in IRemoteFragmentClient remoteFragmentClient,
in IObjectWrapper fragmentArgs) = 16; in IObjectWrapper fragmentArgs) = 16;
// Added in Version 85.
void onMediaSessionServiceStarted(in IObjectWrapper sessionService, in Intent intent) = 17;
void onMediaSessionServiceDestroyed() = 18;
} }
...@@ -8,4 +8,6 @@ import android.content.Intent; ...@@ -8,4 +8,6 @@ import android.content.Intent;
interface IWebLayerClient { interface IWebLayerClient {
Intent createIntent() = 0; Intent createIntent() = 0;
Intent createMediaSessionServiceIntent() = 1;
int getMediaSessionNotificationId() = 2;
} }
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
...@@ -75,5 +76,12 @@ ...@@ -75,5 +76,12 @@
<action android:name="org.chromium.weblayer.webrtc.ACTIVATE_TAB"/> <action android:name="org.chromium.weblayer.webrtc.ACTIVATE_TAB"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name="org.chromium.weblayer.MediaSessionService"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>
...@@ -18,12 +18,14 @@ jinja_template("weblayer_client_manifest") { ...@@ -18,12 +18,14 @@ jinja_template("weblayer_client_manifest") {
} }
android_resources("client_resources") { android_resources("client_resources") {
custom_package = "org.chromium.weblayer"
sources = [ sources = [
"res/values-night/colors.xml", "res/values-night/colors.xml",
"res/values-night/values.xml", "res/values-night/values.xml",
"res/values-v27/styles.xml", "res/values-v27/styles.xml",
"res/values-v28/styles.xml", "res/values-v28/styles.xml",
"res/values/colors.xml", "res/values/colors.xml",
"res/values/ids.xml",
"res/values/styles.xml", "res/values/styles.xml",
"res/values/values.xml", "res/values/values.xml",
"res/xml/weblayer_file_paths.xml", "res/xml/weblayer_file_paths.xml",
...@@ -58,6 +60,7 @@ android_library("java") { ...@@ -58,6 +60,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/MediaSessionService.java",
"org/chromium/weblayer/NavigateParams.java", "org/chromium/weblayer/NavigateParams.java",
"org/chromium/weblayer/Navigation.java", "org/chromium/weblayer/Navigation.java",
"org/chromium/weblayer/NavigationCallback.java", "org/chromium/weblayer/NavigationCallback.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.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.IBinder;
import android.os.RemoteException;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
/**
* A foreground {@link Service} for the Web MediaSession API.
* This class is a thin wrapper that forwards lifecycle events to the WebLayer implementation, which
* in turn manages a system notification and {@link MediaSession}. This service will be in the
* foreground when the MediaSession is active.
* @since 85
*/
public class MediaSessionService extends Service {
// A helper to automatically pause the media session when a user removes headphones.
private BroadcastReceiver mAudioBecomingNoisyReceiver;
public MediaSessionService() {
if (WebLayer.getSupportedMajorVersionInternal() < 85) {
throw new UnsupportedOperationException();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
mAudioBecomingNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
return;
}
Intent i = new Intent(getApplication(), MediaSessionService.class);
i.setAction(intent.getAction());
getApplication().startService(i);
}
};
IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
registerReceiver(mAudioBecomingNoisyReceiver, filter);
}
@Override
public void onDestroy() {
super.onDestroy();
try {
getWebLayer().getImpl().onMediaSessionServiceDestroyed();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
unregisterReceiver(mAudioBecomingNoisyReceiver);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
try {
getWebLayer().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);
}
if (webLayer == null) {
throw new IllegalStateException("WebLayer not initialized");
}
return webLayer;
}
}
...@@ -180,6 +180,14 @@ public class WebLayer { ...@@ -180,6 +180,14 @@ public class WebLayer {
return mImpl; return mImpl;
} }
static WebLayer getLoadedWebLayer(@NonNull Context appContext)
throws UnsupportedVersionException {
ThreadCheck.ensureOnUiThread();
appContext = appContext.getApplicationContext();
checkAvailable(appContext);
return getWebLayerLoader(appContext).getLoadedWebLayer();
}
/** /**
* Returns the supported version. Using any functions defined in a newer version than * Returns the supported version. Using any functions defined in a newer version than
* returned by {@link getSupportedMajorVersion} result in throwing an * returned by {@link getSupportedMajorVersion} result in throwing an
...@@ -364,6 +372,10 @@ public class WebLayer { ...@@ -364,6 +372,10 @@ public class WebLayer {
} }
} }
WebLayer getLoadedWebLayer() {
return mWebLayer;
}
@Nullable @Nullable
private IWebLayer getIWebLayer(@NonNull Context appContext) { private IWebLayer getIWebLayer(@NonNull Context appContext) {
if (mIWebLayer != null) return mIWebLayer; if (mIWebLayer != null) return mIWebLayer;
...@@ -672,5 +684,18 @@ public class WebLayer { ...@@ -672,5 +684,18 @@ public class WebLayer {
// client library because it's referenced in the manifest. // client library because it's referenced in the manifest.
return new Intent(WebLayer.getAppContext(), BroadcastReceiver.class); return new Intent(WebLayer.getAppContext(), BroadcastReceiver.class);
} }
@Override
public Intent createMediaSessionServiceIntent() {
StrictModeWorkaround.apply();
return new Intent(WebLayer.getAppContext(), MediaSessionService.class);
}
@Override
public int getMediaSessionNotificationId() {
StrictModeWorkaround.apply();
// The id is part of the public library to avoid conflicts.
return R.id.weblayer_media_session_notification;
}
} }
} }
<?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>
<item type="id" name="weblayer_media_session_notification" />
</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