diff --git a/mobile/android/actors/GeckoViewClipboardPermissionChild.jsm b/mobile/android/actors/GeckoViewClipboardPermissionChild.jsm new file mode 100644 index 000000000000..d89dd9445326 --- /dev/null +++ b/mobile/android/actors/GeckoViewClipboardPermissionChild.jsm @@ -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" +); diff --git a/mobile/android/actors/GeckoViewClipboardPermissionParent.jsm b/mobile/android/actors/GeckoViewClipboardPermissionParent.jsm new file mode 100644 index 000000000000..1fd293505797 --- /dev/null +++ b/mobile/android/actors/GeckoViewClipboardPermissionParent.jsm @@ -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" +); diff --git a/mobile/android/actors/moz.build b/mobile/android/actors/moz.build index 859e3e17a25c..4ed7fae41e64 100644 --- a/mobile/android/actors/moz.build +++ b/mobile/android/actors/moz.build @@ -10,6 +10,8 @@ FINAL_TARGET_FILES.actors += [ "ContentDelegateParent.jsm", "GeckoViewAutoFillChild.jsm", "GeckoViewAutoFillParent.jsm", + "GeckoViewClipboardPermissionChild.jsm", + "GeckoViewClipboardPermissionParent.jsm", "GeckoViewContentChild.jsm", "GeckoViewContentParent.jsm", "GeckoViewFormValidationChild.jsm", diff --git a/mobile/android/components/geckoview/GeckoViewStartup.jsm b/mobile/android/components/geckoview/GeckoViewStartup.jsm index e290303d5eda..2652f4874b70 100644 --- a/mobile/android/components/geckoview/GeckoViewStartup.jsm +++ b/mobile/android/components/geckoview/GeckoViewStartup.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 { diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt index 9cce6841654d..a1abc2383eb2 100644 --- a/mobile/android/geckoview/api.txt +++ b/mobile/android/geckoview/api.txt @@ -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 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 { diff --git a/mobile/android/geckoview/src/androidTest/assets/www/clipboard_read.html b/mobile/android/geckoview/src/androidTest/assets/www/clipboard_read.html new file mode 100644 index 000000000000..ee82a7f4a58c --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/assets/www/clipboard_read.html @@ -0,0 +1,19 @@ + + + + Hello, world! + + + +

Hello, world!

+ + + diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt index aa6b0729f6bf..df063e9673b4 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt @@ -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" diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SelectionActionDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SelectionActionDelegateTest.kt index b58f271fc203..15ab730682f6 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SelectionActionDelegateTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SelectionActionDelegateTest.kt @@ -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() + mainSession.delegateDuringNextWait(object : SelectionActionDelegate, PromptDelegate { + @AssertCalled(count = 1) + override fun onShowClipboardPermissionRequest( + session: GeckoSession, perm: ClipboardPermission): + GeckoResult { + 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 { + 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() + mainSession.delegateDuringNextWait(object : SelectionActionDelegate, PromptDelegate { + @AssertCalled(count = 1, order = [1]) + override fun onShowClipboardPermissionRequest( + session: GeckoSession, perm: ClipboardPermission): + GeckoResult? { + 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 { + 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() + mainSession.delegateDuringNextWait(object : SelectionActionDelegate { + @AssertCalled(count = 1) + override fun onShowClipboardPermissionRequest( + session: GeckoSession, perm: ClipboardPermission): + GeckoResult? { + 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() {} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java index b83ee5f26539..7647bb393dd0 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java @@ -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. * diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java index c8d3e4221a58..feef54ceaa8b 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java @@ -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 mResult; + + public ClipboardPermissionCallback(final GeckoResult 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 mResult; + private final @NonNull GeckoSession mSession; + private final @Nullable Point mPoint; + + public ClipboardPermissionCallbackM( + final @NonNull GeckoSession session, + final @Nullable Point screenPoint, + final @NonNull GeckoResult 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 onShowClipboardPermissionRequest( + final GeckoSession session, final ClipboardPermission permission) { + ThreadUtils.assertOnUiThread(); + + final GeckoResult 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; + } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java index a8a0a1200c98..c5f832fea243 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java @@ -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 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 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: Clipboard.readText() + */ + 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 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. diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md index 49d8ba866277..56a76fe32a5e 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md @@ -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