forked from mirrors/gecko-dev
Bug 1776829 - Implement "Paste" permission action for clipboard.readText. r=geckoview-reviewers,owlish
When calling `clipboard.readText` on content script, Gecko dispatches `MozClipboardReadPaste` event. On Desktop Firefox uses XUL pop up window to handle it, then it shows "Paste" button whether user can allow to read clipboard data. But GeckoView doesn't have XUL pop up. To implement this feature, we show "Paste" pop up using action mode as default. Also, browser side can override delegated methods if it wants another permission pop up or to support Android L (Android L doesn't have action mode). Differential Revision: https://phabricator.services.mozilla.com/D151102
This commit is contained in:
parent
27d6b966c4
commit
e73b7956db
12 changed files with 598 additions and 1 deletions
100
mobile/android/actors/GeckoViewClipboardPermissionChild.jsm
Normal file
100
mobile/android/actors/GeckoViewClipboardPermissionChild.jsm
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { GeckoViewActorChild } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorChild.jsm"
|
||||
);
|
||||
|
||||
const EXPORTED_SYMBOLS = ["GeckoViewClipboardPermissionChild"];
|
||||
|
||||
class GeckoViewClipboardPermissionChild extends GeckoViewActorChild {
|
||||
constructor() {
|
||||
super();
|
||||
this._pendingPromise = null;
|
||||
}
|
||||
|
||||
async promptPermissionForClipboardRead() {
|
||||
const uri = this.contentWindow.location.href;
|
||||
|
||||
const { x, y } = await this.sendQuery(
|
||||
"ClipboardReadTextPaste:GetLastPointerLocation"
|
||||
);
|
||||
|
||||
const promise = this.eventDispatcher.sendRequestForResult({
|
||||
type: "GeckoView:ClipboardPermissionRequest",
|
||||
uri,
|
||||
screenPoint: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
|
||||
this._pendingPromise = promise;
|
||||
|
||||
try {
|
||||
const allowOrDeny = await promise;
|
||||
if (this._pendingPromise !== promise) {
|
||||
// Current pending promise is newer. So it means that this promise
|
||||
// is already resolved or rejected. Do nothing.
|
||||
return;
|
||||
}
|
||||
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
|
||||
allowOrDeny
|
||||
);
|
||||
this._pendingPromise = null;
|
||||
} catch (error) {
|
||||
debug`Permission error: ${error}`;
|
||||
|
||||
if (this._pendingPromise !== promise) {
|
||||
// Current pending promise is newer. So it means that this promise
|
||||
// is already resolved or rejected. Do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
|
||||
false
|
||||
);
|
||||
this._pendingPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(aEvent) {
|
||||
debug`handleEvent: ${aEvent.type}`;
|
||||
|
||||
switch (aEvent.type) {
|
||||
case "MozClipboardReadPaste":
|
||||
if (aEvent.isTrusted) {
|
||||
this.promptPermissionForClipboardRead();
|
||||
}
|
||||
break;
|
||||
|
||||
// page hide or deactivate cancel clipboard permission.
|
||||
case "pagehide":
|
||||
// fallthrough for the next three events.
|
||||
case "deactivate":
|
||||
case "mousedown":
|
||||
case "mozvisualscroll":
|
||||
// Gecko desktop uses XUL popup to show clipboard permission prompt.
|
||||
// So it will be closed automatically by scroll and other user
|
||||
// activation. So GeckoView has to close permission prompt by some user
|
||||
// activations, too.
|
||||
|
||||
this.eventDispatcher.sendRequest({
|
||||
type: "GeckoView:DismissClipboardPermissionRequest",
|
||||
});
|
||||
if (this._pendingPromise) {
|
||||
this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup(
|
||||
false
|
||||
);
|
||||
this._pendingPromise = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewClipboardPermissionChild.initLogging(
|
||||
"GeckoViewClipboardPermissionChild"
|
||||
);
|
||||
49
mobile/android/actors/GeckoViewClipboardPermissionParent.jsm
Normal file
49
mobile/android/actors/GeckoViewClipboardPermissionParent.jsm
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { GeckoViewActorParent } = ChromeUtils.import(
|
||||
"resource://gre/modules/GeckoViewActorParent.jsm"
|
||||
);
|
||||
|
||||
const EXPORTED_SYMBOLS = ["GeckoViewClipboardPermissionParent"];
|
||||
|
||||
class GeckoViewClipboardPermissionParent extends GeckoViewActorParent {
|
||||
getLastOverWindowPointerLocation() {
|
||||
const mouseXInCSSPixels = {};
|
||||
const mouseYInCSSPixels = {};
|
||||
const windowUtils = this.window.windowUtils;
|
||||
windowUtils.getLastOverWindowPointerLocationInCSSPixels(
|
||||
mouseXInCSSPixels,
|
||||
mouseYInCSSPixels
|
||||
);
|
||||
const screenRect = windowUtils.toScreenRect(
|
||||
mouseXInCSSPixels.value,
|
||||
mouseYInCSSPixels.value,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
x: screenRect.x,
|
||||
y: screenRect.y,
|
||||
};
|
||||
}
|
||||
|
||||
receiveMessage(aMessage) {
|
||||
debug`receiveMessage: ${aMessage.name}`;
|
||||
|
||||
switch (aMessage.name) {
|
||||
case "ClipboardReadTextPaste:GetLastPointerLocation":
|
||||
return this.getLastOverWindowPointerLocation();
|
||||
|
||||
default:
|
||||
return super.receiveMessage(aMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { debug, warn } = GeckoViewClipboardPermissionParent.initLogging(
|
||||
"GeckoViewClipboardPermissionParent"
|
||||
);
|
||||
|
|
@ -10,6 +10,8 @@ FINAL_TARGET_FILES.actors += [
|
|||
"ContentDelegateParent.jsm",
|
||||
"GeckoViewAutoFillChild.jsm",
|
||||
"GeckoViewAutoFillParent.jsm",
|
||||
"GeckoViewClipboardPermissionChild.jsm",
|
||||
"GeckoViewClipboardPermissionParent.jsm",
|
||||
"GeckoViewContentChild.jsm",
|
||||
"GeckoViewContentParent.jsm",
|
||||
"GeckoViewFormValidationChild.jsm",
|
||||
|
|
|
|||
|
|
@ -92,6 +92,22 @@ const JSWINDOWACTORS = {
|
|||
allFrames: true,
|
||||
messageManagerGroups: ["browsers"],
|
||||
},
|
||||
GeckoViewClipboardPermission: {
|
||||
parent: {
|
||||
moduleURI: "resource:///actors/GeckoViewClipboardPermissionParent.jsm",
|
||||
},
|
||||
child: {
|
||||
moduleURI: "resource:///actors/GeckoViewClipboardPermissionChild.jsm",
|
||||
events: {
|
||||
MozClipboardReadPaste: {},
|
||||
deactivate: { mozSystemGroup: true },
|
||||
mousedown: { capture: true, mozSystemGroup: true },
|
||||
mozvisualscroll: { mozSystemGroup: true },
|
||||
pagehide: { capture: true, mozSystemGroup: true },
|
||||
},
|
||||
},
|
||||
allFrames: true,
|
||||
},
|
||||
};
|
||||
|
||||
class GeckoViewStartup {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.content.res.Configuration;
|
|||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
|
|
@ -952,6 +953,9 @@ package org.mozilla.geckoview {
|
|||
field @Nullable protected GeckoSession.Window mWindow;
|
||||
}
|
||||
|
||||
@Retention(value=RetentionPolicy.SOURCE) public static interface GeckoSession.ClipboardPermissionType {
|
||||
}
|
||||
|
||||
public static interface GeckoSession.ContentDelegate {
|
||||
method @UiThread default public void onCloseRequest(@NonNull GeckoSession);
|
||||
method @UiThread default public void onContextMenu(@NonNull GeckoSession, int, int, @NonNull GeckoSession.ContentDelegate.ContextElement);
|
||||
|
|
@ -1446,8 +1450,10 @@ package org.mozilla.geckoview {
|
|||
}
|
||||
|
||||
public static interface GeckoSession.SelectionActionDelegate {
|
||||
method @UiThread default public void onDismissClipboardPermissionRequest(@NonNull GeckoSession);
|
||||
method @UiThread default public void onHideAction(@NonNull GeckoSession, int);
|
||||
method @UiThread default public void onShowActionRequest(@NonNull GeckoSession, @NonNull GeckoSession.SelectionActionDelegate.Selection);
|
||||
method @Nullable @UiThread default public GeckoResult<AllowOrDeny> onShowClipboardPermissionRequest(@NonNull GeckoSession, @NonNull GeckoSession.SelectionActionDelegate.ClipboardPermission);
|
||||
field public static final String ACTION_COLLAPSE_TO_END = "org.mozilla.geckoview.COLLAPSE_TO_END";
|
||||
field public static final String ACTION_COLLAPSE_TO_START = "org.mozilla.geckoview.COLLAPSE_TO_START";
|
||||
field public static final String ACTION_COPY = "org.mozilla.geckoview.COPY";
|
||||
|
|
@ -1465,6 +1471,14 @@ package org.mozilla.geckoview {
|
|||
field public static final int HIDE_REASON_ACTIVE_SELECTION = 2;
|
||||
field public static final int HIDE_REASON_INVISIBLE_SELECTION = 1;
|
||||
field public static final int HIDE_REASON_NO_SELECTION = 0;
|
||||
field public static final int PERMISSION_CLIPBOARD_READ = 1;
|
||||
}
|
||||
|
||||
public static class GeckoSession.SelectionActionDelegate.ClipboardPermission {
|
||||
ctor protected ClipboardPermission();
|
||||
field @Nullable public final Point screenPoint;
|
||||
field public final int type;
|
||||
field @NonNull public final String uri;
|
||||
}
|
||||
|
||||
public static class GeckoSession.SelectionActionDelegate.Selection {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hello, world!</title>
|
||||
<meta name="viewport" content="initial-scale=1.0">
|
||||
</head>
|
||||
<body style="height: 100%">
|
||||
<p>Hello, world!</p>
|
||||
<script>
|
||||
document.body.addEventListener("click", () => {
|
||||
navigator.clipboard.readText().then(() => {
|
||||
window.alert("allow");
|
||||
}).catch(() => {
|
||||
window.alert("deny");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -35,6 +35,7 @@ open class BaseSessionTest(noErrorCollector: Boolean = false) {
|
|||
const val RESUBMIT_CONFIRM = "/assets/www/resubmit.html"
|
||||
const val BEFORE_UNLOAD = "/assets/www/beforeunload.html"
|
||||
const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
|
||||
const val CLIPBOARD_READ_HTML_PATH = "/assets/www/clipboard_read.html"
|
||||
const val CONTENT_CRASH_URL = "about:crashcontent"
|
||||
const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
|
||||
const val FORM_BLANK_HTML_PATH = "/assets/www/form_blank.html"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
package org.mozilla.geckoview.test
|
||||
|
||||
import org.mozilla.geckoview.AllowOrDeny
|
||||
import org.mozilla.geckoview.GeckoResult
|
||||
import org.mozilla.geckoview.GeckoSession.PromptDelegate
|
||||
import org.mozilla.geckoview.GeckoSession.SelectionActionDelegate
|
||||
import org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.*
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
|
||||
|
|
@ -13,6 +16,7 @@ import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||
|
|
@ -254,6 +258,117 @@ class SelectionActionDelegateTest : BaseSessionTest() {
|
|||
testClientRect(selectedContent, jsCssReset, jsBorder10pxPadding10px, expectedDiff)
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test fun clipboardReadAllow() {
|
||||
assumeThat("Unnecessary to run multiple times", id, equalTo("#text"));
|
||||
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf("dom.events.asyncClipboard.readText" to true))
|
||||
|
||||
val url = createTestUrl(CLIPBOARD_READ_HTML_PATH)
|
||||
mainSession.loadUri(url)
|
||||
mainSession.waitForPageStop()
|
||||
|
||||
// Select allow
|
||||
val result = GeckoResult<Void>()
|
||||
mainSession.delegateDuringNextWait(object : SelectionActionDelegate, PromptDelegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onShowClipboardPermissionRequest(
|
||||
session: GeckoSession, perm: ClipboardPermission):
|
||||
GeckoResult<AllowOrDeny> {
|
||||
assertThat("URI should match", perm.uri, startsWith(url))
|
||||
assertThat("Type should match", perm.type,
|
||||
equalTo(SelectionActionDelegate.PERMISSION_CLIPBOARD_READ))
|
||||
assertThat("screenPoint should match", perm.screenPoint, equalTo(Point(50, 50)))
|
||||
return GeckoResult.allow()
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1, order = [2])
|
||||
override fun onAlertPrompt(
|
||||
session: GeckoSession, prompt: PromptDelegate.AlertPrompt):
|
||||
GeckoResult<PromptDelegate.PromptResponse> {
|
||||
assertThat("Message should match", "allow", equalTo(prompt.message))
|
||||
result.complete(null)
|
||||
return GeckoResult.fromValue(prompt.dismiss())
|
||||
}
|
||||
})
|
||||
|
||||
mainSession.synthesizeTap(50, 50) // Provides user activation.
|
||||
sessionRule.waitForResult(result)
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test fun clipboardReadDeny() {
|
||||
assumeThat("Unnecessary to run multiple times", id, equalTo("#text"));
|
||||
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf("dom.events.asyncClipboard.readText" to true))
|
||||
|
||||
val url = createTestUrl(CLIPBOARD_READ_HTML_PATH)
|
||||
mainSession.loadUri(url)
|
||||
mainSession.waitForPageStop()
|
||||
|
||||
// Select deny
|
||||
val result = GeckoResult<Void>()
|
||||
mainSession.delegateDuringNextWait(object : SelectionActionDelegate, PromptDelegate {
|
||||
@AssertCalled(count = 1, order = [1])
|
||||
override fun onShowClipboardPermissionRequest(
|
||||
session: GeckoSession, perm: ClipboardPermission):
|
||||
GeckoResult<AllowOrDeny>? {
|
||||
assertThat("URI should match", perm.uri, startsWith(url))
|
||||
assertThat("Type should match", perm.type,
|
||||
equalTo(SelectionActionDelegate.PERMISSION_CLIPBOARD_READ))
|
||||
return GeckoResult.deny()
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1, order = [2])
|
||||
override fun onAlertPrompt(
|
||||
session: GeckoSession, prompt: PromptDelegate.AlertPrompt):
|
||||
GeckoResult<PromptDelegate.PromptResponse> {
|
||||
assertThat("Message should match", "deny", equalTo(prompt.message))
|
||||
result.complete(null)
|
||||
return GeckoResult.fromValue(prompt.dismiss())
|
||||
}
|
||||
})
|
||||
|
||||
mainSession.synthesizeTap(50, 50) // Provides user activation.
|
||||
sessionRule.waitForResult(result)
|
||||
}
|
||||
|
||||
@WithDisplay(width = 100, height = 100)
|
||||
@Test fun clipboardReadDeactivate() {
|
||||
assumeThat("Unnecessary to run multiple times", id, equalTo("#text"));
|
||||
|
||||
sessionRule.setPrefsUntilTestEnd(mapOf("dom.events.asyncClipboard.readText" to true))
|
||||
|
||||
val url = createTestUrl(CLIPBOARD_READ_HTML_PATH)
|
||||
mainSession.loadUri(url)
|
||||
mainSession.waitForPageStop()
|
||||
|
||||
val result = GeckoResult<Void>()
|
||||
mainSession.delegateDuringNextWait(object : SelectionActionDelegate {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onShowClipboardPermissionRequest(
|
||||
session: GeckoSession, perm: ClipboardPermission):
|
||||
GeckoResult<AllowOrDeny>? {
|
||||
assertThat("Type should match", perm.type,
|
||||
equalTo(SelectionActionDelegate.PERMISSION_CLIPBOARD_READ))
|
||||
result.complete(null)
|
||||
return GeckoResult()
|
||||
}
|
||||
});
|
||||
|
||||
mainSession.synthesizeTap(50, 50) // Provides user activation.
|
||||
sessionRule.waitForResult(result)
|
||||
|
||||
mainSession.delegateDuringNextWait(object : SelectionActionDelegate {
|
||||
@AssertCalled
|
||||
override fun onDismissClipboardPermissionRequest(session: GeckoSession) {
|
||||
}
|
||||
});
|
||||
|
||||
mainSession.loadTestPath(HELLO_HTML_PATH)
|
||||
sessionRule.waitForPageStop()
|
||||
}
|
||||
|
||||
/** Interface that defines behavior for a particular type of content */
|
||||
private interface SelectedContent {
|
||||
fun focus() {}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.mozilla.gecko.util;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
|
@ -398,6 +399,21 @@ public final class GeckoBundle implements Parcelable {
|
|||
(float) rectBundle.getDouble("bottom"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value associated with a Point mapping, or null if the mapping does not exist.
|
||||
*
|
||||
* @param key Key to look for.
|
||||
* @return Point value
|
||||
*/
|
||||
public Point getPoint(final String key) {
|
||||
final GeckoBundle ptBundle = getBundle(key);
|
||||
if (ptBundle == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Point(ptBundle.getInt("x"), ptBundle.getInt("y"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value associated with a GeckoBundle mapping, or null if the mapping does not exist.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import android.content.ActivityNotFoundException;
|
|||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
|
|
@ -77,6 +78,8 @@ public class BasicSelectionActionDelegate
|
|||
protected @Nullable Selection mSelection;
|
||||
protected boolean mRepopulatedMenu;
|
||||
|
||||
private @Nullable ActionMode mActionModeForClipboardPermission;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private class Callback2Wrapper extends ActionMode.Callback2 {
|
||||
@Override
|
||||
|
|
@ -444,6 +447,11 @@ public class BasicSelectionActionDelegate
|
|||
return;
|
||||
}
|
||||
|
||||
if (mActionModeForClipboardPermission != null) {
|
||||
mActionModeForClipboardPermission.finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mUseFloatingToolbar) {
|
||||
mActionMode = mActivity.startActionMode(new Callback2Wrapper(), ActionMode.TYPE_FLOATING);
|
||||
} else {
|
||||
|
|
@ -473,4 +481,159 @@ public class BasicSelectionActionDelegate
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Callback class of clipboard permission. This is used on pre-M only */
|
||||
private class ClipboardPermissionCallback implements ActionMode.Callback {
|
||||
private GeckoResult<AllowOrDeny> mResult;
|
||||
|
||||
public ClipboardPermissionCallback(final GeckoResult<AllowOrDeny> result) {
|
||||
mResult = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
|
||||
return BasicSelectionActionDelegate.this.onCreateActionModeForClipboardPermission(
|
||||
actionMode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
|
||||
mResult.complete(AllowOrDeny.ALLOW);
|
||||
mResult = null;
|
||||
actionMode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode actionMode) {
|
||||
if (mResult != null) {
|
||||
mResult.complete(AllowOrDeny.DENY);
|
||||
}
|
||||
BasicSelectionActionDelegate.this.onDestroyActionModeForClipboardPermission(actionMode);
|
||||
}
|
||||
}
|
||||
|
||||
/** Callback class of clipboard permission for Android M+ */
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private class ClipboardPermissionCallbackM extends ActionMode.Callback2 {
|
||||
private @Nullable GeckoResult<AllowOrDeny> mResult;
|
||||
private final @NonNull GeckoSession mSession;
|
||||
private final @Nullable Point mPoint;
|
||||
|
||||
public ClipboardPermissionCallbackM(
|
||||
final @NonNull GeckoSession session,
|
||||
final @Nullable Point screenPoint,
|
||||
final @NonNull GeckoResult<AllowOrDeny> result) {
|
||||
mSession = session;
|
||||
mPoint = screenPoint;
|
||||
mResult = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
|
||||
return BasicSelectionActionDelegate.this.onCreateActionModeForClipboardPermission(
|
||||
actionMode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
|
||||
mResult.complete(AllowOrDeny.ALLOW);
|
||||
mResult = null;
|
||||
actionMode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(final ActionMode actionMode) {
|
||||
if (mResult != null) {
|
||||
mResult.complete(AllowOrDeny.DENY);
|
||||
}
|
||||
BasicSelectionActionDelegate.this.onDestroyActionModeForClipboardPermission(actionMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetContentRect(final ActionMode mode, final View view, final Rect outRect) {
|
||||
super.onGetContentRect(mode, view, outRect);
|
||||
|
||||
if (mPoint == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
outRect.set(mPoint.x, mPoint.y, mPoint.x + 1, mPoint.y + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show action mode bar to request clipboard permission
|
||||
*
|
||||
* @param session The GeckoSession that initiated the callback.
|
||||
* @param permission An {@link ClipboardPermission} describing the permission being requested.
|
||||
* @return A {@link GeckoResult} with {@link AllowOrDeny}, determining the response to the
|
||||
* permission request for this site.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public GeckoResult<AllowOrDeny> onShowClipboardPermissionRequest(
|
||||
final GeckoSession session, final ClipboardPermission permission) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
final GeckoResult<AllowOrDeny> result = new GeckoResult<>();
|
||||
|
||||
if (mActionMode != null) {
|
||||
mActionMode.finish();
|
||||
mActionMode = null;
|
||||
}
|
||||
if (mActionModeForClipboardPermission != null) {
|
||||
mActionModeForClipboardPermission.finish();
|
||||
mActionModeForClipboardPermission = null;
|
||||
}
|
||||
|
||||
if (mUseFloatingToolbar) {
|
||||
mActionModeForClipboardPermission =
|
||||
mActivity.startActionMode(
|
||||
new ClipboardPermissionCallbackM(session, permission.screenPoint, result),
|
||||
ActionMode.TYPE_FLOATING);
|
||||
} else {
|
||||
mActionModeForClipboardPermission =
|
||||
mActivity.startActionMode(new ClipboardPermissionCallback(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss action mode for requesting clipboard permission popup or model.
|
||||
*
|
||||
* @param session The GeckoSession that initiated the callback.
|
||||
*/
|
||||
@Override
|
||||
public void onDismissClipboardPermissionRequest(final GeckoSession session) {
|
||||
ThreadUtils.assertOnUiThread();
|
||||
|
||||
if (mActionModeForClipboardPermission != null) {
|
||||
mActionModeForClipboardPermission.finish();
|
||||
mActionModeForClipboardPermission = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ boolean onCreateActionModeForClipboardPermission(
|
||||
final ActionMode actionMode, final Menu menu) {
|
||||
final MenuItem item = menu.add(/* group */ Menu.NONE, Menu.FIRST, Menu.FIRST, /* title */ "");
|
||||
item.setTitle(android.R.string.paste);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* package */ void onDestroyActionModeForClipboardPermission(final ActionMode actionMode) {
|
||||
mActionModeForClipboardPermission = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import android.content.Context;
|
|||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
|
@ -895,6 +896,8 @@ public class GeckoSession {
|
|||
"GeckoView:ShowSelectionAction",
|
||||
"GeckoView:HideMagnifier",
|
||||
"GeckoView:ShowMagnifier",
|
||||
"GeckoView:ClipboardPermissionRequest",
|
||||
"GeckoView:DismissClipboardPermissionRequest",
|
||||
}) {
|
||||
@Override
|
||||
public void handleMessage(
|
||||
|
|
@ -902,6 +905,7 @@ public class GeckoSession {
|
|||
final String event,
|
||||
final GeckoBundle message,
|
||||
final EventCallback callback) {
|
||||
Log.d(LOGTAG, "handleMessage: " + event);
|
||||
if ("GeckoView:ShowSelectionAction".equals(event)) {
|
||||
final @SelectionActionDelegateAction HashSet<String> actionsSet =
|
||||
new HashSet<>(Arrays.asList(message.getStringArray("actions")));
|
||||
|
|
@ -941,6 +945,25 @@ public class GeckoSession {
|
|||
GeckoSession.this.getMagnifier().show(new PointF(origin[0], origin[1]));
|
||||
} else if ("GeckoView:HideMagnifier".equals(event)) {
|
||||
GeckoSession.this.getMagnifier().dismiss();
|
||||
} else if ("GeckoView:ClipboardPermissionRequest".equals(event)) {
|
||||
final SelectionActionDelegate.ClipboardPermission permission =
|
||||
new SelectionActionDelegate.ClipboardPermission(message);
|
||||
|
||||
final GeckoResult<AllowOrDeny> result =
|
||||
delegate.onShowClipboardPermissionRequest(GeckoSession.this, permission);
|
||||
callback.resolveTo(
|
||||
result.map(
|
||||
value -> {
|
||||
if (value == AllowOrDeny.ALLOW) {
|
||||
return true;
|
||||
}
|
||||
if (value == AllowOrDeny.DENY) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid response");
|
||||
}));
|
||||
} else if ("GeckoView:DismissClipboardPermissionRequest".equals(event)) {
|
||||
delegate.onDismissClipboardPermissionRequest(GeckoSession.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -3671,6 +3694,63 @@ public class GeckoSession {
|
|||
@UiThread
|
||||
default void onHideAction(
|
||||
@NonNull final GeckoSession session, @SelectionActionDelegateHideReason final int reason) {}
|
||||
|
||||
/**
|
||||
* Permission for reading clipboard data. See: <a
|
||||
* href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText">Clipboard.readText()</a>
|
||||
*/
|
||||
int PERMISSION_CLIPBOARD_READ = 1;
|
||||
|
||||
/** Represents attributes of a clipboard permission. */
|
||||
public class ClipboardPermission {
|
||||
/** The URI associated with this content permission. */
|
||||
public final @NonNull String uri;
|
||||
|
||||
/**
|
||||
* The type of this permission; one of {@link #PERMISSION_CLIPBOARD_READ
|
||||
* PERMISSION_CLIPBOARD_*}.
|
||||
*/
|
||||
public final @ClipboardPermissionType int type;
|
||||
/**
|
||||
* The last mouse or touch location in screen coordinates when the permission is requested.
|
||||
*/
|
||||
public final @Nullable Point screenPoint;
|
||||
|
||||
/** Empty constructor for tests */
|
||||
protected ClipboardPermission() {
|
||||
this.uri = "";
|
||||
this.type = PERMISSION_CLIPBOARD_READ;
|
||||
this.screenPoint = null;
|
||||
}
|
||||
|
||||
private ClipboardPermission(final @NonNull GeckoBundle bundle) {
|
||||
this.uri = bundle.getString("uri");
|
||||
this.type = PERMISSION_CLIPBOARD_READ;
|
||||
this.screenPoint = bundle.getPoint("screenPoint");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request clipboard permission.
|
||||
*
|
||||
* @param session The GeckoSession that initiated the callback.
|
||||
* @param permission An {@link ClipboardPermission} describing the permission being requested.
|
||||
* @return A {@link GeckoResult} with {@link AllowOrDeny}, determining the response to the
|
||||
* permission request for this site.
|
||||
*/
|
||||
@UiThread
|
||||
default @Nullable GeckoResult<AllowOrDeny> onShowClipboardPermissionRequest(
|
||||
@NonNull final GeckoSession session, @NonNull ClipboardPermission permission) {
|
||||
return GeckoResult.deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss requesting clipboard permission popup or model.
|
||||
*
|
||||
* @param session The GeckoSession that initiated the callback.
|
||||
*/
|
||||
@UiThread
|
||||
default void onDismissClipboardPermissionRequest(@NonNull final GeckoSession session) {}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -3707,6 +3787,12 @@ public class GeckoSession {
|
|||
})
|
||||
public @interface SelectionActionDelegateHideReason {}
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
SelectionActionDelegate.PERMISSION_CLIPBOARD_READ,
|
||||
})
|
||||
public @interface ClipboardPermissionType {}
|
||||
|
||||
public interface NavigationDelegate {
|
||||
/**
|
||||
* A view has started loading content from the network.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,22 @@ exclude: true
|
|||
|
||||
⚠️ breaking change and deprecation notices
|
||||
|
||||
## v106
|
||||
- Added [`SelectionActionDelegate.onShowClipboardPermissionRequest`][106.1],
|
||||
[`SelectionActionDelegate.onDismissClipboardPermissionRequest`][106.2],
|
||||
[`BasicSelectionActionDelegate.onShowClipboardPermissionRequest`][106.3],
|
||||
[`BasicSelectionActionDelegate.onDismissCancelClipboardPermissionRequest`][106.4] and
|
||||
[`SelectionActionDelegate.ClipboardPermission`][106.5] to handle permission
|
||||
request for reading clipboard data by [`clipboard.readText`][106.6].
|
||||
([bug 1776829]({{bugzilla}}1776829))
|
||||
|
||||
[106.1]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.html#onShowClipboardPermissionRequest(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.ClipboardPermission)
|
||||
[106.2]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.html#onDismissClipboardPermissionRequest(org.mozilla.geckoview.GeckoSession)
|
||||
[106.3]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#onShowClipboardPermissionRequest(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.ClipboardPermission)
|
||||
[106.4]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#onDismissClipboardPermission(org.mozilla.geckoview.GeckoSession)
|
||||
[106.5]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.ClipboardPermission.html
|
||||
[106.6]: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText
|
||||
|
||||
## v104
|
||||
- Removed deprecated Autofill.Delegate `onAutofill`, Autofill.Node `fillViewStructure`, `getFocused`, `getId`, `getValue`, `getVisible`, Autofill.NodeData `Autofill.Notify`, Autofill.Session `surfaceChanged`.
|
||||
([bug 1781180]({{bugzilla}}1781180))
|
||||
|
|
@ -1224,4 +1240,4 @@ to allow adding gecko profiler markers.
|
|||
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
|
||||
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
||||
|
||||
[api-version]: 771802b68452c32d672df605e3a26d0eebd89b84
|
||||
[api-version]: 5dd4f92b49dec51709788671173c5a03b303287b
|
||||
|
|
|
|||
Loading…
Reference in a new issue