Bug 1509110 - Add HistoryItem and HistoryList classes, HistoryDelegate.onHistoryStateChange callback, and GeckoSession.gotoHistoryIndex. r=esawin,geckoview-reviewers,snorp
☠☠ backed out by 55c2f04c1751 ☠ ☠
authorDylan Roeh <droeh@mozilla.com>
Thu, 11 Apr 2019 16:46:49 +0000
changeset 469030 9ed5bb5e0cfcca516302496a3b29e69aac6b82c9
parent 469029 9a8e3ac367f880c2383561a89944bc6ddeb10477
child 469031 1bc29703a90ff8dc7d480cbae976cb02591be4a8
push id35856
push usercsabou@mozilla.com
push dateFri, 12 Apr 2019 03:19:48 +0000
treeherdermozilla-central@940684cd1065 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersesawin, geckoview-reviewers, snorp
bugs1509110
milestone68.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 1509110 - Add HistoryItem and HistoryList classes, HistoryDelegate.onHistoryStateChange callback, and GeckoSession.gotoHistoryIndex. r=esawin,geckoview-reviewers,snorp Differential Revision: https://phabricator.services.mozilla.com/D24439
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/modules/geckoview/GeckoViewNavigation.jsm
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -48,16 +48,17 @@ import java.lang.Object;
 import java.lang.Runnable;
 import java.lang.RuntimeException;
 import java.lang.String;
 import java.lang.Throwable;
 import java.lang.Void;
 import java.lang.annotation.Annotation;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.util.AbstractSequentialList;
 import java.util.List;
 import java.util.Map;
 import org.json.JSONObject;
 import org.mozilla.geckoview.AllowOrDeny;
 import org.mozilla.geckoview.CompositorController;
 import org.mozilla.geckoview.ContentBlocking;
 import org.mozilla.geckoview.DynamicToolbarAnimator;
 import org.mozilla.geckoview.GeckoDisplay;
