Bug 1247689 - Pre: Move bookmark snackbars / helper UIs into BookmarkStateChangeDelegate r?sebastian draft
authorAndrzej Hunt <ahunt@mozilla.com>
Fri, 29 Apr 2016 12:28:02 +0200
changeset 357786 c9f3ba7a50d53e41e25159eb1620c99565af3a63
parent 357275 03eac758d8d7b0ce6b284bc1cf17512fe7a1ca2e
child 357787 c4af4f89f1a936ffd172139018e3243c25f30084
push id16844
push userahunt@mozilla.com
push dateFri, 29 Apr 2016 10:33:33 +0000
reviewerssebastian
bugs1247689
milestone49.0a1
Bug 1247689 - Pre: Move bookmark snackbars / helper UIs into BookmarkStateChangeDelegate r?sebastian MozReview-Commit-ID: BRGMN0vE8nf
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/BrowserAppDelegate.java
mobile/android/base/java/org/mozilla/gecko/promotion/BookmarkStateChangeDelegate.java
mobile/android/base/moz.build
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -6,17 +6,16 @@
 package org.mozilla.gecko;
 
 import android.Manifest;
 import android.app.DownloadManager;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.os.Environment;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import org.json.JSONArray;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
@@ -54,17 +53,17 @@ import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
-import org.mozilla.gecko.promotion.SimpleHelperUI;
+import org.mozilla.gecko.promotion.BookmarkStateChangeDelegate;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
 import org.mozilla.gecko.restrictions.Restrictions;
@@ -127,17 +126,16 @@ import android.nfc.NfcAdapter;
 import android.nfc.NfcEvent;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.StrictMode;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.ContextCompat;
 import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Base64;
 import android.util.Base64OutputStream;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
@@ -209,19 +207,19 @@ public class BrowserApp extends GeckoApp
 
     private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
 
     private static final String BROWSER_SEARCH_TAG = "browser_search";
 
     // Request ID for startActivityForResult.
     private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
     private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
-    private static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
-    private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
-    private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
+    public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
+    public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
+    public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
 
     public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
 
     @RobocopTarget
     public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
     private static final String EOL_NOTIFIED = "eol_notified";
 
     private BrowserSearch mBrowserSearch;
@@ -302,17 +300,18 @@ public class BrowserApp extends GeckoApp
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
     // race by determining if the web content should be hidden at the animation's end.
     private boolean mHideWebContentOnAnimationEnd;
 
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             (BrowserAppDelegate) new AddToHomeScreenPromotion(),
-            (BrowserAppDelegate) new ScreenshotDelegate()
+            (BrowserAppDelegate) new ScreenshotDelegate(),
+            (BrowserAppDelegate) new BookmarkStateChangeDelegate()
     ));
 
     @NonNull
     private SearchEngineManager searchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
     @Override
     public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
         final View view;
@@ -368,49 +367,16 @@ public class BrowserApp extends GeckoApp
             case MENU_UPDATED:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     invalidateOptionsMenu();
                 }
                 break;
             case PAGE_SHOW:
                 tab.loadFavicon();
                 break;
-            case BOOKMARK_ADDED:
-                // We always show the special offline snackbar whenever we bookmark a reader page.
-                // It's possible that the page is already stored offline, however this is highly
-                // unlikely, and even so it is probably nicer to show the same offline notification
-                // every time we bookmark an about:reader page.
-                if (!AboutPages.isAboutReader(tab.getURL())) {
-                    showBookmarkAddedSnackbar();
-                } else {
-                    final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
-
-                    final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
-
-                    if (hasFirstReaderViewPromptBeenShownBefore) {
-                        showReaderModeBookmarkAddedSnackbar();
-                    } else {
-                        SimpleHelperUI.show(this,
-                                SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
-                                ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
-                                R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
-                                R.drawable.helper_first_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
-                                ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
-                                ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
-
-                        GeckoSharedPrefs.forProfile(this)
-                                .edit()
-                                .putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
-                                .apply();
-                    }
-                }
-                break;
-            case BOOKMARK_REMOVED:
-                showBookmarkRemovedSnackbar();
-                break;
 
             case UNSELECTED:
                 // We receive UNSELECTED immediately after the SELECTED listeners run
                 // so we are ensured that the unselectedTabEditingText has not changed.
                 if (tab.isEditing()) {
                     // Copy to avoid constructing new objects.
                     tab.getEditingState().copyFrom(mLastTabEditingState);
                 }
@@ -456,59 +422,16 @@ public class BrowserApp extends GeckoApp
         // must be called after we restore the editing state in the edit text View.
         if (editingState.isBrowserSearchShown()) {
             showBrowserSearch();
         } else {
             hideBrowserSearch();
         }
     }
 
