Bug 1581161 - Removed GeckoResponse and reworked Text Selection API. r=geckoview-reviewers,snorp,esawin
authorGabriel Luong <gabriel.luong@gmail.com>
Thu, 14 Nov 2019 16:02:04 +0000
changeset 501973 5c9324524d5148b1ae6963768dad2f0d3779f8c1
parent 501972 ec6979a95056337dc316159ce353b39d98b86bd9
child 501974 f09b9242197689e2949df164e6d628a01593b526
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, snorp, esawin
bugs1581161
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1581161 - Removed GeckoResponse and reworked Text Selection API. r=geckoview-reviewers,snorp,esawin Differential Revision: https://phabricator.services.mozilla.com/D47116
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/SelectionActionDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResponse.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -60,17 +60,16 @@ import java.util.Map;
 import org.json.JSONObject;
 import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.Autofill;
 import org.mozilla.geckoview.CompositorController;
 import org.mozilla.geckoview.ContentBlocking;
 import org.mozilla.geckoview.ContentBlockingController;
 import org.mozilla.geckoview.DynamicToolbarAnimator;
 import org.mozilla.geckoview.GeckoDisplay;
-import org.mozilla.geckoview.GeckoResponse;
 import org.mozilla.geckoview.GeckoResult;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 import org.mozilla.geckoview.MediaElement;
 import org.mozilla.geckoview.OverscrollEdgeEffect;