@@ -388,16 +389,17 @@ package org.mozilla.geckoview {
     method @UiThread @Nullable public GeckoSession.ScrollDelegate getScrollDelegate();
     method @AnyThread @Nullable public GeckoSession.SelectionActionDelegate getSelectionActionDelegate();
     method @AnyThread @NonNull public GeckoSessionSettings getSettings();
     method @UiThread public void getSurfaceBounds(@NonNull Rect);
     method @AnyThread @NonNull public SessionTextInput getTextInput();
     method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
     method @AnyThread public void goBack();
     method @AnyThread public void goForward();
+    method @AnyThread public void gotoHistoryIndex(int);
     method @AnyThread public boolean isOpen();
     method @AnyThread public void loadData(@NonNull byte[], @Nullable String);
     method @AnyThread public void loadString(@NonNull String, @Nullable String);
     method @AnyThread public void loadUri(@NonNull String);
     method @AnyThread public void loadUri(@NonNull String, int);
     method @AnyThread public void loadUri(@NonNull String, @Nullable String, int);
     method @AnyThread public void loadUri(@NonNull Uri);
     method @AnyThread public void loadUri(@NonNull Uri, int);
@@ -473,25 +475,35 @@ package org.mozilla.geckoview {
     field @Nullable public final String linkUri;
     field @NonNull public final String searchString;
     field public final int total;
     field public final boolean wrapped;
   }
 
   public static interface GeckoSession.HistoryDelegate {
     method @UiThread @Nullable default public GeckoResult<boolean[]> getVisited(@NonNull GeckoSession, @NonNull String[]);
+    method @UiThread default public void onHistoryStateChange(@NonNull GeckoSession, @NonNull GeckoSession.HistoryDelegate.HistoryList);
     method @UiThread @Nullable default public GeckoResult<Boolean> onVisited(@NonNull GeckoSession, @NonNull String, @Nullable String, int);
     field public static final int VISIT_REDIRECT_PERMANENT = 4;
     field public static final int VISIT_REDIRECT_SOURCE = 8;
     field public static final int VISIT_REDIRECT_SOURCE_PERMANENT = 16;
     field public static final int VISIT_REDIRECT_TEMPORARY = 2;
     field public static final int VISIT_TOP_LEVEL = 1;
     field public static final int VISIT_UNRECOVERABLE_ERROR = 32;
   }
 
+  public static interface GeckoSession.HistoryDelegate.HistoryItem {
+    method @AnyThread @NonNull default public String getTitle();
+    method @AnyThread @NonNull default public String getUri();
+  }
+
+  public static interface GeckoSession.HistoryDelegate.HistoryList implements List {
+    method @AnyThread default public int getCurrentIndex();
+  }
+
   public static interface GeckoSession.HistoryDelegate.VisitFlags implements Annotation {
   }
 
   public static interface GeckoSession.MediaDelegate {
     method @UiThread default public void onMediaAdd(@NonNull GeckoSession, @NonNull MediaElement);
     method @UiThread default public void onMediaRemove(@NonNull GeckoSession, @NonNull MediaElement);
   }
 
@@ -717,17 +729,17 @@ package org.mozilla.geckoview {
 
   public static class GeckoSession.SelectionActionDelegate.Selection {
     ctor protected Selection();
     field @Nullable public final RectF clientRect;
     field public final int flags;
     field @NonNull public final String text;
   }
 
-  @AnyThread public static class GeckoSession.SessionState implements Parcelable {
+  @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);
     method public void readFromParcel(@NonNull Parcel);
     field public static final Parcelable.Creator<GeckoSession.SessionState> CREATOR;
   }
 
   public static interface GeckoSession.TextInputDelegate {
     method @UiThread default public void hideSoftInput(@NonNull GeckoSession);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -4,16 +4,21 @@
  * 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 java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.AbstractSequentialList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
 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.GeckoAppShell;
 import org.mozilla.gecko.GeckoThread;
@@ -597,17 +602,21 @@ public class GeckoSession implements Par
                                               message.getInt("progress"));
                 } else if ("GeckoView:SecurityChanged".equals(event)) {
                     final GeckoBundle identity = message.getBundle("identity");
                     delegate.onSecurityChange(GeckoSession.this, new ProgressDelegate.SecurityInformation(identity));
                 } else if ("GeckoView:StateUpdated".equals(event)) {
                     final GeckoBundle update = message.getBundle("data");
                     if (update != null) {
                         mStateCache.updateSessionState(update);
-                        delegate.onSessionStateChange(GeckoSession.this, new SessionState(mStateCache));
+                        final SessionState state = new SessionState(mStateCache);
+                        delegate.onSessionStateChange(GeckoSession.this, state);
+                        if (update.getBundle("historychange") != null) {
+                            getHistoryDelegate().onHistoryStateChange(GeckoSession.this, state);
+                        }
                     }
                 }
             }
         };
 
     private final GeckoSessionHandler<ScrollDelegate> mScrollHandler =
         new GeckoSessionHandler<ScrollDelegate>(
             "GeckoViewScroll", this,
@@ -1592,16 +1601,31 @@ public class GeckoSession implements Par
     /**
     * Go forward in history.
     */
     @AnyThread
     public void goForward() {
         mEventDispatcher.dispatch("GeckoView:GoForward", null);
     }
 
+    /**
+     * Navigate to an index in browser history; the index of the currently
+     * viewed page can be retrieved from an up-to-date HistoryList by
+     * calling {@link HistoryList#getCurrentIndex()}.
+     *
+     * @param index The index of the location in browser history you want
+     *              to navigate to.
+     */
+    @AnyThread
+    public void gotoHistoryIndex(final int index) {
+        final GeckoBundle msg = new GeckoBundle(1);
+        msg.putInt("index", index);
+        mEventDispatcher.dispatch("GeckoView:GotoHistoryIndex", msg);
+    }
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true,
             value = {FINDER_FIND_BACKWARDS, FINDER_FIND_LINKS_ONLY,
                     FINDER_FIND_MATCH_CASE, FINDER_FIND_WHOLE_WORD})
     /* package */ @interface FinderFindFlags {}
 
     /** Go backwards when finding the next match. */
     public static final int FINDER_FIND_BACKWARDS = 1;