-    private void showBookmarkAddedSnackbar() {
-        // This flow is from the option menu which has check to see if a bookmark was already added.
-        // So, it is safe here to show the snackbar that bookmark_added without any checks.
-
-        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
-            @Override
-            public void onClick(View v) {
-                Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
-                showBookmarkDialog();
-            }
-        };
-
-        SnackbarHelper.showSnackbarWithAction(this,
-                getResources().getString(R.string.bookmark_added),
-                Snackbar.LENGTH_LONG,
-                getResources().getString(R.string.bookmark_options),
-                callback);
-    }
-
-    private void showReaderModeBookmarkAddedSnackbar() {
-        final Drawable iconDownloaded = DrawableUtil.tintDrawable(getContext(), R.drawable.status_icon_readercache, Color.WHITE);
-
-        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
-            @Override
-            public void onClick(View v) {
-                openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
-            }
-        };
-
-        SnackbarHelper.showSnackbarWithActionAndColors(this,
-                getResources().getString(R.string.reader_saved_offline),
-                Snackbar.LENGTH_LONG,
-                getResources().getString(R.string.reader_switch_to_bookmarks),
-                callback,
-                iconDownloaded,
-                ContextCompat.getColor(this, R.color.link_blue),
-                Color.WHITE);
-    }
-
-    private void showBookmarkRemovedSnackbar() {
-        SnackbarHelper.showSnackbar(this, getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
-    }
-
     @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
         if (AndroidGamepadManager.handleKeyEvent(event)) {
             return true;
         }
 
         // Global onKey handler. This is called if the focused UI doesn't
         // handle the key event, and before Gecko swallows the events.
@@ -1076,17 +999,17 @@ public class BrowserApp extends GeckoApp
         final boolean enableGuestSession = GuestSession.shouldUse(this, args);
         final boolean inGuestSession = GeckoProfile.get(this).inGuestMode();
         if (enableGuestSession != inGuestSession) {
             doRestart(getIntent());
             return;
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
-            "Prompt:ShowTop");
+                "Prompt:ShowTop");
 
         processTabQueue();
 
         for (BrowserAppDelegate delegate : delegates) {
             delegate.onResume(this);
         }
     }
 
@@ -1255,67 +1178,16 @@ public class BrowserApp extends GeckoApp
                 }
             }
         });
 
         // Intercept key events for gamepad shortcuts
         mBrowserToolbar.setOnKeyListener(this);
     }
 
-    private void showBookmarkDialog() {
-        final Resources res = getResources();
-        final Tab tab = Tabs.getInstance().getSelectedTab();
-
-        final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
-            @Override
-            public void onPromptFinished(String result) {
-                int itemId = -1;
-                try {
-                  itemId = new JSONObject(result).getInt("button");
-                } catch (JSONException ex) {
-                    Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
-                }
-
-                if (tab == null) {
-                    return;
-                }
-
-                if (itemId == 0) {
-                    final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
-                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
-                        TelemetryContract.Method.DIALOG, extrasId);
-
-                    new EditBookmarkDialog(BrowserApp.this).show(tab.getURL());
-                } else if (itemId == 1) {
-                    final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
-                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
-                        TelemetryContract.Method.DIALOG, extrasId);
-
-                    final String url = tab.getURL();
-                    final String title = tab.getDisplayTitle();
-
-                    if (url != null && title != null) {
-                        ThreadUtils.postToBackgroundThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                GeckoAppShell.createShortcut(title, url);
-                            }
-                        });
-                    }
-                }
-            }
-        });
-
-        final PromptListItem[] items = new PromptListItem[2];
-        items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
-        items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
-
-        ps.show("", "", items, ListView.CHOICE_MODE_NONE);
-    }
-
     private void setDynamicToolbarEnabled(boolean enabled) {
         ThreadUtils.assertOnUiThread();
 
         if (enabled) {
             if (mLayerView != null) {
                 mLayerView.getDynamicToolbarAnimator().addTranslationListener(this);
             }
             setToolbarMargin(0);
@@ -2331,17 +2203,17 @@ public class BrowserApp extends GeckoApp
         mTargetTabForEditingMode = null;
         tabs.selectTab(tab.getId());
 
         mBrowserToolbar.cancelEdit();
 
         return true;
     }
 
-    private void openUrlAndStopEditing(String url) {
+    public void openUrlAndStopEditing(String url) {
         openUrlAndStopEditing(url, null, false);
     }
 
     private void openUrlAndStopEditing(String url, boolean newTab) {
         openUrlAndStopEditing(url, null, newTab);
     }
 
     private void openUrlAndStopEditing(String url, String searchEngine) {
@@ -2683,25 +2555,21 @@ public class BrowserApp extends GeckoApp
                     }
                 });
                 break;
 
             case ACTIVITY_REQUEST_TAB_QUEUE:
                 TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
                 break;
 