@@ -165,29 +164,29 @@ package org.mozilla.geckoview {
     method @AnyThread @NonNull public Rect getDefaultDimensions();
     method @AnyThread @NonNull public Autofill.Node getRoot();
   }
 
   @UiThread public class BasicSelectionActionDelegate implements ActionMode.Callback GeckoSession.SelectionActionDelegate {
     ctor public BasicSelectionActionDelegate(@NonNull Activity);
     ctor public BasicSelectionActionDelegate(@NonNull Activity, boolean);
     method public boolean areExternalActionsEnabled();
+    method public void clearSelection();
     method public void enableExternalActions(boolean);
+    method @Nullable public GeckoSession.SelectionActionDelegate.Selection getSelection();
+    method public boolean isActionAvailable();
     method public void onGetContentRect(@Nullable ActionMode, @Nullable View, @NonNull Rect);
-    method protected void clearSelection();
     method @NonNull protected String[] getAllActions();
     method protected boolean isActionAvailable(@NonNull String);
     method protected boolean performAction(@NonNull String, @NonNull MenuItem);
     method protected void prepareAction(@NonNull String, @NonNull MenuItem);
     field protected static final String ACTION_PROCESS_TEXT = "android.intent.action.PROCESS_TEXT";
     field @Nullable protected ActionMode mActionMode;
-    field @Nullable protected List<String> mActions;
     field @NonNull protected final Activity mActivity;
     field protected boolean mRepopulatedMenu;
-    field @Nullable protected GeckoResponse<String> mResponse;
     field @Nullable protected GeckoSession.SelectionActionDelegate.Selection mSelection;
     field @Nullable protected GeckoSession mSession;
     field @NonNull protected final Matrix mTempMatrix;
     field @NonNull protected final RectF mTempRect;
     field protected final boolean mUseFloatingToolbar;
   }
 
   @UiThread public final class CompositorController {
@@ -399,20 +398,16 @@ package org.mozilla.geckoview {
     method @AnyThread @NonNull public GeckoDisplay.ScreenshotBuilder bitmap(@Nullable Bitmap);
     method @UiThread @NonNull public GeckoResult<Bitmap> capture();
     method @AnyThread @NonNull public GeckoDisplay.ScreenshotBuilder scale(float);
     method @AnyThread @NonNull public GeckoDisplay.ScreenshotBuilder size(int, int);
     method @AnyThread @NonNull public GeckoDisplay.ScreenshotBuilder source(int, int, int, int);
     method @AnyThread @NonNull public GeckoDisplay.ScreenshotBuilder source(@NonNull Rect);
   }
 
-  public interface GeckoResponse<T> {
-    method @AnyThread public void respond(@Nullable T);
-  }
-
   @AnyThread public class GeckoResult<T> {
     ctor public GeckoResult();
     ctor public GeckoResult(Handler);
     ctor public GeckoResult(GeckoResult<T>);
     method @NonNull public GeckoResult<Void> accept(@Nullable GeckoResult.Consumer<T>);
     method @NonNull public GeckoResult<Void> accept(@Nullable GeckoResult.Consumer<T>, @Nullable GeckoResult.Consumer<Throwable>);
     method public synchronized void complete(@Nullable T);
     method public synchronized void completeExceptionally(@NonNull Throwable);
@@ -1016,17 +1011,17 @@ package org.mozilla.geckoview {
   }
 
   public static interface GeckoSession.ScrollDelegate {
     method @UiThread default public void onScrollChanged(@NonNull GeckoSession, int, int);
   }
 
   public static interface GeckoSession.SelectionActionDelegate {
     method @UiThread default public void onHideAction(@NonNull GeckoSession, int);
-    method @UiThread default public void onShowActionRequest(@NonNull GeckoSession, @NonNull GeckoSession.SelectionActionDelegate.Selection, @NonNull String[], @NonNull GeckoResponse<String>);
+    method @UiThread default public void onShowActionRequest(@NonNull GeckoSession, @NonNull GeckoSession.SelectionActionDelegate.Selection);
     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";
     field public static final String ACTION_CUT = "org.mozilla.geckoview.CUT";
     field public static final String ACTION_DELETE = "org.mozilla.geckoview.DELETE";
     field public static final String ACTION_HIDE = "org.mozilla.geckoview.HIDE";
     field public static final String ACTION_PASTE = "org.mozilla.geckoview.PASTE";
     field public static final String ACTION_SELECT_ALL = "org.mozilla.geckoview.SELECT_ALL";
@@ -1037,16 +1032,28 @@ package org.mozilla.geckoview {
     field public static final int HIDE_REASON_ACTIVE_SCROLL = 3;
     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;
   }
 
   public static class GeckoSession.SelectionActionDelegate.Selection {
     ctor protected Selection();
+    method @AnyThread public void collapseToEnd();
+    method @AnyThread public void collapseToStart();
+    method @AnyThread public void copy();
+    method @AnyThread public void cut();
+    method @AnyThread public void delete();
+    method @AnyThread public void execute(@NonNull String);
+    method @AnyThread public void hide();
+    method @AnyThread public boolean isActionAvailable(@NonNull String);
+    method @AnyThread public void paste();
+    method @AnyThread public void selectAll();
+    method @AnyThread public void unselect();
+    field @NonNull public final Collection<String> availableActions;
     field @Nullable public final RectF clientRect;
     field public final int flags;
     field @NonNull public final String text;
   }
 
   @AnyThread public static class GeckoSession.SessionState extends AbstractSequentialList implements GeckoSession.HistoryDelegate.HistoryList Parcelable {
     ctor public SessionState(@NonNull GeckoSession.SessionState);
     method @NonNull public static GeckoSession.SessionState fromString(@NonNull String);
--- 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
@@ -1,16 +1,14 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
-import org.mozilla.geckoview.GeckoResponse
-import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.*
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.NullDelegate
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.util.Callbacks
 
 import android.content.ClipData
 import android.content.ClipboardManager
@@ -22,16 +20,17 @@ import org.hamcrest.Matcher
 import org.hamcrest.Matchers.*
 import org.json.JSONArray
 import org.junit.Assume.assumeThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameter
 import org.junit.runners.Parameterized.Parameters
+import org.mozilla.geckoview.GeckoSession
 
 @MediumTest
 @RunWith(Parameterized::class)
 @WithDisplay(width = 100, height = 100)
 class SelectionActionDelegateTest : BaseSessionTest() {
     enum class ContentType {
         DIV, EDITABLE_ELEMENT, IFRAME
     }
@@ -190,41 +189,41 @@ class SelectionActionDelegateTest : Base
         fun select() {}
         val initialContent: String
         val content: String
         val selectionOffsets: Pair<Int, Int>
     }
 
     /** Main method that performs test logic. */
     private fun testThat(content: SelectedContent,
-                         respondingWith: (GeckoResponse<String>) -> Unit,
+                         respondingWith: (Selection) -> Unit,
                          result: (SelectedContent) -> Unit,
                          vararg sideEffects: (SelectedContent) -> Unit) {
 
         mainSession.loadTestPath(INPUTS_PATH)
         mainSession.waitForPageStop()
 
         content.focus()
 
         // Show selection actions for collapsed selections, so we can test them.
         // Also, always show accessible carets / selection actions for changes due to JS calls.
         sessionRule.setPrefsUntilTestEnd(mapOf(
                 "geckoview.selection_action.show_on_focus" to true,
                 "layout.accessiblecaret.script_change_update_mode" to 2))
 
         mainSession.delegateDuringNextWait(object : Callbacks.SelectionActionDelegate {
-            override fun onShowActionRequest(session: GeckoSession, selection: GeckoSession.SelectionActionDelegate.Selection, actions: Array<out String>, response: GeckoResponse<String>) {
-                respondingWith(response)
+            override fun onShowActionRequest(session: GeckoSession, selection: GeckoSession.SelectionActionDelegate.Selection) {
+                respondingWith(selection)
             }
         })
 
         content.select()
         mainSession.waitUntilCalled(object : Callbacks.SelectionActionDelegate {
             @AssertCalled(count = 1)
-            override fun onShowActionRequest(session: GeckoSession, selection: Selection, actions: Array<out String>, response: GeckoResponse<String>) {
+            override fun onShowActionRequest(session: GeckoSession, selection: Selection) {
                 assertThat("Initial content should match",
                            selection.text, equalTo(content.initialContent))
             }
         })
 
         result(content)
         sideEffects.forEach { it(content) }
     }
@@ -356,41 +355,41 @@ class SelectionActionDelegateTest : Base
 
     inner class CollapsedFrame(id: String) : SelectedFrame(id, "") {
         override fun select() = selectTo(0)
     }
 
 
     /** Lambda for responding with certain actions. */
 
-    private fun withResponse(vararg actions: String): (GeckoResponse<String>) -> Unit {
+    private fun withResponse(vararg actions: String): (Selection) -> Unit {
         var responded = false
         return { response ->
             if (!responded) {
                 responded = true
-                actions.forEach { response.respond(it) }
+                actions.forEach { response.execute(it) }
             }
         }
     }
 
 
     /** Lambdas for asserting the results of actions. */
 
     private fun hasShowActionRequest(expectedFlags: Int,
                                      expectedActions: Array<out String>) = { it: SelectedContent ->
         mainSession.forCallbacksDuringWait(object : Callbacks.SelectionActionDelegate {
             @AssertCalled(count = 1)
-            override fun onShowActionRequest(session: GeckoSession, selection: GeckoSession.SelectionActionDelegate.Selection, actions: Array<out String>, response: GeckoResponse<String>) {
+            override fun onShowActionRequest(session: GeckoSession, selection: GeckoSession.SelectionActionDelegate.Selection) {
                 assertThat("Selection text should be valid",
                            selection.text, equalTo(it.initialContent))
                 assertThat("Selection flags should be valid",
                            selection.flags, equalTo(expectedFlags))
                 assertThat("Selection rect should be valid",
                            selection.clientRect!!.isEmpty, equalTo(false))
-                assertThat("Actions must be valid", actions,
+                assertThat("Actions must be valid", selection.availableActions.toTypedArray(),
                            arrayContainingInAnyOrder(*expectedActions))
             }
         })
     }
 
     private fun copiesText() = { it: SelectedContent ->
         sessionRule.waitUntilCalled(ClipboardManager.OnPrimaryClipChangedListener {
             assertThat("Clipboard should contain correct text",
@@ -399,17 +398,17 @@ class SelectionActionDelegateTest : Base
         })
     }
 
     private fun changesSelectionTo(text: String) = changesSelectionTo(equalTo(text))
 
     private fun changesSelectionTo(matcher: Matcher<String>) = { _: SelectedContent ->
         sessionRule.waitUntilCalled(object : Callbacks.SelectionActionDelegate {
             @AssertCalled(count = 1)
-            override fun onShowActionRequest(session: GeckoSession, selection: Selection, actions: Array<out String>, response: GeckoResponse<String>) {
+            override fun onShowActionRequest(session: GeckoSession, selection: Selection) {
                 assertThat("New selection text should match", selection.text, matcher)
             }
         })
     }
 
     private fun clearsSelection() = { _: SelectedContent ->
         sessionRule.waitUntilCalled(object : Callbacks.SelectionActionDelegate {
             @AssertCalled(count = 1)
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -3,17 +3,16 @@
  * 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/. */
 
 package org.mozilla.geckoview.test.util
 
 import org.mozilla.geckoview.AllowOrDeny
 import org.mozilla.geckoview.Autofill
 import org.mozilla.geckoview.ContentBlocking
-import org.mozilla.geckoview.GeckoResponse
 import org.mozilla.geckoview.GeckoResult
 import org.mozilla.geckoview.GeckoSession
 import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement
 import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
 import org.mozilla.geckoview.MediaElement
 import org.mozilla.geckoview.WebRequestError
 
 import android.view.inputmethod.CursorAnchorInfo
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/BasicSelectionActionDelegate.java
@@ -21,19 +21,16 @@ import android.support.annotation.UiThre
 import android.util.Log;
 import android.view.ActionMode;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 
 import org.mozilla.gecko.util.ThreadUtils;
 
-import java.util.Arrays;
-import java.util.List;
-
 /**
  * Class that implements a basic SelectionActionDelegate. This class is used by GeckoView by
  * default if the consumer does not explicitly set a SelectionActionDelegate.
  *
  * To provide custom actions, extend this class and override the following methods,
  *
  * 1) Override {@link #getAllActions} to include custom action IDs in the returned array. This
  * array must include all actions, available or not, and must not change over the class lifetime.
@@ -63,18 +60,16 @@ public class BasicSelectionActionDelegat
     protected final @NonNull Matrix mTempMatrix = new Matrix();
     protected final @NonNull RectF mTempRect = new RectF();
 
     private boolean mExternalActionsEnabled;
 
     protected @Nullable ActionMode mActionMode;
     protected @Nullable GeckoSession mSession;
     protected @Nullable Selection mSelection;
-    protected @Nullable List<String> mActions;
-    protected @Nullable GeckoResponse<String> mResponse;
     protected boolean mRepopulatedMenu;
 
     @TargetApi(Build.VERSION_CODES.M)
     private class Callback2Wrapper extends ActionMode.Callback2 {
         @Override
         public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
             return BasicSelectionActionDelegate.this.onCreateActionMode(actionMode, menu);
         }
@@ -149,23 +144,42 @@ public class BasicSelectionActionDelegat
     /**
      * Return whether an action is presently available. Override to indicate
      * availability for custom actions.
      *
      * @param id Action ID.
      * @return True if the action is presently available.
      */
     protected boolean isActionAvailable(final @NonNull String id) {
+        if (mSelection == null) {
+            return false;
+        }
+
         if (mExternalActionsEnabled && !mSelection.text.isEmpty() &&
                 ACTION_PROCESS_TEXT.equals(id)) {
             final PackageManager pm = mActivity.getPackageManager();
             return pm.resolveActivity(getProcessTextIntent(),
                                       PackageManager.MATCH_DEFAULT_ONLY) != null;
         }
-        return mActions.contains(id);
+        return mSelection.isActionAvailable(id);
+    }
+
+    /**
+     * Provides access to whether there are text selection actions available. Override to indicate
+     * availability for custom actions.
+     *
+     * @return True if there are text selection actions available.
+     */
+    public boolean isActionAvailable() {
+        if (mSelection == null) {
+            return false;
+        }
+
+        return isActionAvailable(ACTION_PROCESS_TEXT) ||
+                !mSelection.availableActions.isEmpty();
     }
 
     /**
      * Prepare a menu item corresponding to a certain action. Override to prepare
      * menu item for custom action.
      *
      * @param id Action ID.
      * @param item New menu item to prepare.
@@ -202,44 +216,57 @@ public class BasicSelectionActionDelegat
                 mActivity.startActivity(item.getIntent());
             } catch (final ActivityNotFoundException e) {
                 Log.e(LOGTAG, "Cannot perform action", e);
                 return false;
             }
             return true;
         }
 
-        if (mResponse == null) {
+        if (mSelection == null) {
             return false;
         }
-        mResponse.respond(id);
+        mSelection.execute(id);
 
         // Android behavior is to clear selection on copy.
         if (ACTION_COPY.equals(id)) {
             if (mUseFloatingToolbar) {
                 clearSelection();
             } else {
                 mActionMode.finish();
             }
         }
         return true;
     }
 
     /**
+     * Get the current selection object. This object should not be stored as it does not update
+     * when the selection becomes invalid. Stale actions are ignored.
+     *
+     * @return The {@link GeckoSession.SelectionActionDelegate.Selection} attached to the current
+     *         action menu. <code>null</code> if no action menu is active.
+     */
+    public @Nullable Selection getSelection() {
+        return mSelection;
+    }
+
+    /**
      * Clear the current selection, if possible.
      */
-    protected void clearSelection() {
-        if (mResponse != null) {
-            if (isActionAvailable(ACTION_COLLAPSE_TO_END)) {
-                mResponse.respond(ACTION_COLLAPSE_TO_END);
-            } else if (isActionAvailable(ACTION_UNSELECT)) {
-                mResponse.respond(ACTION_UNSELECT);
-            } else {
-                mResponse.respond(ACTION_HIDE);
-            }
+    public void clearSelection() {
+        if (mSelection == null) {
+            return;
+        }
+
+        if (isActionAvailable(ACTION_COLLAPSE_TO_END)) {
+            mSelection.collapseToEnd();
+        } else if (isActionAvailable(ACTION_UNSELECT)) {
+            mSelection.unselect();
+        } else {
+            mSelection.hide();
         }
     }
 
     private Intent getProcessTextIntent() {
         final Intent intent = new Intent(Intent.ACTION_PROCESS_TEXT);
         intent.addCategory(Intent.CATEGORY_DEFAULT);
         intent.setType("text/plain");
         intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mSelection.text);
@@ -339,45 +366,39 @@ public class BasicSelectionActionDelegat
     @Override
     public void onDestroyActionMode(final ActionMode actionMode) {
         ThreadUtils.assertOnUiThread();
         if (!mUseFloatingToolbar) {
             clearSelection();
         }
         mSession = null;
         mSelection = null;
-        mActions = null;
-        mResponse = null;
         mActionMode = null;
     }
 
     public void onGetContentRect(final @Nullable ActionMode mode, final @Nullable View view,
                                  final @NonNull Rect outRect) {
         ThreadUtils.assertOnUiThread();
-        if (mSelection.clientRect == null) {
+        if (mSelection == null || mSelection.clientRect == null) {
             return;
         }
         mSession.getClientToScreenMatrix(mTempMatrix);
         mTempMatrix.mapRect(mTempRect, mSelection.clientRect);
         mTempRect.roundOut(outRect);
     }
 
     @TargetApi(Build.VERSION_CODES.M)
     @Override
-    public void onShowActionRequest(final GeckoSession session, final Selection selection,
-                                    final String[] actions,
-                                    final GeckoResponse<String> response) {
+    public void onShowActionRequest(final GeckoSession session, final Selection selection) {
         ThreadUtils.assertOnUiThread();
         mSession = session;
         mSelection = selection;
-        mActions = Arrays.asList(actions);
-        mResponse = response;
 
         if (mActionMode != null) {
-            if (actions.length > 0) {
+            if (isActionAvailable()) {
                 mActionMode.invalidate();
             } else {
                 mActionMode.finish();
             }
             return;
         }
 
         if (mUseFloatingToolbar) {
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoResponse.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * vim: ts=4 sw=4 expandtab:
- * 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/. */
-
-package org.mozilla.geckoview;
-
-import android.support.annotation.AnyThread;
-import android.support.annotation.Nullable;
-
-/**
- * This is used to receive async responses from delegate methods.
- */
-public interface GeckoResponse<T> {
-    /**
-     * Called when async processing has finished.
-     *
-     * @param value The value contained in the response.
-     */
-    @AnyThread
-    void respond(@Nullable T value);
-}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -7,22 +7,26 @@
 package org.mozilla.geckoview;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.nio.ByteBuffer;
 import java.util.AbstractSequentialList;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.UUID;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.IGeckoEditableParent;
@@ -876,33 +880,22 @@ public class GeckoSession implements Par
             }
         ) {
             @Override
             public void handleMessage(final SelectionActionDelegate delegate,
                                       final String event,
                                       final GeckoBundle message,
                                       final EventCallback callback) {
                 if ("GeckoView:ShowSelectionAction".equals(event)) {
+                    final @SelectionActionDelegateAction HashSet<String> actionsSet =
+                            new HashSet<>(Arrays.asList(message.getStringArray("actions")));
                     final SelectionActionDelegate.Selection selection =
-                            new SelectionActionDelegate.Selection(message);
-
-                    final @SelectionActionDelegateAction String[] actions = message.getStringArray("actions");
-                    final int seqNo = message.getInt("seqNo");
-                    final GeckoResponse<String> response = new GeckoResponse<String>() {
-                        @Override
-                        public void respond(final String action) {
-                            final GeckoBundle response = new GeckoBundle(2);
-                            response.putString("id", action);
-                            response.putInt("seqNo", seqNo);
-                            callback.sendSuccess(response);
-                        }
-                    };
-
-                    delegate.onShowActionRequest(GeckoSession.this, selection,
-                                                 actions, response);
+                            new SelectionActionDelegate.Selection(message, actionsSet, callback);
+
+                    delegate.onShowActionRequest(GeckoSession.this, selection);
 
                 } else if ("GeckoView:HideSelectionAction".equals(event)) {
                     final String reasonString = message.getString("reason");
                     final int reason;
                     if ("invisibleselection".equals(reasonString)) {
                         reason = SelectionActionDelegate.HIDE_REASON_INVISIBLE_SELECTION;
                     } else if ("presscaret".equals(reasonString)) {
                         reason = SelectionActionDelegate.HIDE_REASON_ACTIVE_SELECTION;
@@ -3269,17 +3262,28 @@ public class GeckoSession implements Par
 
             /**
              * The bounds of the current selection in client coordinates. Use {@link
              * GeckoSession#getClientToScreenMatrix} to perform transformation to screen
              * coordinates.
              */
             public final @Nullable RectF clientRect;
 
-            /* package */ Selection(final GeckoBundle bundle) {
+            /**
+             * Set of valid actions available through {@link Selection#execute(String)}
+             */
+            public final @NonNull @SelectionActionDelegateAction Collection<String> availableActions;
+
+            private final int mSeqNo;
+
+            private final EventCallback mEventCallback;
+
+            /* package */ Selection(final GeckoBundle bundle,
+                                    final @NonNull @SelectionActionDelegateAction Set<String> actions,
+                                    final EventCallback callback) {
                 flags = (bundle.getBoolean("collapsed") ?
                          SelectionActionDelegate.FLAG_IS_COLLAPSED : 0) |
                         (bundle.getBoolean("editable") ?
                          SelectionActionDelegate.FLAG_IS_EDITABLE : 0) |
                         (bundle.getBoolean("password") ?
                          SelectionActionDelegate.FLAG_IS_PASSWORD : 0);
                 text = bundle.getString("selection");
 
@@ -3287,55 +3291,174 @@ public class GeckoSession implements Par
                 if (rectBundle == null) {
                     clientRect = null;
                 } else {
                     clientRect = new RectF((float) rectBundle.getDouble("left"),
                                            (float) rectBundle.getDouble("top"),
                                            (float) rectBundle.getDouble("right"),
                                            (float) rectBundle.getDouble("bottom"));
                 }
+
+                availableActions = actions;
+                mSeqNo = bundle.getInt("seqNo");
+                mEventCallback = callback;
             }
 
             /**
              * Empty constructor for tests.
              */
             protected Selection() {
                 flags = 0;
                 text = "";
                 clientRect = null;
+                availableActions = new HashSet<>();
+                mSeqNo = 0;
+                mEventCallback = null;
+            }
+
+            /**
+             * Checks if the passed action is available
+             * @param action An {@link SelectionActionDelegateAction} to perform
+             * @return True if the action is available.
+             */
+            @AnyThread
+            public boolean isActionAvailable(@NonNull @SelectionActionDelegateAction final String action) {
+                return availableActions.contains(action);
+            }
+
+            /**
+             * Execute an {@link SelectionActionDelegateAction} action.
+             *
+             * @throws IllegalStateException If the action was not available.
+             * @param action A {@link SelectionActionDelegateAction} action.
+             */
+            @AnyThread
+            public void execute(@NonNull @SelectionActionDelegateAction final String action) {
+                if (!isActionAvailable(action)) {
+                    throw new IllegalStateException("Action not available");
+                }
+                final GeckoBundle response = new GeckoBundle(2);
+                response.putString("id", action);
+                response.putInt("seqNo", mSeqNo);
+                mEventCallback.sendSuccess(response);
+            }
+
+            /**
+             * Hide selection actions and cause {@link #onHideAction} to be called.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void hide() {
+                execute(ACTION_HIDE);
+            }
+
+            /**
+             * Copy onto the clipboard then delete the selected content.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void cut() {
+                execute(ACTION_CUT);
+            }
+
+            /**
+             * Copy the selected content onto the clipboard.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void copy() {
+                execute(ACTION_COPY);
+            }
+
+            /**
+             * Delete the selected content.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void delete() {
+                execute(ACTION_DELETE);
+            }
+
+            /**
+             * Replace the selected content with the clipboard content.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void paste() {
+                execute(ACTION_PASTE);
+            }
+
+            /**
+             * Select the entire content of the document or editor.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void selectAll() {
+                execute(ACTION_SELECT_ALL);
+            }
+
+            /**
+             * Clear the current selection.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void unselect() {
+                execute(ACTION_UNSELECT);
+            }
+
+            /**
+             * Collapse the current selection to its start position.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void collapseToStart() {
+                execute(ACTION_COLLAPSE_TO_START);
+            }
+
+            /**
+             * Collapse the current selection to its end position.
+             *
+             * @throws IllegalStateException If the action was not available.
+             */
+            @AnyThread
+            public void collapseToEnd() {
+                execute(ACTION_COLLAPSE_TO_END);
             }
         }
 
         /**
          * Selection actions are available. Selection actions become available when the
          * user selects some content in the document or editor. Inside an editor,
          * selection actions can also become available when the user explicitly requests
          * editor action UI, for example by tapping on the caret handle.
          *
          * In response to this callback, applications typically display a toolbar
-         * containing the selection actions. To perform a certain action, pass the Action
-         * object back through the response parameter, which may be used multiple times to
-         * perform multiple actions at once.
+         * containing the selection actions. To perform a certain action, check if the action
+         * is available with {@link Selection#isActionAvailable} then either use the relevant
+         * helper method or {@link Selection#execute}
          *
-         * Once a {@link #onHideAction} call (with particular reasons) or another {@link
-         * #onShowActionRequest} call is received, any previously received actions are no
-         * longer unavailable.
+         * Once an {@link #onHideAction} call (with particular reasons) or another {@link
+         * #onShowActionRequest} call is received, the previous Selection object is no longer
+         * usable.
          *
          * @param session The GeckoSession that initiated the callback.
-         * @param selection Current selection attributes.
-         * @param actions Array of built-in actions available; possible values
-         * come from the {@link #ACTION_HIDE ACTION_*} constants.
-         * @param response Callback object for performing built-in actions. For example,
-         * {@code response.respond(actions[0])} performs the first action. May be used
-         * multiple times to perform multiple actions at once.
+         * @param selection Current selection attributes and Callback object for performing built-in
+         *                  actions. May be used multiple times to perform multiple actions at once.
          */
         @UiThread
-        default void onShowActionRequest(@NonNull GeckoSession session, @NonNull Selection selection,
-                                         @NonNull @SelectionActionDelegateAction String[] actions,
-                                         @NonNull GeckoResponse<String> response) {}
+        default void onShowActionRequest(@NonNull GeckoSession session,
+                                         @NonNull Selection selection) {}
 
         /**
          * Actions are no longer available due to the user clearing the selection.
          */
         final int HIDE_REASON_NO_SELECTION = 0;
         /**
          * Actions are no longer available due to the user moving the selection out of view.
          * Previous actions are still available after a callback with this reason.
--- 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
@@ -19,22 +19,38 @@ exclude: true
   ([bug 1555337]({{bugzilla}}1555337))
 - ⚠️  Refactored `AutofillElement` and `AutofillSupport` into the
   [`Autofill`][72.2] API.
   ([bug 1591462]({{bugzilla}}1591462))
 - Make `read()` in the `InputStream` returned from [`WebResponse#body`][72.3] timeout according
   to [`WebResponse#setReadTimeoutMillis()`][72.4]. The default timeout value is reflected in
   [`WebResponse#DEFAULT_READ_TIMEOUT_MS`][72.5], currently 30s.
   ([bug 1595145]({{bugzilla}}1595145))
+- ⚠️  Removed `GeckoResponse`
+  ([bug 1581161]({{bugzilla}}1581161))
+- ⚠️  Removed `actions` and `response` arguments from [`SelectionActionDelegate.onShowActionRequest`][72.6]
+  and [`BasicSelectionActionDelegate.onShowActionRequest`][72.7]
+  ([bug 1581161]({{bugzilla}}1581161))
+- Added text selection action methods to [`SelectionActionDelegate.Selection`][72.8]
+  ([bug 1581161]({{bugzilla}}1581161))
+- Added [`BasicSelectionActionDelegate.getSelection`][72.9]
+  ([bug 1581161]({{bugzilla}}1581161))
+- Changed [`BasicSelectionActionDelegate.clearSelection`][72.10] to public.
+  ([bug 1581161]({{bugzilla}}1581161))
 
 [72.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
 [72.2]: {{javadoc_uri}}/Autofill.html
 [72.3]: {{javadoc_uri}}/WebResponse.html#body
 [72.4]: {{javadoc_uri}}/WebResponse.html#setReadTimeoutMillis-long-
 [72.5]: {{javadoc_uri}}/WebResponse.html#DEFAULT_READ_TIMEOUT_MS
+[72.6]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
+[72.7]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#onShowActionRequest-org.mozilla.geckoview.GeckoSession-org.mozilla.geckoview.GeckoSession.SelectionActionDelegate.Selection-
+[72.8]: {{javadoc_uri}}/GeckoSession.SelectionActionDelegate.Selection.html
+[72.9]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#getSelection-
+[72.10]: {{javadoc_uri}}/BasicSelectionActionDelegate.html#clearSelection-
 
 ## v71
 - Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
   ([bug 1584479]({{bugzilla}}1584479))
 - Added [`onBooleanScalar`][71.1], [`onLongScalar`][71.2],
   [`onStringScalar`][71.3] to [`RuntimeTelemetry.Delegate`][70.12] to support
   scalars in streaming telemetry. ⚠️  As part of this change,
   `onTelemetryReceived` has been renamed to [`onHistogram`][71.4], and
@@ -422,9 +438,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER    
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: 5fce802ebb83bfd1237dd1ad541dceccb3801d9d
+[api-version]: 8d6a09b6a33550dffb6303dc01c5e6ff2d3cc499