@@ -1728,19 +1752,120 @@ public class GeckoSession implements Par
         msg.putBoolean("focused", focused);
         mEventDispatcher.dispatch("GeckoView:SetFocused", msg);
     }
 
     /**
      * Class representing a saved session state.
      */
     @AnyThread
-    public static class SessionState implements Parcelable {
+    public static class SessionState extends AbstractSequentialList<HistoryDelegate.HistoryItem>
+                                     implements HistoryDelegate.HistoryList, Parcelable {
         private GeckoBundle mState;
 
+        private class SessionStateItem implements HistoryDelegate.HistoryItem {
+            private final GeckoBundle mItem;
+
+            private SessionStateItem(final @NonNull GeckoBundle item) {
+                mItem = item;
+            }
+
+            @Override /* HistoryItem */
+            public String getUri() {
+                return mItem.getString("url");
+            }
+
+            @Override /* HistoryItem */
+            public String getTitle() {
+                return mItem.getString("title");
+            }
+        }
+
+        private class SessionStateIterator implements ListIterator<HistoryDelegate.HistoryItem> {
+            private final SessionState mState;
+            private int mIndex;
+
+            private SessionStateIterator(final @NonNull SessionState state) {
+                this(state, 0);
+            }
+
+            private SessionStateIterator(final @NonNull SessionState state, final int index) {
+                mIndex = index;
+                mState = state;
+            }
+
+            @Override /* ListIterator */
+            public void add(final HistoryDelegate.HistoryItem item) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override /* ListIterator */
+            public boolean hasNext() {
+                final GeckoBundle[] entries = mState.getHistoryEntries();
+
+                if (entries == null) {
+                    Log.w(LOGTAG, "No history entries found.");
+                    return false;
+                }
+
+                if (mIndex >= mState.getHistoryEntries().length) {
+                    return false;
+                }
+                return true;
+            }
+
+            @Override /* ListIterator */
+            public boolean hasPrevious() {
+                if (mIndex <= 0) {
+                    return false;
+                }
+                return true;
+            }
+
+            @Override /* ListIterator */
+            public HistoryDelegate.HistoryItem next() {
+                if (hasNext()) {
+                    mIndex++;
+                    return new SessionStateItem(mState.getHistoryEntries()[mIndex - 1]);
+                } else {
+                    throw new NoSuchElementException();
+                }
+            }
+
+            @Override /* ListIterator */
+            public int nextIndex() {
+                return mIndex;
+            }
+
+            @Override /* ListIterator */
+            public HistoryDelegate.HistoryItem previous() {
+                if (hasPrevious()) {
+                    mIndex--;
+                    return new SessionStateItem(mState.getHistoryEntries()[mIndex]);
+                } else {
+                    throw new NoSuchElementException();
+                }
+            }
+
+            @Override /* ListIterator */
+            public int previousIndex() {
+                return mIndex - 1;
+            }
+
+            @Override /* ListIterator */
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override /* ListIterator */
+            public void set(final @NonNull HistoryDelegate.HistoryItem item) {
+                throw new UnsupportedOperationException();
+            }
+        }
+
         private SessionState() {
             mState = new GeckoBundle(3);
         }
 
         private SessionState(final @NonNull GeckoBundle state) {
             mState = new GeckoBundle(state);
         }
 
@@ -1862,16 +1987,79 @@ public class GeckoSession implements Par
                 return new SessionState(res);
             }
 
             @Override
             public SessionState[] newArray(final int size) {
                 return new SessionState[size];
             }
         };
