Commit 0acc4d7a authored by Francois Beaufort's avatar Francois Beaufort Committed by Commit Bot

[Picture-in-Picture] Detect if feature is enabled on Android.

This CL exposes a new internal setting that determines whether
Picture-in-Picture is enabled and if the user has disabled
Picture-in-Picture for Chrome using the system's per-application
settings. This setting is used in document.pictureInPictureEnabled and
video.requestPictureInPicture().

Test page: https://beaufortfrancois.github.io/sandbox/media/picture-in-picture-enabled
Screenshots: https://i.imgur.com/nLL3l0U.png
Recording: https://drive.google.com/file/d/1DgIH8XPPGr7giPvaFQ8bvDd_WM-1lzSj/view

Bug: 806249
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation;master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: I6a4a8ca69c8b53a4bce3d8375c16725b3e545d52
Reviewed-on: https://chromium-review.googlesource.com/922081Reviewed-by: default avatarMaria Khomenko <mariakhomenko@chromium.org>
Reviewed-by: default avatarMike West <mkwst@chromium.org>
Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarNasko Oskov <nasko@chromium.org>
Commit-Queue: François Beaufort <beaufort.francois@gmail.com>
Cr-Commit-Position: refs/heads/master@{#539085}
parent e5ba4940
......@@ -87,6 +87,7 @@ import org.chromium.chrome.browser.infobar.InfoBarContainer;
import org.chromium.chrome.browser.init.AsyncInitializationActivity;
import org.chromium.chrome.browser.init.ProcessInitializationHandler;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.media.PictureInPicture;
import org.chromium.chrome.browser.media.PictureInPictureController;
import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.metrics.StartupMetrics;
......@@ -855,6 +856,11 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
FeatureUtilities.setIsInMultiWindowMode(
MultiWindowUtils.getInstance().isInMultiWindowMode(this));
if (getActivityTab() != null) {
getActivityTab().setPictureInPictureEnabled(
PictureInPicture.isEnabled(getApplicationContext()));
}
if (mPictureInPictureController != null) {
mPictureInPictureController.cleanup(this);
}
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.Build;
/**
* Utility for determining if Picture-in-Picture is available and whether the user has disabled
* Picture-in-Picture for Chrome using the system's per-application settings.
*/
public abstract class PictureInPicture {
private PictureInPicture() {}
/**
* Determines whether Picture-is-Picture is enabled for the app represented by |context|.
* Picture-in-Picture may be disabled because either the user, or a management tool, has
* explicitly disallowed the Chrome App to enter Picture-in-Picture.
*
* @param context The context to check of whether it can enter Picture-in-Picture.
* @return boolean true if Picture-In-Picture is enabled, otherwise false.
*/
public static boolean isEnabled(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
final AppOpsManager appOpsManager =
(AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
final int status = appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE,
context.getApplicationInfo().uid, context.getPackageName());
return (status == AppOpsManager.MODE_ALLOWED);
}
}
......@@ -3369,6 +3369,15 @@ public class Tab
nativeSetWebappManifestScope(mNativeTabAndroid, scope);
}
/**
* Configures web preferences for enabling Picture-in-Picture.
* @param enabled Whether Picture-in-Picture should be enabled.
*/
public void setPictureInPictureEnabled(boolean enabled) {
if (mNativeTabAndroid == 0) return;
nativeSetPictureInPictureEnabled(mNativeTabAndroid, enabled);
}
/**
* Configures web preferences for viewing downloaded media.
* @param enabled Whether embedded media experience should be enabled.
......@@ -3505,6 +3514,7 @@ public class Tab
private native void nativeClearThumbnailPlaceholder(long nativeTabAndroid);
private native boolean nativeHasPrerenderedUrl(long nativeTabAndroid, String url);
private native void nativeSetWebappManifestScope(long nativeTabAndroid, String scope);
private native void nativeSetPictureInPictureEnabled(long nativeTabAndroid, boolean enabled);
private native void nativeEnableEmbeddedMediaExperience(long nativeTabAndroid, boolean enabled);
private native void nativeAttachDetachedTab(long nativeTabAndroid);
private native void nativeMediaDownloadInProductHelpDismissed(long nativeTabAndroid);
......
......@@ -571,6 +571,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/locale/SpecialLocaleHandler.java",
"java/src/org/chromium/chrome/browser/media/MediaCaptureNotificationService.java",
"java/src/org/chromium/chrome/browser/media/MediaViewerUtils.java",
"java/src/org/chromium/chrome/browser/media/PictureInPicture.java",
"java/src/org/chromium/chrome/browser/media/PictureInPictureController.java",
"java/src/org/chromium/chrome/browser/media/cdm/MediaDrmCredentialManager.java",
"java/src/org/chromium/chrome/browser/media/remote/AbstractMediaRouteController.java",
......
......@@ -166,6 +166,7 @@ TabAndroid::TabAndroid(JNIEnv* env, const JavaRef<jobject>& obj)
content_layer_(cc::Layer::Create()),
tab_content_manager_(NULL),
synced_tab_delegate_(new browser_sync::SyncedTabDelegateAndroid(this)),
picture_in_picture_enabled_(false),
embedded_media_experience_enabled_(false),
weak_factory_(this) {
Java_Tab_setNativePtr(env, obj, reinterpret_cast<intptr_t>(this));
......@@ -797,6 +798,22 @@ bool TabAndroid::ShouldEnableEmbeddedMediaExperience() const {
return embedded_media_experience_enabled_;
}
void TabAndroid::SetPictureInPictureEnabled(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
jboolean enabled) {
picture_in_picture_enabled_ = enabled;
if (!web_contents() || !web_contents()->GetRenderViewHost())
return;
web_contents()->GetRenderViewHost()->OnWebkitPreferencesChanged();
}
bool TabAndroid::IsPictureInPictureEnabled() const {
return picture_in_picture_enabled_;
}
void TabAndroid::AttachDetachedTab(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) {
......
......@@ -253,6 +253,13 @@ class TabAndroid : public CoreTabHelperDelegate,
return webapp_manifest_scope_;
}
void SetPictureInPictureEnabled(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
jboolean enabled);
bool IsPictureInPictureEnabled() const;
void EnableEmbeddedMediaExperience(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
......@@ -316,6 +323,7 @@ class TabAndroid : public CoreTabHelperDelegate,
std::unique_ptr<browser_sync::SyncedTabDelegateAndroid> synced_tab_delegate_;
std::string webapp_manifest_scope_;
bool picture_in_picture_enabled_;
bool embedded_media_experience_enabled_;
std::unique_ptr<MediaDownloadInProductHelp> media_in_product_help_;
......
......@@ -2721,6 +2721,9 @@ void ChromeContentBrowserClient::OverrideWebkitPrefs(
web_prefs->embedded_media_experience_enabled =
tab_android->ShouldEnableEmbeddedMediaExperience();
web_prefs->picture_in_picture_enabled =
tab_android->IsPictureInPictureEnabled();
if (base::FeatureList::IsEnabled(
features::kAllowAutoplayUnmutedInWebappManifestScope)) {
web_prefs->media_playback_gesture_whitelist_scope =
......
......@@ -199,6 +199,7 @@ IPC_STRUCT_TRAITS_BEGIN(content::WebPreferences)
IPC_STRUCT_TRAITS_MEMBER(force_enable_zoom)
IPC_STRUCT_TRAITS_MEMBER(fullscreen_supported)
IPC_STRUCT_TRAITS_MEMBER(double_tap_to_zoom_enabled)
IPC_STRUCT_TRAITS_MEMBER(picture_in_picture_enabled)
IPC_STRUCT_TRAITS_MEMBER(media_playback_gesture_whitelist_scope)
IPC_STRUCT_TRAITS_MEMBER(default_video_poster_url)
IPC_STRUCT_TRAITS_MEMBER(support_deprecated_target_density_dpi)
......
......@@ -219,6 +219,7 @@ struct CONTENT_EXPORT WebPreferences {
bool force_enable_zoom;
bool fullscreen_supported;
bool double_tap_to_zoom_enabled;
bool picture_in_picture_enabled;
std::string media_playback_gesture_whitelist_scope;
GURL default_video_poster_url;
bool support_deprecated_target_density_dpi;
......
......@@ -865,6 +865,7 @@ void RenderView::ApplyWebPreferences(const WebPreferences& prefs,
web_view->SetIgnoreViewportTagScaleLimits(prefs.force_enable_zoom);
settings->SetAutoZoomFocusedNodeToLegibleScale(true);
settings->SetDoubleTapToZoomEnabled(prefs.double_tap_to_zoom_enabled);
settings->SetPictureInPictureEnabled(prefs.picture_in_picture_enabled);
settings->SetMediaPlaybackGestureWhitelistScope(
blink::WebString::FromUTF8(prefs.media_playback_gesture_whitelist_scope));
settings->SetDefaultVideoPosterURL(
......
<!DOCTYPE html>
<title>Test Picture-in-Picture disabled by system</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../resources/testdriver.js"></script>
<script src="../resources/testdriver-vendor.js"></script>
<script src="../external/wpt/picture-in-picture/resources/picture-in-picture-helpers.js"></script>
<body></body>
<script>
promise_test(t => {
assert_true(document.pictureInPictureEnabled);
window.internals.settings.setPictureInPictureEnabled(false);
assert_false(document.pictureInPictureEnabled);
return promise_rejects(t, 'NotSupportedError',
requestPictureInPictureWithTrustedClick(document.createElement('video')));
});
</script>
......@@ -617,6 +617,10 @@ void WebSettingsImpl::SetShouldRespectImageOrientation(bool enabled) {
settings_->SetShouldRespectImageOrientation(enabled);
}
void WebSettingsImpl::SetPictureInPictureEnabled(bool enabled) {
settings_->SetPictureInPictureEnabled(enabled);
}
void WebSettingsImpl::SetMediaPlaybackGestureWhitelistScope(
const WebString& scope) {
settings_->SetMediaPlaybackGestureWhitelistScope(scope);
......
......@@ -116,6 +116,7 @@ class CORE_EXPORT WebSettingsImpl final : public WebSettings {
void SetMainFrameClipsContent(bool) override;
void SetMainFrameResizesAreOrientationChanges(bool) override;
void SetMaxTouchPoints(int) override;
void SetPictureInPictureEnabled(bool) override;
void SetMediaPlaybackGestureWhitelistScope(const WebString&) override;
void SetPresentationRequiresUserGesture(bool) override;
void SetEmbeddedMediaExperienceEnabled(bool) override;
......
......@@ -215,6 +215,11 @@
initial: true,
},
{
name: "pictureInPictureEnabled",
initial: true,
},
{
name: "mediaPlaybackGestureWhitelistScope",
type: "String",
......
......@@ -289,7 +289,6 @@ jumbo_source_set("unit_tests") {
"payments/PaymentsValidatorsTest.cpp",
"peerconnection/RTCDataChannelTest.cpp",
"peerconnection/RTCPeerConnectionTest.cpp",
"picture_in_picture/PictureInPictureTest.cpp",
"presentation/MockPresentationService.h",
"presentation/MockWebPresentationClient.h",
"presentation/PresentationAvailabilityStateTest.cpp",
......
include_rules = [
"-modules",
"+modules/EventTargetModules.h",
"+modules/ModulesExport.h",
"+modules/picture_in_picture",
]
\ No newline at end of file
......@@ -7,7 +7,6 @@
#include "core/dom/QualifiedName.h"
#include "modules/EventTargetModules.h"
#include "modules/ModulesExport.h"
#include "platform/heap/Handle.h"
namespace blink {
......@@ -16,7 +15,7 @@ class HTMLVideoElement;
class ScriptPromise;
class ScriptState;
class MODULES_EXPORT HTMLVideoElementPictureInPicture {
class HTMLVideoElementPictureInPicture {
STATIC_ONLY(HTMLVideoElementPictureInPicture);
public:
......
......@@ -5,6 +5,7 @@
#include "modules/picture_in_picture/PictureInPictureController.h"
#include "core/dom/Document.h"
#include "core/frame/Settings.h"
#include "core/html/media/HTMLVideoElement.h"
#include "modules/picture_in_picture/PictureInPictureWindow.h"
#include "platform/feature_policy/FeaturePolicy.h"
......@@ -45,9 +46,10 @@ PictureInPictureController::IsDocumentAllowed() const {
if (!frame)
return Status::kFrameDetached;
// `picture_in_picture_enabled_` is set to false by the embedder when it
// or the system forbids the page from using Picture-in-Picture.
if (!picture_in_picture_enabled_)
// `GetPictureInPictureEnabled()` returns false when the embedder or the
// system forbids the page from using Picture-in-Picture.
DCHECK(GetSupplementable()->GetSettings());
if (!GetSupplementable()->GetSettings()->GetPictureInPictureEnabled())
return Status::kDisabledBySystem;
// If document is not allowed to use the policy-controlled feature named
......@@ -74,11 +76,6 @@ PictureInPictureController::Status PictureInPictureController::IsElementAllowed(
return Status::kEnabled;
}
void PictureInPictureController::SetPictureInPictureEnabledForTesting(
bool value) {
picture_in_picture_enabled_ = value;
}
void PictureInPictureController::SetPictureInPictureElement(
HTMLVideoElement& element) {
picture_in_picture_element_ = &element;
......
......@@ -6,7 +6,6 @@
#define PictureInPictureController_h
#include "core/frame/LocalFrame.h"
#include "modules/ModulesExport.h"
namespace blink {
......@@ -20,7 +19,7 @@ class PictureInPictureWindow;
// PictureInPictureController instance is associated to a Document. It is
// supplement and therefore can be lazy-initiated. Callers should consider
// whether they want to instantiate an object when they make a call.
class MODULES_EXPORT PictureInPictureController
class PictureInPictureController
: public GarbageCollectedFinalized<PictureInPictureController>,
public Supplement<Document> {
USING_GARBAGE_COLLECTED_MIXIN(PictureInPictureController);
......@@ -37,8 +36,6 @@ class MODULES_EXPORT PictureInPictureController
// the associated document.
bool PictureInPictureEnabled() const;
void SetPictureInPictureEnabledForTesting(bool);
// List of Picture-in-Picture support statuses. If status is kEnabled,
// Picture-in-Picture is enabled for a document or element, otherwise it is
// not supported.
......@@ -82,10 +79,6 @@ class MODULES_EXPORT PictureInPictureController
private:
explicit PictureInPictureController(Document&);
// Whether system allows Picture-in-Picture feature for the associated
// document.
bool picture_in_picture_enabled_ = true;
// The Picture-in-Picture element for the associated document.
Member<HTMLVideoElement> picture_in_picture_element_;
......
// Copyright 2018 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 "bindings/core/v8/V8BindingForCore.h"
#include "core/html/media/HTMLVideoElement.h"
#include "core/testing/PageTestBase.h"
#include "modules/picture_in_picture/HTMLVideoElementPictureInPicture.h"
#include "modules/picture_in_picture/PictureInPictureController.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace blink {
const char kNotSupportedString[] =
"NotSupportedError: Picture-in-Picture is not available.";
class PictureInPictureTest : public PageTestBase {
protected:
void SetUp() final { PageTestBase::SetUp(); }
ScriptState* GetScriptState() {
return ToScriptStateForMainWorld(GetDocument().GetFrame());
}
v8::Isolate* GetIsolate() { return GetScriptState()->GetIsolate(); }
v8::Local<v8::Context> GetContext() { return GetScriptState()->GetContext(); }
// Convenience methods for testing the returned promises.
ScriptValue GetRejectValue(ScriptPromise& promise) {
ScriptValue on_reject;
promise.Then(UnreachableFunction::Create(GetScriptState()),
TestFunction::Create(GetScriptState(), &on_reject));
v8::MicrotasksScope::PerformCheckpoint(GetIsolate());
return on_reject;
}
std::string GetRejectString(ScriptPromise& promise) {
ScriptValue on_reject = GetRejectValue(promise);
return ToCoreString(
on_reject.V8Value()->ToString(GetContext()).ToLocalChecked())
.Ascii()
.data();
}
private:
// A ScriptFunction that creates a test failure if it is ever called.
class UnreachableFunction : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state) {
UnreachableFunction* self = new UnreachableFunction(script_state);
return self->BindToV8Function();
}
ScriptValue Call(ScriptValue value) override {
ADD_FAILURE() << "Unexpected call to a null ScriptFunction.";
return value;
}
private:
UnreachableFunction(ScriptState* script_state)
: ScriptFunction(script_state) {}
};
// A ScriptFunction that saves its parameter; used by tests to assert on
// correct values being passed.
class TestFunction : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state,
ScriptValue* out_value) {
TestFunction* self = new TestFunction(script_state, out_value);
return self->BindToV8Function();
}
ScriptValue Call(ScriptValue value) override {
DCHECK(!value.IsEmpty());
*value_ = value;
return value;
}
private:
TestFunction(ScriptState* script_state, ScriptValue* out_value)
: ScriptFunction(script_state), value_(out_value) {}
ScriptValue* value_;
};
};
TEST_F(PictureInPictureTest,
RequestPictureInPictureRejectsWhenPictureInPictureEnabledIsFalse) {
Persistent<PictureInPictureController> controller =
PictureInPictureController::Ensure(GetDocument());
ScriptState::Scope scope(GetScriptState());
HTMLVideoElement& video =
static_cast<HTMLVideoElement&>(*HTMLVideoElement::Create(GetDocument()));
controller->SetPictureInPictureEnabledForTesting(false);
ScriptPromise promise =
HTMLVideoElementPictureInPicture::requestPictureInPicture(
GetScriptState(), video);
EXPECT_EQ(kNotSupportedString, GetRejectString(promise));
}
TEST_F(PictureInPictureTest,
PictureInPictureEnabledReturnsFalseWhenPictureInPictureEnabledIsFalse) {
Persistent<PictureInPictureController> controller =
PictureInPictureController::Ensure(GetDocument());
controller->SetPictureInPictureEnabledForTesting(false);
EXPECT_FALSE(controller->PictureInPictureEnabled());
}
} // namespace blink
......@@ -199,6 +199,7 @@ class WebSettings {
virtual void SetMainFrameClipsContent(bool) = 0;
virtual void SetMainFrameResizesAreOrientationChanges(bool) = 0;
virtual void SetMaxTouchPoints(int) = 0;
virtual void SetPictureInPictureEnabled(bool) = 0;
virtual void SetMediaPlaybackGestureWhitelistScope(const WebString&) = 0;
virtual void SetPresentationRequiresUserGesture(bool) = 0;
virtual void SetEmbeddedMediaExperienceEnabled(bool) = 0;
......
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