-            case ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK:
-                if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
-                    openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
-                } else if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
-                    showReaderModeBookmarkAddedSnackbar();
+            default:
+                for (final BrowserAppDelegate delegate : delegates) {
+                    delegate.onActivityResult(this, requestCode, resultCode, data);
                 }
-                break;
-
-            default:
+
                 super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
     private void showFirstrunPager() {
         if (mFirstrunAnimationContainer == null) {
             final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
             mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserAppDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserAppDelegate.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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.gecko;
 
+import android.content.Intent;
 import android.os.Bundle;
 
 import org.mozilla.gecko.tabs.TabsPanel;
 
 /**
  * Abstract class for extending the behavior of BrowserApp without adding additional code to the
  * already huge class.
  */
@@ -53,9 +54,18 @@ public abstract class BrowserAppDelegate
      * Called when the tabs tray is opened.
      */
     public void onTabsTrayShown(BrowserApp browserApp, TabsPanel tabsPanel) {}
 
     /**
      * Called when the tabs tray is closed.
      */
     public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {}
+
+    /**
+     * Called when an activity started using startActivityForResult() returns.
+     *
+     * Delegates should only use request and result codes declared in BrowserApp itself (as opposed
+     * to declarations in the delegate), in order to avoid conflicts.
+     */
+    public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {}
 }
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/BookmarkStateChangeDelegate.java
@@ -0,0 +1,235 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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.gecko.promotion;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.BrowserAppDelegate;
+import org.mozilla.gecko.EditBookmarkDialog;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.home.HomeConfig;
+import org.mozilla.gecko.prompts.Prompt;
+import org.mozilla.gecko.prompts.PromptListItem;
+import org.mozilla.gecko.util.DrawableUtil;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.lang.ref.WeakReference;
+
+public class BookmarkStateChangeDelegate extends BrowserAppDelegate implements Tabs.OnTabsChangedListener {
+    private static final String LOGTAG = "BookmarkDelegate";
+
+    private WeakReference<BrowserApp> mBrowserApp;
+
+    @Override
+    public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
+        mBrowserApp =  new WeakReference<>(browserApp);
+    }
+
+    @Override
+    public void onResume(BrowserApp browserApp) {
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
+    @Override
+    public void onPause(BrowserApp browserApp) {
+        Tabs.unregisterOnTabsChangedListener(this);
+    }
+
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+        switch (msg) {
+            case BOOKMARK_ADDED:
+                // We always show the special offline snackbar whenever we bookmark a reader page.
+                // It's possible that the page is already stored offline, however this is highly
+                // unlikely, and even so it is probably nicer to show the same offline notification
+                // every time we bookmark an about:reader page.
+                if (!AboutPages.isAboutReader(tab.getURL())) {
+                    showBookmarkAddedSnackbar();
+                } else {
+                    if (!promoteReaderViewBookmarkAdded()) {
+                        showReaderModeBookmarkAddedSnackbar();
+                    }
+                }
+                break;
+
+            case BOOKMARK_REMOVED:
+                showBookmarkRemovedSnackbar();
+                break;
+        }
+    }
+
+    @Override
+    public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK:
+                if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
+                    browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
+                } else if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
+                    showReaderModeBookmarkAddedSnackbar();
+                }
+                break;
+        }
+    }
+
+    private boolean promoteReaderViewBookmarkAdded() {
+        final BrowserApp browserApp = mBrowserApp.get();
+        if (browserApp == null) {
+            return false;
+        }
+
+        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp.getContext());
+
+        final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
+
+        if (hasFirstReaderViewPromptBeenShownBefore) {
+            return false;
+        }
+
+        SimpleHelperUI.show(browserApp,
+                SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
+                BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
+                R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
+                R.drawable.helper_first_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
+                BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
+                BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
+
+        GeckoSharedPrefs.forProfile(browserApp.getContext())
+                .edit()
+                .putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
+                .apply();
+
+        return true;
+    }
+
+    private void showBookmarkAddedSnackbar() {
+        final BrowserApp browserApp = mBrowserApp.get();
+        if (browserApp == null) {
+            return;
+        }
+
+        // This flow is from the option menu which has check to see if a bookmark was already added.
+        // So, it is safe here to show the snackbar that bookmark_added without any checks.
+        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
+            @Override
+            public void onClick(View v) {
+                Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
+                showBookmarkDialog(browserApp);
+            }
+        };
+
+        SnackbarHelper.showSnackbarWithAction(browserApp,
+                browserApp.getResources().getString(R.string.bookmark_added),
+                Snackbar.LENGTH_LONG,
+                browserApp.getResources().getString(R.string.bookmark_options),
+                callback);
+    }
+
+    private void showBookmarkRemovedSnackbar() {
+        final BrowserApp browserApp = mBrowserApp.get();
+        if (browserApp == null) {
+            return;
+        }
+
+        SnackbarHelper.showSnackbar(browserApp, browserApp.getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
+    }
+
+    private static void showBookmarkDialog(final BrowserApp browserApp) {
+        final Resources res = browserApp.getResources();
+        final Tab tab = Tabs.getInstance().getSelectedTab();
+
+        final Prompt ps = new Prompt(browserApp, new Prompt.PromptCallback() {
+            @Override
+            public void onPromptFinished(String result) {
+                int itemId = -1;
+                try {
+                    itemId = new JSONObject(result).getInt("button");
+                } catch (JSONException ex) {
+                    Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
+                }
+
+                if (tab == null) {
+                    return;
+                }
+
+                if (itemId == 0) {
+                    final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
+                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
+                            TelemetryContract.Method.DIALOG, extrasId);
+
+                    new EditBookmarkDialog(browserApp).show(tab.getURL());
+                } else if (itemId == 1) {
+                    final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
+                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
+                            TelemetryContract.Method.DIALOG, extrasId);
+
+                    final String url = tab.getURL();
+                    final String title = tab.getDisplayTitle();
+
+                    if (url != null && title != null) {
+                        ThreadUtils.postToBackgroundThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                GeckoAppShell.createShortcut(title, url);
+                            }
+                        });
+                    }
+                }
+            }
+        });
+
+        final PromptListItem[] items = new PromptListItem[2];
+        items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
+        items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
+
+        ps.show("", "", items, ListView.CHOICE_MODE_NONE);
+    }
+
+    private void showReaderModeBookmarkAddedSnackbar() {
+        final BrowserApp browserApp = mBrowserApp.get();
+        if (browserApp == null) {
+            return;
+        }
+
+        final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE);
+
+        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
+            @Override
+            public void onClick(View v) {
+                browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
+            }
+        };
+
+        SnackbarHelper.showSnackbarWithActionAndColors(browserApp,
+                browserApp.getResources().getString(R.string.reader_saved_offline),
+                Snackbar.LENGTH_LONG,
+                browserApp.getResources().getString(R.string.reader_switch_to_bookmarks),
+                callback,
+                iconDownloaded,
+                ContextCompat.getColor(browserApp, R.color.link_blue),
+                Color.WHITE);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -503,16 +503,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SetHomepagePreference.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrintHelper.java',
     'PrivateTab.java',
     'promotion/AddToHomeScreenPromotion.java',
+    'promotion/BookmarkStateChangeDelegate.java',
     'promotion/HomeScreenPrompt.java',
     'promotion/SimpleHelperUI.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
     'prompts/IntentChooserPrompt.java',
     'prompts/IntentHandler.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',