+
+        @Override /* AbstractSequentialList */
+        public @NonNull HistoryDelegate.HistoryItem get(final int index) {
+            final GeckoBundle[] entries = getHistoryEntries();
+
+            if (entries == null || index < 0 || index >= entries.length) {
+                throw new NoSuchElementException();
+            }
+
+            return new SessionStateItem(entries[index]);
+        }
+
+        @Override /* AbstractSequentialList */
+        public @NonNull Iterator<HistoryDelegate.HistoryItem> iterator() {
+            return listIterator(0);
+        }
+
+        @Override /* AbstractSequentialList */
+        public @NonNull ListIterator<HistoryDelegate.HistoryItem> listIterator(final int index) {
+            return new SessionStateIterator(this, index);
+        }
+
+        @Override /* AbstractSequentialList */
+        public int size() {
+            final GeckoBundle[] entries = getHistoryEntries();
+
+            if (entries == null) {
+                Log.w(LOGTAG, "No history entries found.");
+                return 0;
+            }
+
+            return entries.length;
+        }
+
+        @Override /* HistoryList */
+        public int getCurrentIndex() {
+            final GeckoBundle history = getHistory();
+
+            if (history == null) {
+                throw new IllegalStateException("No history state exists.");
+            }
+
+            return history.getInt("index") + history.getInt("fromIdx");
+        }
+
+        // Some helpers for common code.
+        private GeckoBundle getHistory() {
+            if (mState == null) {
+                return null;
+            }
+
+            return mState.getBundle("history");
+        }
+
+        private GeckoBundle[] getHistoryEntries() {
+            final GeckoBundle history = getHistory();
+
+            if (history == null) {
+                return null;
+            }
+
+            return history.getBundleArray("entries");
+        }
     }
 
     private SessionState mStateCache = new SessionState();
 
     /**
      * Restore a saved state to this GeckoSession; only data that is saved (history, scroll
      * position, zoom, and form data) will be restored. These will overwrite the corresponding
      * state of this GeckoSession.
@@ -4650,16 +4838,58 @@ public class GeckoSession implements Par
         default void onMediaRemove(@NonNull GeckoSession session, @NonNull MediaElement element) {}
     }
 
     /**
      * An interface for recording new history visits and fetching the visited
      * status for links.
      */
     public interface HistoryDelegate {
+        /**
+         * A representation of an entry in browser history.
+         */
+        public interface HistoryItem {
+            /**
+             * Get the URI of this history element.
+             *
+             * @return A String representing the URI of this history element.
+             */
+            @AnyThread
+            default @NonNull String getUri() {
+                throw new UnsupportedOperationException("HistoryItem.getUri() called on invalid object.");
+            }
+
+            /**
+             * Get the title of this history element.
+             *
+             * @return A String representing the title of this history element.
+             */
+            @AnyThread
+            default @NonNull String getTitle() {
+                throw new UnsupportedOperationException("HistoryItem.getString() called on invalid object.");
+            }
+        }
+
+        /**
+         * A representation of browser history, accessible as a `List`. The list itself
+         * and its entries are immutable; any attempt to mutate will result in an
+         * `UnsupportedOperationException`.
+         */
+        public interface HistoryList extends List<HistoryItem> {
+            /**
+             * Get the current index in browser history.
+             *
+             * @return An int representing the current index in browser history.
+             */
+            @AnyThread
+            default int getCurrentIndex() {
+                throw new UnsupportedOperationException("HistoryList.getCurrentIndex() called on invalid object.");
+            }
+        }
+
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(flag = true,
                 value = { VISIT_TOP_LEVEL,
                           VISIT_REDIRECT_TEMPORARY, VISIT_REDIRECT_PERMANENT,
                           VISIT_REDIRECT_SOURCE, VISIT_REDIRECT_SOURCE_PERMANENT,
                           VISIT_UNRECOVERABLE_ERROR })
         /* package */ @interface VisitFlags {}
 
@@ -4713,10 +4943,13 @@ public class GeckoSession implements Par
          *         whether to highlight links for each URL as visited
          *         ({@code true}) or unvisited ({@code false}).
          */
         @UiThread
         default @Nullable GeckoResult<boolean[]> getVisited(@NonNull GeckoSession session,
                                                             @NonNull String[] urls) {
             return null;
         }
+
+        @UiThread
+        default void onHistoryStateChange(@NonNull GeckoSession session, @NonNull HistoryList historyList) {}
     }
 }
