Commit 507e933e authored by Evan Stade's avatar Evan Stade Committed by Commit Bot

WebLayer: Show notification while media is streaming

Bug: 1025622
Change-Id: I3a704f734cb8c69bb5fc2ce14b0ac6ce71c630d2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2135898Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Commit-Queue: Evan Stade <estade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#757576}
parent 490a54a8
...@@ -354,6 +354,8 @@ source_set("weblayer_lib_base") { ...@@ -354,6 +354,8 @@ source_set("weblayer_lib_base") {
"browser/top_controls_container_view.cc", "browser/top_controls_container_view.cc",
"browser/top_controls_container_view.h", "browser/top_controls_container_view.h",
"browser/weblayer_impl_android.cc", "browser/weblayer_impl_android.cc",
"browser/webrtc/media_stream_manager.cc",
"browser/webrtc/media_stream_manager.h",
"common/crash_reporter/crash_keys.cc", "common/crash_reporter/crash_keys.cc",
"common/crash_reporter/crash_keys.h", "common/crash_reporter/crash_keys.h",
"common/crash_reporter/crash_reporter_client.cc", "common/crash_reporter/crash_reporter_client.cc",
......
...@@ -60,6 +60,7 @@ android_library("java") { ...@@ -60,6 +60,7 @@ android_library("java") {
"org/chromium/weblayer_private/FullscreenCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/InterceptNavigationDelegateImpl.java", "org/chromium/weblayer_private/InterceptNavigationDelegateImpl.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/MediaStreamManager.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationControllerImpl.java",
"org/chromium/weblayer_private/NavigationImpl.java", "org/chromium/weblayer_private/NavigationImpl.java",
"org/chromium/weblayer_private/NewTabCallbackProxy.java", "org/chromium/weblayer_private/NewTabCallbackProxy.java",
...@@ -178,6 +179,7 @@ generate_jni("jni") { ...@@ -178,6 +179,7 @@ generate_jni("jni") {
"org/chromium/weblayer_private/FullscreenCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/InterceptNavigationDelegateImpl.java", "org/chromium/weblayer_private/InterceptNavigationDelegateImpl.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/MediaStreamManager.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationControllerImpl.java",
"org/chromium/weblayer_private/NavigationImpl.java", "org/chromium/weblayer_private/NavigationImpl.java",
"org/chromium/weblayer_private/NewTabCallbackProxy.java", "org/chromium/weblayer_private/NewTabCallbackProxy.java",
......
...@@ -10,7 +10,6 @@ import android.app.PendingIntent; ...@@ -10,7 +10,6 @@ import android.app.PendingIntent;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Build; import android.os.Build;
import android.os.RemoteException; import android.os.RemoteException;
...@@ -42,22 +41,26 @@ import java.util.HashMap; ...@@ -42,22 +41,26 @@ import java.util.HashMap;
*/ */
@JNINamespace("weblayer") @JNINamespace("weblayer")
public final class DownloadImpl extends IDownload.Stub { public final class DownloadImpl extends IDownload.Stub {
private static final String DOWNLOADS_PREFIX = "org.chromium.weblayer.downloads";
// These actions have to be synchronized with the receiver defined in AndroidManifest.xml. // These actions have to be synchronized with the receiver defined in AndroidManifest.xml.
static final String OPEN_INTENT = "org.chromium.weblayer.downloads.OPEN"; private static final String OPEN_INTENT = DOWNLOADS_PREFIX + ".OPEN";
static final String DELETE_INTENT = "org.chromium.weblayer.downloads.DELETE"; private static final String DELETE_INTENT = DOWNLOADS_PREFIX + ".DELETE";
static final String PAUSE_INTENT = "org.chromium.weblayer.downloads.PAUSE"; private static final String PAUSE_INTENT = DOWNLOADS_PREFIX + ".PAUSE";
static final String RESUME_INTENT = "org.chromium.weblayer.downloads.RESUME"; private static final String RESUME_INTENT = DOWNLOADS_PREFIX + ".RESUME";
static final String CANCEL_INTENT = "org.chromium.weblayer.downloads.CANCEL"; private static final String CANCEL_INTENT = DOWNLOADS_PREFIX + ".CANCEL";
static final String EXTRA_NOTIFICATION_ID = "org.chromium.weblayer.downloads.NOTIFICATION_ID";
static final String EXTRA_NOTIFICATION_LOCATION = private static final String EXTRA_NOTIFICATION_ID = DOWNLOADS_PREFIX + ".NOTIFICATION_ID";
"org.chromium.weblayer.downloads.NOTIFICATION_LOCATION"; private static final String EXTRA_NOTIFICATION_LOCATION =
static final String EXTRA_NOTIFICATION_MIME_TYPE = DOWNLOADS_PREFIX + ".NOTIFICATION_LOCATION";
"org.chromium.weblayer.downloads.NOTIFICATION_MIME_TYPE"; private static final String EXTRA_NOTIFICATION_MIME_TYPE =
static final String EXTRA_NOTIFICATION_PROFILE = DOWNLOADS_PREFIX + ".NOTIFICATION_MIME_TYPE";
"org.chromium.weblayer.downloads.NOTIFICATION_PROFILE"; private static final String EXTRA_NOTIFICATION_PROFILE =
static final String PREF_NEXT_NOTIFICATION_ID = DOWNLOADS_PREFIX + ".NOTIFICATION_PROFILE";
"org.chromium.weblayer.downloads.notification_next_id"; private static final String CHANNEL_ID = DOWNLOADS_PREFIX + ".channel";
private static final String CHANNEL_ID = "org.chromium.weblayer.downloads.channel"; // The intent prefix is used as the notification's tag since it's guaranteed not to conflict
// with intent prefixes used by other subsystems that display notifications.
private static final String NOTIFICATION_TAG = DOWNLOADS_PREFIX;
private static final String TAG = "DownloadImpl"; private static final String TAG = "DownloadImpl";
private final String mProfileName; private final String mProfileName;
...@@ -72,6 +75,13 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -72,6 +75,13 @@ public final class DownloadImpl extends IDownload.Stub {
private static boolean sCreatedChannel = false; private static boolean sCreatedChannel = false;
private static final HashMap<Integer, DownloadImpl> sMap = new HashMap<Integer, DownloadImpl>(); private static final HashMap<Integer, DownloadImpl> sMap = new HashMap<Integer, DownloadImpl>();
/**
* @return a string that prefixes all intents that can be handled by {@link forwardIntent}.
*/
public static String getIntentPrefix() {
return DOWNLOADS_PREFIX;
}
public static void forwardIntent( public static void forwardIntent(
Context context, Intent intent, ProfileManager profileManager) { Context context, Intent intent, ProfileManager profileManager) {
if (intent.getAction().equals(OPEN_INTENT)) { if (intent.getAction().equals(OPEN_INTENT)) {
...@@ -132,22 +142,6 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -132,22 +142,6 @@ public final class DownloadImpl extends IDownload.Stub {
} }
} }
/**
* Need to return a unique id, even across crashes, to avoid notification intents with
* different data (e.g. notification GUID) getting dup'd.
*/
private static int getNextNotificationId() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
int nextId = prefs.getInt(PREF_NEXT_NOTIFICATION_ID, -1);
// Reset the counter when it gets close to max value
if (nextId >= Integer.MAX_VALUE - 1) {
nextId = -1;
}
nextId++;
prefs.edit().putInt(PREF_NEXT_NOTIFICATION_ID, nextId).apply();
return nextId;
}
public DownloadImpl( public DownloadImpl(
String profileName, IDownloadCallbackClient client, long nativeDownloadImpl, int id) { String profileName, IDownloadCallbackClient client, long nativeDownloadImpl, int id) {
mProfileName = profileName; mProfileName = profileName;
...@@ -291,7 +285,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -291,7 +285,7 @@ public final class DownloadImpl extends IDownload.Stub {
if (mBuilder != null) { if (mBuilder != null) {
NotificationManagerCompat notificationManager = getNotificationManager(); NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager != null) { if (notificationManager != null) {
notificationManager.cancel(mNotificationId); notificationManager.cancel(NOTIFICATION_TAG, mNotificationId);
} }
mBuilder = null; mBuilder = null;
} }
...@@ -389,7 +383,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -389,7 +383,7 @@ public final class DownloadImpl extends IDownload.Stub {
int state = getState(); int state = getState();
if (state == DownloadState.CANCELLED) { if (state == DownloadState.CANCELLED) {
if (notificationManager != null) { if (notificationManager != null) {
notificationManager.cancel(mNotificationId); notificationManager.cancel(NOTIFICATION_TAG, mNotificationId);
} }
mBuilder = null; mBuilder = null;
return; return;
...@@ -483,7 +477,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -483,7 +477,7 @@ public final class DownloadImpl extends IDownload.Stub {
if (notificationManager != null) { if (notificationManager != null) {
// mNotificationId is a unique int for each notification that you must define. // mNotificationId is a unique int for each notification that you must define.
notificationManager.notify(mNotificationId, mBuilder.build()); notificationManager.notify(NOTIFICATION_TAG, mNotificationId, mBuilder.build());
} }
} }
......
// 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.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.os.RemoteException;
import android.util.AndroidRuntimeException;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.chromium.base.ContextUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.content_public.browser.WebContents;
/**
* A per-tab object that manages notifications for ongoing media streams (microphone/camera). This
* object is created by {@link TabImpl} and creates and destroys its native equivalent.
*
* TODO(estade): remove notifications that have persisted across restarts (due to the app crashing).
*/
@JNINamespace("weblayer")
public class MediaStreamManager {
private static boolean sCreatedChannel = false;
private static final String WEBRTC_PREFIX = "org.chromium.weblayer.webrtc";
private static final String CHANNEL_ID = WEBRTC_PREFIX + ".channel";
private static final String EXTRA_TAB_ID = WEBRTC_PREFIX + ".TAB_ID";
private static final String ACTIVATE_TAB_INTENT = WEBRTC_PREFIX + ".ACTIVATE_TAB";
private static final String AV_STREAM_TAG = WEBRTC_PREFIX + ".avstream";
// The notification ID matches the tab ID, which uniquely identifies the notification when
// paired with the tag.
private int mNotificationId;
// Pointer to the native MediaStreamManager.
private long mNative;
/**
* @return a string that prefixes all intents that can be handled by {@link forwardIntent}.
*/
public static String getIntentPrefix() {
return WEBRTC_PREFIX;
}
/**
* Handles an intent coming from a media streaming notification.
* @param intent the intent which was previously posted via {@link update}.
*/
public static void forwardIntent(Intent intent) {
assert intent.getAction().equals(ACTIVATE_TAB_INTENT);
int tabId = intent.getIntExtra(EXTRA_TAB_ID, -1);
TabImpl tab = TabImpl.getTabById(tabId);
if (tab == null) return;
try {
tab.getClient().bringTabToFront();
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
}
}
public MediaStreamManager(TabImpl tab) {
mNotificationId = tab.getId();
mNative = MediaStreamManagerJni.get().create(this, tab.getWebContents());
}
public void destroy() {
NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager == null) return;
notificationManager.cancel(AV_STREAM_TAG, mNotificationId);
MediaStreamManagerJni.get().destroy(mNative);
}
/**
* Called after the tab's media streaming state has changed.
*
* A notification should be shown (or updated) iff one of the parameters is true, otherwise any
* existing notification will be removed.
*
* @param audio true if the tab is streaming audio.
* @param video true if the tab is streaming video.
*/
@CalledByNative
private void update(boolean audio, boolean video) {
// The notification intent is not handled in the client prior to M84.
if (WebLayerFactoryImpl.getClientMajorVersion() < 84) return;
NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager == null) return;
createNotificationChannel();
if (!audio && !video) {
notificationManager.cancel(AV_STREAM_TAG, mNotificationId);
return;
}
Intent intent = WebLayerImpl.createIntent();
intent.putExtra(EXTRA_TAB_ID, mNotificationId);
intent.setAction(ACTIVATE_TAB_INTENT);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
ContextUtils.getApplicationContext(), mNotificationId, intent, 0);
// TODO(estade): use localized text and correct icon.
NotificationCompat.Builder builder =
new NotificationCompat.Builder(ContextUtils.getApplicationContext(), CHANNEL_ID)
.setOngoing(true)
.setLocalOnly(true)
.setAutoCancel(false)
.setContentIntent(pendingIntent)
.setSmallIcon(android.R.drawable.ic_menu_camera)
.setContentTitle(audio && video
? "all the streamz"
: audio ? "audio streamz" : "video streamz");
notificationManager.notify(AV_STREAM_TAG, mNotificationId, builder.build());
}
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (!sCreatedChannel && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// TODO(estade): use localized channel name.
ContextUtils.getApplicationContext()
.getSystemService(NotificationManager.class)
.createNotificationChannel(new NotificationChannel(
CHANNEL_ID, "WebRTC", NotificationManager.IMPORTANCE_LOW));
}
sCreatedChannel = true;
}
private NotificationManagerCompat getNotificationManager() {
if (ContextUtils.getApplicationContext() == null) {
return null;
}
return NotificationManagerCompat.from(ContextUtils.getApplicationContext());
}
@NativeMethods
interface Natives {
long create(MediaStreamManager caller, WebContents webContents);
void destroy(long manager);
}
}
...@@ -50,7 +50,9 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper; ...@@ -50,7 +50,9 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Implementation of ITab. * Implementation of ITab.
...@@ -58,6 +60,8 @@ import java.util.List; ...@@ -58,6 +60,8 @@ import java.util.List;
@JNINamespace("weblayer") @JNINamespace("weblayer")
public final class TabImpl extends ITab.Stub { public final class TabImpl extends ITab.Stub {
private static int sNextId = 1; private static int sNextId = 1;
// Map from id to TabImpl.
private static final Map<Integer, TabImpl> sTabMap = new HashMap<Integer, TabImpl>();
private long mNativeTab; private long mNativeTab;
private ProfileImpl mProfile; private ProfileImpl mProfile;
...@@ -75,6 +79,7 @@ public final class TabImpl extends ITab.Stub { ...@@ -75,6 +79,7 @@ public final class TabImpl extends ITab.Stub {
* updateFromBrowser() is invoked. * updateFromBrowser() is invoked.
*/ */
private AutofillProvider mAutofillProvider; private AutofillProvider mAutofillProvider;
private MediaStreamManager mMediaStreamManager;
private NewTabCallbackProxy mNewTabCallbackProxy; private NewTabCallbackProxy mNewTabCallbackProxy;
private ITabClient mClient; private ITabClient mClient;
private final int mId; private final int mId;
...@@ -114,6 +119,10 @@ public final class TabImpl extends ITab.Stub { ...@@ -114,6 +119,10 @@ public final class TabImpl extends ITab.Stub {
public void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix) {} public void onScrollChanged(int lPix, int tPix, int oldlPix, int oldtPix) {}
} }
public static TabImpl getTabById(int tabId) {
return sTabMap.get(tabId);
}
public TabImpl(ProfileImpl profile, WindowAndroid windowAndroid) { public TabImpl(ProfileImpl profile, WindowAndroid windowAndroid) {
mId = ++sNextId; mId = ++sNextId;
init(profile, windowAndroid, TabImplJni.get().createTab(profile.getNativeProfile(), this)); init(profile, windowAndroid, TabImplJni.get().createTab(profile.getNativeProfile(), this));
...@@ -156,6 +165,8 @@ public final class TabImpl extends ITab.Stub { ...@@ -156,6 +165,8 @@ public final class TabImpl extends ITab.Stub {
}; };
mWebContents.addObserver(mWebContentsObserver); mWebContents.addObserver(mWebContentsObserver);
mMediaStreamManager = new MediaStreamManager(this);
mBrowserControlsDelegates = new ArrayList<BrowserControlsVisibilityDelegate>(); mBrowserControlsDelegates = new ArrayList<BrowserControlsVisibilityDelegate>();
mBrowserControlsVisibility = new ComposedBrowserControlsVisibilityDelegate(); mBrowserControlsVisibility = new ComposedBrowserControlsVisibilityDelegate();
for (int i = 0; i < ImplControlsVisibilityReason.REASON_COUNT; ++i) { for (int i = 0; i < ImplControlsVisibilityReason.REASON_COUNT; ++i) {
...@@ -168,6 +179,8 @@ public final class TabImpl extends ITab.Stub { ...@@ -168,6 +179,8 @@ public final class TabImpl extends ITab.Stub {
mBrowserControlsVisibility.addObserver(mConstraintsUpdatedCallback); mBrowserControlsVisibility.addObserver(mConstraintsUpdatedCallback);
mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(this); mInterceptNavigationDelegate = new InterceptNavigationDelegateImpl(this);
sTabMap.put(mId, this);
} }
public ProfileImpl getProfile() { public ProfileImpl getProfile() {
...@@ -536,6 +549,11 @@ public final class TabImpl extends ITab.Stub { ...@@ -536,6 +549,11 @@ public final class TabImpl extends ITab.Stub {
mInterceptNavigationDelegate.onTabDestroyed(); mInterceptNavigationDelegate.onTabDestroyed();
mInterceptNavigationDelegate = null; mInterceptNavigationDelegate = null;
mMediaStreamManager.destroy();
mMediaStreamManager = null;
sTabMap.remove(mId);
// ObservableSupplierImpl.addObserver() posts a task to notify the observer, ensure the // ObservableSupplierImpl.addObserver() posts a task to notify the observer, ensure the
// callback isn't run after destroy() is called (otherwise we'll get crashes as the native // callback isn't run after destroy() is called (otherwise we'll get crashes as the native
// tab has been deleted). // tab has been deleted).
......
...@@ -308,7 +308,11 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -308,7 +308,11 @@ public final class WebLayerImpl extends IWebLayer.Stub {
public void onReceivedBroadcast(IObjectWrapper appContextWrapper, Intent intent) { public void onReceivedBroadcast(IObjectWrapper appContextWrapper, Intent intent) {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class); Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class);
if (intent.getAction().startsWith(DownloadImpl.getIntentPrefix())) {
DownloadImpl.forwardIntent(context, intent, mProfileManager); DownloadImpl.forwardIntent(context, intent, mProfileManager);
} else if (intent.getAction().startsWith(MediaStreamManager.getIntentPrefix())) {
MediaStreamManager.forwardIntent(intent);
}
} }
@Override @Override
......
...@@ -29,4 +29,7 @@ interface ITabClient { ...@@ -29,4 +29,7 @@ interface ITabClient {
// Added in M83. // Added in M83.
void onTitleUpdated(in IObjectWrapper title) = 6; void onTitleUpdated(in IObjectWrapper title) = 6;
// Added in M84.
void bringTabToFront() = 7;
} }
...@@ -60,6 +60,7 @@ ...@@ -60,6 +60,7 @@
#include "weblayer/browser/java/jni/TabImpl_jni.h" #include "weblayer/browser/java/jni/TabImpl_jni.h"
#include "weblayer/browser/javascript_tab_modal_dialog_manager_delegate_android.h" #include "weblayer/browser/javascript_tab_modal_dialog_manager_delegate_android.h"
#include "weblayer/browser/top_controls_container_view.h" #include "weblayer/browser/top_controls_container_view.h"
#include "weblayer/browser/webrtc/media_stream_manager.h"
#endif #endif
#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION) #if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
...@@ -553,17 +554,13 @@ void TabImpl::RequestMediaAccessPermission( ...@@ -553,17 +554,13 @@ void TabImpl::RequestMediaAccessPermission(
content::WebContents* web_contents, content::WebContents* web_contents,
const content::MediaStreamRequest& request, const content::MediaStreamRequest& request,
content::MediaResponseCallback callback) { content::MediaResponseCallback callback) {
webrtc::MediaStreamDevicesController::RequestPermissions( #if defined(OS_ANDROID)
request, nullptr, MediaStreamManager::FromWebContents(web_contents)
base::BindOnce( ->RequestMediaAccessPermission(request, std::move(callback));
[](content::MediaResponseCallback callback, #else
const blink::MediaStreamDevices& devices, std::move(callback).Run(
blink::mojom::MediaStreamRequestResult result, {}, blink::mojom::MediaStreamRequestResult::NOT_SUPPORTED, nullptr);
bool blocked_by_feature_policy, ContentSetting audio_setting, #endif
ContentSetting video_setting) {
std::move(callback).Run(devices, result, {});
},
base::Passed(std::move(callback))));
} }
bool TabImpl::CheckMediaAccessPermission( bool TabImpl::CheckMediaAccessPermission(
......
// 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.
#include "weblayer/browser/webrtc/media_stream_manager.h"
#include "base/supports_user_data.h"
#include "components/webrtc/media_stream_devices_controller.h"
#include "content/public/browser/media_stream_request.h"
#include "content/public/browser/web_contents.h"
#include "weblayer/browser/java/jni/MediaStreamManager_jni.h"
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace weblayer {
namespace {
constexpr int kWebContentsUserDataKey = 0;
struct UserData : public base::SupportsUserData::Data {
MediaStreamManager* manager = nullptr;
};
} // namespace
// A class that tracks the lifecycle of a single active media stream. Ownership
// is passed off to MediaResponseCallback.
class MediaStreamManager::StreamUi : public content::MediaStreamUI {
public:
StreamUi(MediaStreamManager* manager,
const blink::MediaStreamDevices& devices)
: manager_(manager) {
DCHECK(manager_);
for (const auto& device : devices) {
if (device.type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE)
streaming_audio_ = true;
if (device.type == blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE)
streaming_video_ = true;
}
}
StreamUi(const StreamUi&) = delete;
StreamUi& operator=(const StreamUi&) = delete;
~StreamUi() override {
if (manager_)
manager_->UnregisterStream(this);
}
// content::MediaStreamUi:
gfx::NativeViewId OnStarted(base::OnceClosure stop,
SourceCallback source) override {
if (manager_)
manager_->RegisterStream(this);
return 0;
}
void OnManagerGone() { manager_ = nullptr; }
bool streaming_audio() const { return streaming_audio_; }
bool streaming_video() const { return streaming_video_; }
private:
MediaStreamManager* manager_;
bool streaming_audio_ = false;
bool streaming_video_ = false;
};
MediaStreamManager::MediaStreamManager(
const JavaParamRef<jobject>& j_object,
const JavaParamRef<jobject>& j_web_contents)
: j_object_(j_object) {
auto user_data = std::make_unique<UserData>();
user_data->manager = this;
content::WebContents::FromJavaWebContents(j_web_contents)
->SetUserData(&kWebContentsUserDataKey, std::move(user_data));
}
MediaStreamManager::~MediaStreamManager() {
for (auto* stream : active_streams_)
stream->OnManagerGone();
}
// static
MediaStreamManager* MediaStreamManager::FromWebContents(
content::WebContents* contents) {
UserData* user_data = reinterpret_cast<UserData*>(
contents->GetUserData(&kWebContentsUserDataKey));
DCHECK(user_data);
return user_data->manager;
}
void MediaStreamManager::RequestMediaAccessPermission(
const content::MediaStreamRequest& request,
content::MediaResponseCallback callback) {
webrtc::MediaStreamDevicesController::RequestPermissions(
request, nullptr,
base::BindOnce(&MediaStreamManager::OnMediaAccessPermissionResult,
weak_factory_.GetWeakPtr(),
base::Passed(std::move(callback))));
}
void MediaStreamManager::OnMediaAccessPermissionResult(
content::MediaResponseCallback callback,
const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result,
bool blocked_by_feature_policy,
ContentSetting audio_setting,
ContentSetting video_setting) {
std::move(callback).Run(devices, result,
std::make_unique<StreamUi>(this, devices));
}
void MediaStreamManager::RegisterStream(StreamUi* stream) {
active_streams_.insert(stream);
Update();
}
void MediaStreamManager::UnregisterStream(StreamUi* stream) {
active_streams_.erase(stream);
Update();
}
void MediaStreamManager::Update() {
bool audio = false;
bool video = false;
for (const auto* stream : active_streams_) {
audio = audio || stream->streaming_audio();
video = video || stream->streaming_video();
}
Java_MediaStreamManager_update(base::android::AttachCurrentThread(),
j_object_, audio, video);
}
static jlong JNI_MediaStreamManager_Create(
JNIEnv* env,
const JavaParamRef<jobject>& j_object,
const JavaParamRef<jobject>& j_web_contents) {
return reinterpret_cast<intptr_t>(
new MediaStreamManager(j_object, j_web_contents));
}
static void JNI_MediaStreamManager_Destroy(JNIEnv* env, jlong native_manager) {
delete reinterpret_cast<MediaStreamManager*>(native_manager);
}
} // namespace 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.
#ifndef WEBLAYER_BROWSER_WEBRTC_MEDIA_STREAM_MANAGER_H_
#define WEBLAYER_BROWSER_WEBRTC_MEDIA_STREAM_MANAGER_H_
#include <set>
#include "base/android/scoped_java_ref.h"
#include "base/memory/weak_ptr.h"
#include "components/content_settings/core/common/content_settings.h"
#include "content/public/browser/media_stream_request.h"
namespace content {
class WebContents;
}
namespace weblayer {
// On Android, this class tracks active media streams and updates the Java
// object of the same name as streams come and go. The class is created and
// destroyed by the Java object.
class MediaStreamManager {
public:
// It's expected that |j_web_contents| outlasts |this|.
MediaStreamManager(
const base::android::JavaParamRef<jobject>& j_object,
const base::android::JavaParamRef<jobject>& j_web_contents);
MediaStreamManager(const MediaStreamManager&) = delete;
MediaStreamManager& operator=(const MediaStreamManager&) = delete;
~MediaStreamManager();
static MediaStreamManager* FromWebContents(content::WebContents* contents);
// Requests media access permission for the tab, if necessary, and runs
// |callback| as appropriate. This will create a StreamUi.
void RequestMediaAccessPermission(const content::MediaStreamRequest& request,
content::MediaResponseCallback callback);
private:
class StreamUi;
void OnMediaAccessPermissionResult(
content::MediaResponseCallback callback,
const blink::MediaStreamDevices& devices,
blink::mojom::MediaStreamRequestResult result,
bool blocked_by_feature_policy,
ContentSetting audio_setting,
ContentSetting video_setting);
void RegisterStream(StreamUi* stream);
void UnregisterStream(StreamUi* stream);
void Update();
std::set<StreamUi*> active_streams_;
base::android::ScopedJavaGlobalRef<jobject> j_object_;
base::WeakPtrFactory<MediaStreamManager> weak_factory_{this};
};
} // namespace weblayer
#endif // WEBLAYER_BROWSER_WEBRTC_MEDIA_STREAM_MANAGER_H_
...@@ -63,6 +63,8 @@ ...@@ -63,6 +63,8 @@
<action android:name="org.chromium.weblayer.downloads.PAUSE"/> <action android:name="org.chromium.weblayer.downloads.PAUSE"/>
<action android:name="org.chromium.weblayer.downloads.RESUME"/> <action android:name="org.chromium.weblayer.downloads.RESUME"/>
<action android:name="org.chromium.weblayer.downloads.CANCEL"/> <action android:name="org.chromium.weblayer.downloads.CANCEL"/>
<!-- this needs to be in sync with MediaStreamManager.java-->
<action android:name="org.chromium.weblayer.webrtc.ACTIVATE_TAB"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>
......
...@@ -371,6 +371,14 @@ public class Tab { ...@@ -371,6 +371,14 @@ public class Tab {
callback.onTitleUpdated(titleString); callback.onTitleUpdated(titleString);
} }
} }
@Override
public void bringTabToFront() {
StrictModeWorkaround.apply();
for (TabCallback callback : mCallbacks) {
callback.bringTabToFront();
}
}
} }
private static final class ErrorPageCallbackClientImpl extends IErrorPageCallbackClient.Stub { private static final class ErrorPageCallbackClientImpl extends IErrorPageCallbackClient.Stub {
......
...@@ -48,4 +48,10 @@ public abstract class TabCallback { ...@@ -48,4 +48,10 @@ public abstract class TabCallback {
* @since 83 * @since 83
*/ */
public void onTitleUpdated(@NonNull String title) {} public void onTitleUpdated(@NonNull String title) {}
/**
* Called when user attention should be brought to this tab. This should cause the tab, its
* containing Activity, and the task to be foregrounded.
*/
public void bringTabToFront() {}
} }
...@@ -338,9 +338,19 @@ public class WebLayerShellActivity extends FragmentActivity { ...@@ -338,9 +338,19 @@ public class WebLayerShellActivity extends FragmentActivity {
@Override @Override
public void showContextMenu(ContextMenuParams params) { public void showContextMenu(ContextMenuParams params) {
View weblayerView = getSupportFragmentManager().getFragments().get(0).getView(); View webLayerView = getSupportFragmentManager().getFragments().get(0).getView();
weblayerView.setOnCreateContextMenuListener(new ContextMenuCreator(params)); webLayerView.setOnCreateContextMenuListener(new ContextMenuCreator(params));
weblayerView.showContextMenu(); webLayerView.showContextMenu();
}
@Override
public void bringTabToFront() {
tab.getBrowser().setActiveTab(tab);
Context context = WebLayerShellActivity.this;
Intent intent = new Intent(context, WebLayerShellActivity.class);
intent.setAction(Intent.ACTION_MAIN);
context.getApplicationContext().startActivity(intent);
} }
}); });
tab.getNavigationController().registerNavigationCallback(new NavigationCallback() { tab.getNavigationController().registerNavigationCallback(new NavigationCallback() {
......
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