Bug 1264347 - Refactor SnackbarHelper to use builder pattern. r?sebastian draft
authorTom Klein <twointofive@gmail.com>
Mon, 11 Jul 2016 09:42:01 -0500
changeset 387280 b26c27c75dab19e020c75cc585e0be22e3096dde
parent 386689 fcfa26398425779f0dc87fc4658f2ebcc6f6f8cb
child 525307 e86ada78ef6c8d81749c88e0e715346ed25a6ba9
push id22915
push userbmo:twointofive@gmail.com
push dateWed, 13 Jul 2016 15:53:46 +0000
reviewerssebastian
bugs1264347
milestone50.0a1
Bug 1264347 - Refactor SnackbarHelper to use builder pattern. r?sebastian MozReview-Commit-ID: 9WZBhhJIdNq
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
mobile/android/base/java/org/mozilla/gecko/SnackbarHelper.java
mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.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
@@ -803,30 +803,31 @@ public class BrowserApp extends GeckoApp
         // those before upload can occur in Adjust.onResume.
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
         final boolean enabled = !isInAutomation &&
                 prefs.getBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
         adjustHelper.setEnabled(enabled);
     }
 
     private void showUpdaterPermissionSnackbar() {
-        SnackbarHelper.SnackbarCallback allowCallback = new SnackbarHelper.SnackbarCallback() {
+        SnackbarBuilder.SnackbarCallback allowCallback = new SnackbarBuilder.SnackbarCallback() {
             @Override
             public void onClick(View v) {
                 Permissions.from(BrowserApp.this)
                         .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                         .run();
             }
         };
 
-        SnackbarHelper.showSnackbarWithAction(this,
-                getString(R.string.updater_permission_text),
-                Snackbar.LENGTH_INDEFINITE,
-                getString(R.string.updater_permission_allow),
-                allowCallback);
+        SnackbarBuilder.builder(this)
+                .message(R.string.updater_permission_text)
+                .duration(Snackbar.LENGTH_INDEFINITE)
+                .action(R.string.updater_permission_allow)
+                .callback(allowCallback)
+                .buildAndShow();
     }
 
     private void conditionallyNotifyEOL() {
         final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
         try {
             final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
             if (!prefs.contains(EOL_NOTIFIED)) {
 
@@ -2894,17 +2895,17 @@ public class BrowserApp extends GeckoApp
      */
     private class HideOnTouchListener implements TouchEventInterceptor {
         private boolean mIsHidingTabs;
         private final Rect mTempRect = new Rect();
 
         @Override
         public boolean onInterceptTouchEvent(View view, MotionEvent event) {
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-                SnackbarHelper.dismissCurrentSnackbar();
+                SnackbarBuilder.dismissCurrentSnackbar();
             }
 
 
 
             // We need to account for scroll state for the touched view otherwise
             // tapping on an "empty" part of the view will still be considered a
             // valid touch event.
             if (view.getScrollX() != 0 || view.getScrollY() != 0) {
@@ -3903,31 +3904,36 @@ public class BrowserApp extends GeckoApp
         final Tab newTab = Tabs.getInstance().loadUrl(pageURL, loadFlags);
 
         // We switch to the desired tab by unique ID, which closes any window
         // for a race between opening the tab and closing it, and switching to
         // it. We could also switch to the Tab explicitly, but we don't want to
         // hold a reference to the Tab itself in the anonymous listener class.
         final int newTabId = newTab.getId();
 
-        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
+        final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "switchtab");
 
                 maybeSwitchToTab(newTabId);
             }
         };
 
         final String message = isPrivate ?
                 getResources().getString(R.string.new_private_tab_opened) :
                 getResources().getString(R.string.new_tab_opened);
         final String buttonMessage = getResources().getString(R.string.switch_button_message);
 
-        SnackbarHelper.showSnackbarWithAction(this, message, Snackbar.LENGTH_LONG, buttonMessage, callback);
+        SnackbarBuilder.builder(this)
+                .message(message)
+                .duration(Snackbar.LENGTH_LONG)
+                .action(buttonMessage)
+                .callback(callback)
+                .buildAndShow();
     }
 
     // BrowserSearch.OnSearchListener
     @Override
     public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
         // Don't store searches that happen in private tabs. This assumes the user can only
         // perform a search inside the currently selected tab, which is true for searches
         // that come from SearchEngineRow.
--- a/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
+++ b/mobile/android/base/java/org/mozilla/gecko/EditBookmarkDialog.java
@@ -212,19 +212,20 @@ public class EditBookmarkDialog {
                         String newKeyword = keywordText.getText().toString().trim();
 
                         db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
                         return null;
                     }
 
                     @Override
                     public void onPostExecute(Void result) {
-                        SnackbarHelper.showSnackbar((Activity) context,
-                                context.getString(R.string.bookmark_updated),
-                                Snackbar.LENGTH_LONG);
+                        SnackbarBuilder.builder((Activity) context)
+                                .message(R.string.bookmark_updated)
+                                .duration(Snackbar.LENGTH_LONG)
+                                .buildAndShow();
                     }
                 }).execute();
             }
         });
 
         editPrompt.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int whichButton) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -550,17 +550,20 @@ public abstract class GeckoApp
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url);
                     final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added;
                     ThreadUtils.postToUiThread(new Runnable() {
                         @Override
                         public void run() {
-                            SnackbarHelper.showSnackbar(GeckoApp.this, getString(resId), Snackbar.LENGTH_LONG);
+                            SnackbarBuilder.builder(GeckoApp.this)
+                                    .message(resId)
+                                    .duration(Snackbar.LENGTH_LONG)
+                                    .buildAndShow();
                         }
                     });
                 }
             });
 
         } else if ("Contact:Add".equals(event)) {
             final String email = message.optString("email", null);
             final String phone = message.optString("phone", null);
@@ -620,17 +623,21 @@ public abstract class GeckoApp
                 title = tab.getDisplayTitle();
             }
             IntentHelper.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false);
 
             // Context: Sharing via chrome list (no explicit session is active)
             Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "text");
 
         } else if ("Snackbar:Show".equals(event)) {
-            SnackbarHelper.showSnackbar(this, message, callback);
+            SnackbarBuilder.builder(this)
+                    .fromEvent(message)
+                    .callback(callback)
+                    .buildAndShow();
+
         } else if ("SystemUI:Visibility".equals(event)) {
             setSystemUiVisible(message.getBoolean("visible"));
 
         } else if ("ToggleChrome:Focus".equals(event)) {
             focusChrome();
 
         } else if ("ToggleChrome:Hide".equals(event)) {
             toggleChrome(false);
@@ -930,39 +937,48 @@ public abstract class GeckoApp
                 byte[] imgBuffer = os.toByteArray();
                 image = BitmapUtils.decodeByteArray(imgBuffer);
             }
             if (image != null) {
                 // Some devices don't have a DCIM folder and the Media.insertImage call will fail.
                 File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
 
                 if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) {
-                    SnackbarHelper.showSnackbar(this, getString(R.string.set_image_path_fail), Snackbar.LENGTH_LONG);
+                    SnackbarBuilder.builder(this)
+                            .message(R.string.set_image_path_fail)
+                            .duration(Snackbar.LENGTH_LONG)
+                            .buildAndShow();
                     return;
                 }
                 String path = Media.insertImage(getContentResolver(), image, null, null);
                 if (path == null) {
-                    SnackbarHelper.showSnackbar(this, getString(R.string.set_image_path_fail), Snackbar.LENGTH_LONG);
+                    SnackbarBuilder.builder(this)
+                            .message(R.string.set_image_path_fail)
+                            .duration(Snackbar.LENGTH_LONG)
+                            .buildAndShow();
                     return;
                 }
                 final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
                 intent.addCategory(Intent.CATEGORY_DEFAULT);
                 intent.setData(Uri.parse(path));
 
                 // Removes the image from storage once the chooser activity ends.
                 Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title));
                 ActivityResultHandler handler = new ActivityResultHandler() {
                     @Override
                     public void onActivityResult (int resultCode, Intent data) {
                         getContentResolver().delete(intent.getData(), null, null);
                     }
                 };
                 ActivityHandlerHelper.startIntentForActivity(this, chooser, handler);
             } else {
-                SnackbarHelper.showSnackbar(this, getString(R.string.set_image_fail), Snackbar.LENGTH_LONG);
+                SnackbarBuilder.builder(this)
+                        .message(R.string.set_image_fail)
+                        .duration(Snackbar.LENGTH_LONG)
+                        .buildAndShow();
             }
         } catch (OutOfMemoryError ome) {
             Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
         } catch (IOException ioe) {
             Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
         } finally {
             if (is != null) {
                 try {
rename from mobile/android/base/java/org/mozilla/gecko/SnackbarHelper.java
rename to mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
--- a/mobile/android/base/java/org/mozilla/gecko/SnackbarHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
@@ -7,37 +7,38 @@ package org.mozilla.gecko;
 
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeJSObject;
 
 import android.app.Activity;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
+import android.support.annotation.StringRes;
 import android.support.design.widget.Snackbar;
 import android.support.v4.content.ContextCompat;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.View;
 import android.widget.TextView;
 
 import java.lang.ref.WeakReference;
 
 /**
  * Helper class for creating and dismissing snackbars. Use this class to guarantee a consistent style and behavior
  * across the app.
  */
-public class SnackbarHelper {
+public class SnackbarBuilder {
     /**
      * Combined interface for handling all callbacks from a snackbar because anonymous classes can only extend one
      * interface or class.
      */
     public static abstract class SnackbarCallback extends Snackbar.Callback implements View.OnClickListener {}
-    public static final String LOGTAG = "GeckoSnackbarHelper";
+    public static final String LOGTAG = "GeckoSnackbarBuilder";
 
     /**
      * SnackbarCallback implementation for delegating snackbar events to an EventCallback.
      */
     private static class SnackbarEventCallback extends SnackbarCallback {
         private EventCallback callback;
 
         public SnackbarEventCallback(EventCallback callback) {
@@ -63,87 +64,147 @@ public class SnackbarHelper {
             callback.sendError(null);
             callback = null; // Releasing reference. We only want to execute the callback once.
         }
     }
 
     private static final Object currentSnackbarLock = new Object();
     private static WeakReference<Snackbar> currentSnackbar = new WeakReference<>(null); // Guarded by 'currentSnackbarLock'
 
+    private final Activity activity;
+    private String message;
+    private int duration;
+    private String action;
+    private SnackbarCallback callback;
+    private Drawable icon;
+    private Integer backgroundColor;
+    private Integer actionColor;
+
     /**
-     * Show a snackbar to display a message.
-     *
      * @param activity Activity to show the snackbar in.
+     */
+    private SnackbarBuilder(final Activity activity) {
+        this.activity = activity;
+    }
+
+    public static SnackbarBuilder builder(final Activity activity) {
+        return new SnackbarBuilder(activity);
+    }
+
+    /**
      * @param message The text to show. Can be formatted text.
+     */
+    public SnackbarBuilder message(final String message) {
+        this.message = message;
+        return this;
+    }
+
+    /**
+     * @param id The id of the string resource to show. Can be formatted text.
+     */
+    public SnackbarBuilder message(@StringRes final int id) {
+        message = activity.getResources().getString(id);
+        return this;
+    }
+
+    /**
      * @param duration How long to display the message.
      */
-    public static void showSnackbar(Activity activity, String message, int duration) {
-        showSnackbarWithAction(activity, message, duration, null, null);
+    public SnackbarBuilder duration(final int duration) {
+        this.duration = duration;
+        return this;
+    }
+
+    /**
+     * @param action Action text to display.
+     */
+    public SnackbarBuilder action(final String action) {
+        this.action = action;
+        return this;
     }
 
     /**
-     * Build and show a snackbar from a Gecko Snackbar:Show event.
+     * @param id The id of the string resource for the action text to display.
+     */
+    public SnackbarBuilder action(@StringRes final int id) {
+        action = activity.getResources().getString(id);
+        return this;
+    }
+
+    /**
+     * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
+     */
+    public SnackbarBuilder callback(final SnackbarCallback callback) {
+        this.callback = callback;
+        return this;
+    }
+
+    /**
+     * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
+     */
+    public SnackbarBuilder callback(final EventCallback callback) {
+        this.callback = new SnackbarEventCallback(callback);
+        return this;
+    }
+
+    /**
+     * @param icon Icon to be displayed with the snackbar text.
      */
-    public static void showSnackbar(Activity activity, final NativeJSObject object, final EventCallback callback) {
-        final String message = object.getString("message");
-        final int duration = object.getInt("duration");
+    public SnackbarBuilder icon(final Drawable icon) {
+        this.icon = icon;
+        return this;
+    }
+
+    /**
+     * @param backgroundColor Snackbar background color.
+     */
+    public SnackbarBuilder backgroundColor(final Integer backgroundColor) {
+        this.backgroundColor = backgroundColor;
+        return this;
+    }
 
-        Integer backgroundColor = null;
+    /**
+     * @param actionColor Action text color.
+     */
+    public SnackbarBuilder actionColor(final Integer actionColor) {
+        this.actionColor = actionColor;
+        return this;
+    }
+
+    /**
+     * @param object Populate the builder with data from a Gecko Snackbar:Show event.
+     */
+    public SnackbarBuilder fromEvent(final NativeJSObject object) {
+        message = object.getString("message");
+        duration = object.getInt("duration");
 
         if (object.has("backgroundColor")) {
             final String providedColor = object.getString("backgroundColor");
             try {
                 backgroundColor = Color.parseColor(providedColor);
             } catch (IllegalArgumentException e) {
                 Log.w(LOGTAG, "Failed to parse color string: " + providedColor);
             }
         }
 
-        NativeJSObject action = object.optObject("action", null);
-
-        showSnackbarWithActionAndColors(activity,
-                message,
-                duration,
-                action != null ? action.optString("label", null) : null,
-                new SnackbarHelper.SnackbarEventCallback(callback),
-                null,
-                backgroundColor,
-                null
-        );
+        NativeJSObject actionObject = object.optObject("action", null);
+        if (actionObject != null) {
+            action = actionObject.optString("label", null);
+        }
+        return this;
     }
 
-    /**
-     * Show a snackbar to display a message and an action.
-     *
-     * @param activity Activity to show the snackbar in.
-     * @param message The text to show. Can be formatted text.
-     * @param duration How long to display the message.
-     * @param action Action text to display.
-     * @param callback Callback to be invoked when the action is clicked or the snackbar is dismissed.
-     */
-    public static void showSnackbarWithAction(Activity activity, String message, int duration, String action, SnackbarCallback callback) {
-        showSnackbarWithActionAndColors(activity, message, duration, action, callback, null, null, null);
-    }
-
-
-    public static void showSnackbarWithActionAndColors(Activity activity,
-                                                       String message,
-                                                       int duration,
-                                                       String action,
-                                                       SnackbarCallback callback,
-                                                       Drawable icon,
-                                                       Integer backgroundColor,
-                                                       Integer actionColor) {
+    public void buildAndShow() {
         final View parentView = findBestParentView(activity);
         final Snackbar snackbar = Snackbar.make(parentView, message, duration);
 
         if (callback != null && !TextUtils.isEmpty(action)) {
             snackbar.setAction(action, callback);
             if (actionColor == null) {
-                ContextCompat.getColor(activity, R.color.fennec_ui_orange);
+                snackbar.setActionTextColor(ContextCompat.getColor(activity, R.color.fennec_ui_orange));
             } else {
                 snackbar.setActionTextColor(actionColor);
             }
             snackbar.setCallback(callback);
         }
 
         if (icon != null) {
             int leftPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, activity.getResources().getDisplayMetrics());
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/BookmarkStateChangeDelegate.java
@@ -20,17 +20,17 @@ 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.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.SnackbarBuilder;
 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.promotion.SimpleHelperUI;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
@@ -125,38 +125,42 @@ public class BookmarkStateChangeDelegate
     private void showBookmarkAddedSnackbar() {
         final BrowserApp browserApp = getBrowserApp();
         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() {
+        final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.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);
+        SnackbarBuilder.builder(browserApp)
+                .message(R.string.bookmark_added)
+                .duration(Snackbar.LENGTH_LONG)
+                .action(R.string.bookmark_options)
+                .callback(callback)
+                .buildAndShow();
     }
 
     private void showBookmarkRemovedSnackbar() {
         final BrowserApp browserApp = getBrowserApp();
         if (browserApp == null) {
             return;
         }
 
-        SnackbarHelper.showSnackbar(browserApp, browserApp.getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
+        SnackbarBuilder.builder(browserApp)
+                .message(R.string.bookmark_removed)
+                .duration(Snackbar.LENGTH_LONG)
+                .buildAndShow();
     }
 
     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
@@ -208,25 +212,26 @@ public class BookmarkStateChangeDelegate
     private void showReaderModeBookmarkAddedSnackbar() {
         final BrowserApp browserApp = getBrowserApp();
         if (browserApp == null) {
             return;
         }
 
         final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE);
 
-        final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
+        final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.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);
+        SnackbarBuilder.builder(browserApp)
+                .message(R.string.reader_saved_offline)
+                .duration(Snackbar.LENGTH_LONG)
+                .action(R.string.reader_switch_to_bookmarks)
+                .callback(callback)
+                .icon(iconDownloaded)
+                .backgroundColor(ContextCompat.getColor(browserApp, R.color.link_blue))
+                .actionColor(Color.WHITE)
+                .buildAndShow();
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/OfflineTabStatusDelegate.java
@@ -9,17 +9,17 @@ import android.app.Activity;
 import android.os.Bundle;
 import android.support.annotation.CallSuper;
 import android.support.design.widget.Snackbar;
 import android.support.v4.content.ContextCompat;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.WeakHashMap;
@@ -95,18 +95,15 @@ public class OfflineTabStatusDelegate ex
      */
     private static void showLoadedOfflineSnackbar(final Activity activity) {
         if (activity == null) {
             return;
         }
 
         Telemetry.sendUIEvent(TelemetryContract.Event.NETERROR, TelemetryContract.Method.TOAST, "usecache");
 
-        SnackbarHelper.showSnackbarWithActionAndColors(
-                activity,
-                activity.getResources().getString(R.string.tab_offline_version),
-                Snackbar.LENGTH_INDEFINITE,
-                null, null, null,
-                ContextCompat.getColor(activity, R.color.link_blue),
-                null
-        );
+        SnackbarBuilder.builder(activity)
+                .message(R.string.tab_offline_version)
+                .duration(Snackbar.LENGTH_INDEFINITE)
+                .backgroundColor(ContextCompat.getColor(activity, R.color.link_blue))
+                .buildAndShow();
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/delegates/ScreenshotDelegate.java
@@ -10,17 +10,17 @@ import android.os.Bundle;
 import android.support.design.widget.Snackbar;
 import android.util.Log;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ScreenshotObserver;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 
 import java.lang.ref.WeakReference;
 
 /**
@@ -56,18 +56,20 @@ public class ScreenshotDelegate extends 
         final Activity activity = getBrowserApp();
         if (activity == null) {
             return;
         }
 
         GeckoProfile.get(activity).getDB().getUrlAnnotations().insertScreenshot(
                 activity.getContentResolver(), selectedTab.getURL(), screenshotPath);
 
-        SnackbarHelper.showSnackbar(activity,
-                activity.getResources().getString(R.string.screenshot_added_to_bookmarks), Snackbar.LENGTH_SHORT);
+        SnackbarBuilder.builder(activity)
+                .message(R.string.screenshot_added_to_bookmarks)
+                .duration(Snackbar.LENGTH_SHORT)
+                .buildAndShow();
     }
 
     @Override
     public void onResume(BrowserApp browserApp) {
         mScreenshotObserver.start();
     }
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -8,17 +8,17 @@ package org.mozilla.gecko.home;
 import java.util.EnumSet;
 
 import org.mozilla.gecko.EditBookmarkDialog;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
@@ -431,14 +431,15 @@ public abstract class HomeFragment exten
                     Log.e(LOGTAG, "Can't remove item type " + mType.toString());
                     break;
             }
             return null;
         }
 
         @Override
         public void onPostExecute(Void result) {
-            SnackbarHelper.showSnackbar((Activity) mContext,
-                    mContext.getString(R.string.page_removed),
-                    Snackbar.LENGTH_LONG);
+            SnackbarBuilder.builder((Activity) mContext)
+                    .message(R.string.page_removed)
+                    .duration(Snackbar.LENGTH_LONG)
+                    .buildAndShow();
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -19,17 +19,17 @@ import org.mozilla.gecko.GeckoActivitySt
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.LocaleManager;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.TelemetryContract.Method;
 import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
 import org.mozilla.gecko.permissions.Permissions;
@@ -616,29 +616,33 @@ OnSharedPreferenceChangeListener
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("Sanitize:Finished")) {
                 boolean success = message.getBoolean("success");
                 final int stringRes = success ? R.string.private_data_success : R.string.private_data_fail;
 
-                SnackbarHelper.showSnackbar(GeckoPreferences.this,
-                        getString(stringRes),
-                        Snackbar.LENGTH_LONG);
+                SnackbarBuilder.builder(GeckoPreferences.this)
+                        .message(stringRes)
+                        .duration(Snackbar.LENGTH_LONG)
+                        .buildAndShow();
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
         if ("Snackbar:Show".equals(event)) {
-            SnackbarHelper.showSnackbar(this, message, callback);
+            SnackbarBuilder.builder(this)
+                    .fromEvent(message)
+                    .callback(callback)
+                    .buildAndShow();
         }
     }
 
     /**
       * Initialize all of the preferences (native of Gecko ones) for this screen.
       *
       * @param prefs The android.preference.PreferenceGroup to initialize
       * @return The integer id for the PrefsHelper.PrefHandlerBase listener added
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchEnginePreference.java
@@ -2,17 +2,17 @@
  * 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.preferences;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.FaviconView;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -87,19 +87,20 @@ public class SearchEnginePreference exte
 
     @Override
     public void showDialog() {
         // If this is the last engine, then we are the default, and none of the options
         // on this menu can do anything.
         if (mParentCategory.getPreferenceCount() == 1) {
             Activity activity = (Activity) getContext();
 
-            SnackbarHelper.showSnackbar(activity,
-                    activity.getString(R.string.pref_search_last_toast),
-                    Snackbar.LENGTH_LONG);
+            SnackbarBuilder.builder(activity)
+                    .message(R.string.pref_search_last_toast)
+                    .duration(Snackbar.LENGTH_LONG)
+                    .buildAndShow();
 
             return;
         }
 
         super.showDialog();
     }
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
@@ -21,17 +21,17 @@ import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.SiteIdentity;
 import org.mozilla.gecko.SiteIdentity.SecurityMode;
 import org.mozilla.gecko.SiteIdentity.MixedMode;
 import org.mozilla.gecko.SiteIdentity.TrackingMode;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.AnchoredPopup;
 import org.mozilla.gecko.widget.DoorHanger;
 import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
 import org.json.JSONObject;
@@ -226,22 +226,28 @@ public class SiteIdentityPopup extends A
                         } else {
                             password = login.getString("password");
                         }
                         if (AppConstants.Versions.feature11Plus) {
                             manager.setPrimaryClip(ClipData.newPlainText("password", password));
                         } else {
                             manager.setText(password);
                         }
-                        SnackbarHelper.showSnackbar(activity, activity.getString(R.string.doorhanger_login_select_toast_copy), Snackbar.LENGTH_SHORT);
+                        SnackbarBuilder.builder(activity)
+                                .message(R.string.doorhanger_login_select_toast_copy)
+                                .duration(Snackbar.LENGTH_SHORT)
+                                .buildAndShow();
                     }
                     dismiss();
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "Error handling Select login button click", e);
-                    SnackbarHelper.showSnackbar(activity, activity.getString(R.string.doorhanger_login_select_toast_copy_error), Snackbar.LENGTH_SHORT);
+                    SnackbarBuilder.builder(activity)
+                            .message(R.string.doorhanger_login_select_toast_copy_error)
+                            .duration(Snackbar.LENGTH_SHORT)
+                            .buildAndShow();
                 }
             }
         };
 
         final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.LOGIN, buttonClickListener);
 
         // Set buttons.
         config.setButton(mContext.getString(R.string.button_cancel), ButtonType.CANCEL.ordinal(), false);
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
@@ -8,17 +8,17 @@ package org.mozilla.gecko.widget;
 import android.app.Activity;
 import android.net.Uri;
 import android.support.design.widget.Snackbar;
 import android.util.Base64;
 import android.view.Menu;
 
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
+import org.mozilla.gecko.SnackbarBuilder;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
@@ -291,19 +291,20 @@ public class GeckoActionProvider {
     public void downloadImageForIntent(final Intent intent) {
         final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
         final File dir = GeckoApp.getTempDirectory();
 
         if (src == null || dir == null) {
             // We should be, but currently aren't, statically guaranteed an Activity context.
             // Try our best.
             if (mContext instanceof Activity) {
-                SnackbarHelper.showSnackbar((Activity) mContext,
-                        mContext.getApplicationContext().getString(R.string.share_image_failed),
-                        Snackbar.LENGTH_LONG);
+                SnackbarBuilder.builder((Activity) mContext)
+                        .message(mContext.getApplicationContext().getString(R.string.share_image_failed))
+                        .duration(Snackbar.LENGTH_LONG)
+                        .buildAndShow();
             }
             return;
         }
 
         GeckoApp.deleteTempFiles();
 
         String type = intent.getType();
         OutputStream os = null;
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -559,17 +559,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'ScreenshotObserver.java',
     'search/SearchEngine.java',
     'search/SearchEngineManager.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentity.java',
     'SmsManager.java',
-    'SnackbarHelper.java',
+    'SnackbarBuilder.java',
     'sqlite/ByteBufferInputStream.java',
     'sqlite/MatrixBlobCursor.java',
     'sqlite/SQLiteBridge.java',
     'sqlite/SQLiteBridgeException.java',
     'SuggestClient.java',
     'SurfaceBits.java',
     'Tab.java',
     'tabqueue/TabQueueHelper.java',