--- 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
@@ -45,16 +45,22 @@ exclude: true
 - Added [`@NonNull`][66.1] or [`@Nullable`][66.2] to all fields.
 
 - [`RuntimeTelemetry#getSnapshots`][68.10] returns a [`JSONObject`][67.22] now.
 
 [68.10]: ../RuntimeTelemetry.html#getSnapshots-boolean-
 
 - Removed all `org.mozilla.gecko` references in the API.
 
+- Added `HistoryItem` and `HistoryList` interfaces and `onHistoryStateChange` to 
+  [`HistoryDelegate`][68.11] and added `gotoHistoryIndex` to [`GeckoSession`][68.12].
+
+[68.11]: ../GeckoSession.HistoryDelegate.html
+[68.12]: ../GeckoSession.html
+
 ## v67
 - Added [`setAutomaticFontSizeAdjustment`][67.2] to
   [`GeckoRuntimeSettings`][67.3] for automatically adjusting font size settings
   depending on the OS-level font size setting.
 
 [67.2]: ../GeckoRuntimeSettings.html#setAutomaticFontSizeAdjustment-boolean-
 [67.3]: ../GeckoRuntimeSettings.html
 
@@ -251,9 +257,9 @@ exclude: true
 [65.23]: ../GeckoSession.FinderResult.html
 
 - Update [`CrashReporter#sendCrashReport`][65.24] to return the crash ID as a
   [`GeckoResult<String>`][65.25].
 
 [65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: ../GeckoResult.html
 
-[api-version]: 6a931a2d4f327a5b4be2ff75c83c5a53eb610854
+[api-version]: 437e1c98a06571fd92c242e027494ec6947a4a9b
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -462,16 +462,22 @@ public class GeckoViewActivity extends A
         @Override
         public GeckoResult<boolean[]> getVisited(GeckoSession session, String[] urls) {
             boolean[] visited = new boolean[urls.length];
             for (int i = 0; i < urls.length; i++) {
                 visited[i] = mVisitedURLs.contains(urls[i]);
             }
             return GeckoResult.fromValue(visited);
         }
+
+        @Override
+        public void onHistoryStateChange(final GeckoSession session,
+                                         final GeckoSession.HistoryDelegate.HistoryList state) {
+            Log.i(LOGTAG, "History state updated");
+        }
     }
 
     private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
         @Override
         public void onTitleChange(GeckoSession session, String title) {
             Log.i(LOGTAG, "Content title changed to " + title);
         }
 
--- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
@@ -30,16 +30,17 @@ class GeckoViewNavigation extends GeckoV
     // to do that now.
     Services.obs.notifyObservers(this.window, "geckoview-window-created");
   }
 
   onInit() {
     this.registerListener([
       "GeckoView:GoBack",
       "GeckoView:GoForward",
+      "GeckoView:GotoHistoryIndex",
       "GeckoView:LoadUri",
       "GeckoView:Reload",
       "GeckoView:Stop",
     ]);
 
     this.messageManager.addMessageListener("Browser:LoadURI", this);
   }
 
@@ -49,16 +50,19 @@ class GeckoViewNavigation extends GeckoV
 
     switch (aEvent) {
       case "GeckoView:GoBack":
         this.browser.goBack();
         break;
       case "GeckoView:GoForward":
         this.browser.goForward();
         break;
+      case "GeckoView:GotoHistoryIndex":
+        this.browser.gotoIndex(aData.index);
+        break;
       case "GeckoView:LoadUri":
         const { uri, referrer, flags } = aData;
 
         let navFlags = 0;
 
         // These need to match the values in GeckoSession.LOAD_FLAGS_*
         if (flags & (1 << 0)) {
           navFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;