jenkins-bot has submitted this change and it was merged.

Change subject: Replace Toast and Crouton with Snackbar.
......................................................................


Replace Toast and Crouton with Snackbar.

Unified the display of errors and warnings using Snackbar in place of
Toast and/or Crouton, while utilizing the new methods for converting
Throwables to plain strings.

Turned the Wikipedia Zero banner from a Crouton into a Snackbar. Was able
to keep the exact same formatting as the Crouton implementation, except
the banner now pops up from the bottom, instead of the top. This is the
only change that the Partnership team will need to approve.

Note: this will need a LOT of testing.

Bug: T102548

Change-Id: I4314a8d219a90415636cfaca867152b9251e551d
---
D wikipedia/assets/licenses/Crouton
M wikipedia/build.gradle
M wikipedia/res/values/credits.xml
M wikipedia/res/values/strings.xml
M wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java
M wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
M wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountTask.java
M wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java
M wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java
M wikipedia/src/main/java/org/wikipedia/login/LoginActivity.java
D wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchException.java
M wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchTask.java
M wikipedia/src/main/java/org/wikipedia/nearby/NearbyFragment.java
M wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
M wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
M wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java
M wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
M wikipedia/src/main/java/org/wikipedia/page/linkpreview/LinkPreviewDialog.java
M wikipedia/src/main/java/org/wikipedia/random/RandomArticleIdTask.java
M wikipedia/src/main/java/org/wikipedia/random/RandomHandler.java
M wikipedia/src/main/java/org/wikipedia/savedpages/RefreshPagesHandler.java
M wikipedia/src/main/java/org/wikipedia/savedpages/SavedPagesFragment.java
M wikipedia/src/main/java/org/wikipedia/search/SearchResultsFragment.java
M wikipedia/src/main/java/org/wikipedia/settings/AboutActivity.java
A wikipedia/src/main/java/org/wikipedia/util/FeedbackUtil.java
M wikipedia/src/main/java/org/wikipedia/zero/WikipediaZeroHandler.java
26 files changed, 231 insertions(+), 447 deletions(-)

Approvals:
  BearND: Looks good to me, approved
  Dbrant: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/wikipedia/assets/licenses/Crouton 
b/wikipedia/assets/licenses/Crouton
deleted file mode 100644
index f433b1a..0000000
--- a/wikipedia/assets/licenses/Crouton
+++ /dev/null
@@ -1,177 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
diff --git a/wikipedia/build.gradle b/wikipedia/build.gradle
index d4777de..d0dd470 100644
--- a/wikipedia/build.gradle
+++ b/wikipedia/build.gradle
@@ -125,7 +125,6 @@
     compile('ch.acra:acra:4.5.0') {
         exclude group: 'org.json', module: 'json'
     }
-    compile 'de.keyboardsurfer.android.widget:crouton:1.8.5@aar'
     compile 'com.github.chrisbanes.photoview:library:1.2.3'
     compile 'com.github.ryanjohn1:onboarding:1.0.3'
     compile 'com.jakewharton:disklrucache:2.0.2'
diff --git a/wikipedia/res/values/credits.xml b/wikipedia/res/values/credits.xml
index d9c0c1b..6a7da72 100644
--- a/wikipedia/res/values/credits.xml
+++ b/wikipedia/res/values/credits.xml
@@ -5,10 +5,6 @@
         <!--  https://raw.githubusercontent.com/ACRA/acra/master/LICENSE -->
         (<a title="acra-title" name="acra-name" 
href="file:///android_asset/licenses/ACRA">license</a>),
 
-        <a href="https://github.com/keyboardsurfer/Crouton";>Crouton</a>
-        <!-- 
https://raw.githubusercontent.com/keyboardsurfer/Crouton/master/LICENSE -->
-        (<a href="file:///android_asset/licenses/Crouton">license</a>),
-
         <a href="https://github.com/JakeWharton/DiskLruCache";>DiskLruCache</a>
         <!-- 
https://github.com/JakeWharton/DiskLruCache/blob/master/LICENSE.txt -->
         (<a href="file:///android_asset/licenses/DiskLruCache">license</a>),
diff --git a/wikipedia/res/values/strings.xml b/wikipedia/res/values/strings.xml
index c9cbfa4..7a1bcc0 100644
--- a/wikipedia/res/values/strings.xml
+++ b/wikipedia/res/values/strings.xml
@@ -100,7 +100,7 @@
     <string name="login_error_wrong_password">Incorrect password</string>
     <string name="login_error_wrong_username">Incorrect username</string>
     <string name="login_error_illegal">Invalid username</string>
-    <string name="login_success_toast">Login Success!</string>
+    <string name="login_success_toast">Logged in successfully!</string>
     <string name="login_error_blocked">This user has been blocked</string>
     <string name="login_error_throttled"><![CDATA[Too many login attempts. 
Wait for a few minutes and try again.]]></string>
     <string name="login_error_unknown">Login failed :(</string>
diff --git a/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java 
b/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java
index 0e725ff..b9fdb6c 100644
--- a/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java
+++ b/wikipedia/src/main/java/org/wikipedia/NavDrawerFragment.java
@@ -13,7 +13,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
-import android.widget.Toast;
 import org.wikipedia.analytics.LoginFunnel;
 import org.wikipedia.history.HistoryEntry;
 import org.wikipedia.history.HistoryFragment;
@@ -27,6 +26,7 @@
 import org.wikipedia.settings.SettingsActivity;
 import org.wikipedia.settings.SettingsActivityGB;
 import org.wikipedia.util.ApiUtil;
+import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.widgets.WidgetProviderFeaturedPage;
 
 public class NavDrawerFragment extends Fragment implements 
View.OnClickListener {
@@ -90,15 +90,16 @@
                         if (!isAdded()) {
                             return;
                         }
-                        if (title == null) {
+                        HistoryEntry historyEntry = new HistoryEntry(title, 
HistoryEntry.SOURCE_RANDOM);
+                        ((PageActivity) getActivity()).displayNewPage(title, 
historyEntry, true);
+                    }
 
-                            // TODO: show a SnackBar if we fail to fetch a 
random page.
-                            // (next patch, I promise)
-
-                        } else {
-                            HistoryEntry historyEntry = new 
HistoryEntry(title, HistoryEntry.SOURCE_RANDOM);
-                            ((PageActivity) 
getActivity()).displayNewPage(title, historyEntry, true);
+                    @Override
+                    public void onRandomPageFailed(Throwable caught) {
+                        if (!isAdded()) {
+                            return;
                         }
+                        FeedbackUtil.showError(getView(), caught);
                     }
                 });
     }
@@ -173,7 +174,7 @@
         app.getEditTokenStorage().clearAllTokens();
         app.getCookieManager().clearAllCookies();
         app.getUserInfoStorage().clearUser();
-        Toast.makeText(app, R.string.toast_logout_complete, 
Toast.LENGTH_LONG).show();
+        FeedbackUtil.showMessage(getActivity(), 
R.string.toast_logout_complete);
         if (doUpdate) {
             setupDynamicItems();
         }
@@ -206,7 +207,7 @@
             case R.id.nav_item_login:
                 intent.setClass(this.getActivity(), LoginActivity.class);
                 intent.putExtra(LoginActivity.LOGIN_REQUEST_SOURCE, 
LoginFunnel.SOURCE_NAV);
-                startActivity(intent);
+                startActivityForResult(intent, LoginActivity.REQUEST_LOGIN);
                 break;
             case R.id.nav_item_random:
                 randomHandler.doVisitRandomArticle();
@@ -246,6 +247,9 @@
             } else if (resultCode == SettingsActivity.ACTIVITY_RESULT_LOGOUT) {
                 doLogout();
             }
+        } else if (requestCode == LoginActivity.REQUEST_LOGIN
+                   && resultCode == LoginActivity.RESULT_LOGIN_SUCCESS) {
+            FeedbackUtil.showMessage(getActivity(), 
R.string.login_success_toast);
         }
     }
 
diff --git 
a/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
 
b/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
index 8b4480a..dc34a7f 100644
--- 
a/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
+++ 
b/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountActivity.java
@@ -3,6 +3,7 @@
 import android.app.ProgressDialog;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.util.Log;
 import android.view.MenuItem;
 import android.view.KeyEvent;
@@ -11,7 +12,6 @@
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
-import android.widget.Toast;
 import com.mobsandgeeks.saripaar.Rule;
 import com.mobsandgeeks.saripaar.Validator;
 import com.mobsandgeeks.saripaar.annotation.ConfirmPassword;
@@ -20,15 +20,16 @@
 import com.mobsandgeeks.saripaar.annotation.Required;
 import com.nineoldandroids.animation.Animator;
 import com.nineoldandroids.animation.AnimatorListenerAdapter;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import org.mediawiki.api.json.Api;
+import org.mediawiki.api.json.ApiException;
 import org.mediawiki.api.json.RequestBuilder;
 import org.wikipedia.*;
 import org.wikipedia.activity.ActivityUtil;
 import org.wikipedia.activity.ThemedActionBarActivity;
 import org.wikipedia.analytics.CreateAccountFunnel;
 import org.wikipedia.editing.CaptchaHandler;
+import static org.wikipedia.util.FeedbackUtil.showMessage;
+import static org.wikipedia.util.FeedbackUtil.showError;
 
 public class CreateAccountActivity extends ThemedActionBarActivity {
     public static final int RESULT_ACCOUNT_CREATED = 1;
@@ -215,43 +216,52 @@
         Utils.showPrivacyPolicy(this);
     }
 
-    public void handleError(CreateAccountResult result) {
-        String errorCode = result.getResult();
-        switch (errorCode) {
-            case "blocked":
-                if (app.getUserInfoStorage().isLoggedIn()) {
-                    Crouton.makeText(this, 
R.string.create_account_blocked_error, Style.ALERT).show();
-                } else {
-                    Crouton.makeText(this, 
R.string.create_account_blocked_anon_error, Style.ALERT).show();
-                }
-                break;
-            case "acct_creation_throttle_hit":
-                Crouton.makeText(this, 
R.string.create_account_ip_throttle_error, Style.ALERT).show();
-                break;
-            case "sorbs_create_account_reason":
-                Crouton.makeText(this, 
R.string.create_account_open_proxy_error, Style.ALERT).show();
-                break;
-            case "userexists":
-                //Request focus before displaying error message, so that it 
pops up on its own
-                usernameEdit.requestFocus();
-                Utils.setErrorPopup(usernameEdit, 
getString(R.string.create_account_username_exists_error));
-                break;
-            case "noname":
-                Crouton.makeText(this, R.string.create_account_noname_error, 
Style.ALERT).show();
-                break;
-            case "invalidemailaddress":
-                Crouton.makeText(this, 
R.string.create_account_invalid_email_error, Style.ALERT).show();
-                break;
-            case "passwordtooshort":
-                //FIXME: Find the value of $wgMinimalPasswordLength and tell 
the user the minimum pwd length
-                Crouton.makeText(this, 
R.string.create_account_password_too_short_error, Style.ALERT).show();
-                break;
-            case "password-name-match":
-                Crouton.makeText(this, 
R.string.create_account_password_name_match_error, Style.ALERT).show();
-                break;
-            default:
-                Crouton.makeText(this, R.string.create_account_generic_error, 
Style.ALERT).show();
-                break;
+    public void handleError(@NonNull Throwable e) {
+        if (e instanceof ApiException && ((ApiException) e).getCode() != null) 
{
+            String errorCode = ((ApiException) e).getCode();
+            funnel.logError(errorCode);
+            switch (errorCode) {
+                case "blocked":
+                    if (app.getUserInfoStorage().isLoggedIn()) {
+                        showMessage(this, 
R.string.create_account_blocked_error);
+                    } else {
+                        showMessage(this, 
R.string.create_account_blocked_anon_error);
+                    }
+                    break;
+                case "acct_creation_throttle_hit":
+                    showMessage(this, 
R.string.create_account_ip_throttle_error);
+                    break;
+                case "sorbs_create_account_reason":
+                    showMessage(this, 
R.string.create_account_open_proxy_error);
+                    break;
+                case "userexists":
+                    //Request focus before displaying error message, so that 
it pops up on its own
+                    usernameEdit.requestFocus();
+                    Utils.setErrorPopup(usernameEdit, 
getString(R.string.create_account_username_exists_error));
+                    break;
+                case "noname":
+                    showMessage(this, R.string.create_account_noname_error);
+                    break;
+                case "invalidemailaddress":
+                    showMessage(this, 
R.string.create_account_invalid_email_error);
+                    break;
+                case "passwordtooshort":
+                    //FIXME: Find the value of $wgMinimalPasswordLength and 
tell the user the minimum pwd length
+                    showMessage(this, 
R.string.create_account_password_too_short_error);
+                    break;
+                case "password-name-match":
+                    showMessage(this, 
R.string.create_account_password_name_match_error);
+                    break;
+                case "createaccount-hook-aborted":
+                    // show the actual message provided by the hook
+                    showMessage(this, ((ApiException) e).getInfo());
+                    break;
+                default:
+                    showMessage(this, R.string.create_account_generic_error);
+                    break;
+            }
+        } else {
+            showError(this, e);
         }
     }
 
@@ -282,7 +292,7 @@
                     return;
                 }
                 progressDialog.dismiss();
-                Crouton.makeText(CreateAccountActivity.this, 
R.string.create_account_no_network, Style.ALERT).show();
+                handleError(caught);
             }
 
             @Override
@@ -308,7 +318,6 @@
                     resultIntent.putExtra("username", 
((CreateAccountSuccessResult) result).getUsername());
                     resultIntent.putExtra("password", 
passwordEdit.getText().toString());
                     setResult(RESULT_ACCOUNT_CREATED, resultIntent);
-                    Toast.makeText(CreateAccountActivity.this, 
R.string.create_account_account_created_toast, Toast.LENGTH_LONG).show();
                     finish();
                 } else {
                     progressDialog.dismiss();
@@ -318,9 +327,6 @@
                         // Eventually this should be fixed to have the new 
captcha info come back.
                         createAccountResult = null;
                         doCreateAccount();
-                    } else {
-                        funnel.logError(result.getResult());
-                        handleError(result);
                     }
                 }
             }
diff --git 
a/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountTask.java 
b/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountTask.java
index c971037..bf96a74 100644
--- a/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountTask.java
+++ b/wikipedia/src/main/java/org/wikipedia/createaccount/CreateAccountTask.java
@@ -46,9 +46,6 @@
 
     @Override
     public CreateAccountResult processResult(ApiResult result) throws 
Throwable {
-        if (result.asObject().has("error")) {
-            return new 
CreateAccountResult(result.asObject().optJSONObject("error").optString("code"));
-        }
         JSONObject ca = result.asObject().optJSONObject("createaccount");
         String apiResult = ca.optString("result");
         switch (apiResult) {
diff --git a/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java 
b/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java
index a8c8412..77df881 100644
--- a/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java
+++ b/wikipedia/src/main/java/org/wikipedia/editing/CaptchaHandler.java
@@ -12,13 +12,12 @@
 import android.widget.ImageView;
 import com.squareup.picasso.Callback;
 import com.squareup.picasso.Picasso;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import org.mediawiki.api.json.RequestBuilder;
 import org.wikipedia.R;
 import org.wikipedia.Site;
 import org.wikipedia.ViewAnimations;
 import org.wikipedia.WikipediaApp;
+import org.wikipedia.util.FeedbackUtil;
 
 public class CaptchaHandler {
     private final Activity activity;
@@ -69,7 +68,7 @@
                     @Override
                     public void onCatch(Throwable caught) {
                         cancelCaptcha();
-                        Crouton.makeText(activity, 
R.string.create_account_no_network, Style.ALERT).show();
+                        FeedbackUtil.showError(activity, caught);
                     }
 
                 }.execute();
diff --git 
a/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java 
b/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java
index ed6f14d..8d7c471 100644
--- a/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/editing/EditSectionActivity.java
@@ -6,8 +6,8 @@
 import android.graphics.Typeface;
 import android.net.Uri;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.v7.app.AlertDialog;
-import android.support.annotation.Nullable;
 import android.text.Editable;
 import android.text.Html;
 import android.text.TextUtils;
@@ -23,10 +23,7 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
-import android.widget.Toast;
 import com.squareup.otto.Bus;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import org.mediawiki.api.json.Api;
 import org.mediawiki.api.json.ApiException;
 import org.mediawiki.api.json.RequestBuilder;
@@ -48,6 +45,7 @@
 import org.wikipedia.page.LinkMovementMethodExt;
 import org.wikipedia.page.PageProperties;
 import org.wikipedia.util.ApiUtil;
+import org.wikipedia.util.FeedbackUtil;
 
 public class EditSectionActivity extends ThemedActionBarActivity {
     public static final String ACTION_EDIT_SECTION = 
"org.wikipedia.edit_section";
@@ -235,6 +233,7 @@
             if (resultCode == LoginActivity.RESULT_LOGIN_SUCCESS) {
                 updateEditLicenseText();
                 funnel.logLoginSuccess();
+                FeedbackUtil.showMessage(this, R.string.login_success_toast);
             } else {
                 funnel.logLoginFailure();
             }
@@ -273,8 +272,7 @@
                         }
                         if (caught instanceof ApiException) {
                             // This is a fairly standard editing exception. 
Handle it appropriately.
-                            ApiException e = (ApiException) caught;
-                            handleEditingException(e.getCode());
+                            handleEditingException((ApiException) caught);
                         } else {
                             // If it's not an API exception, we have no idea 
what's wrong.
                             // Show the user a generic error message.
@@ -297,7 +295,6 @@
                             Intent data = new Intent();
                             data.putExtra(EXTRA_SECTION_ID, sectionID);
                             setResult(EditHandler.RESULT_REFRESH_PAGE, data);
-                            Toast.makeText(EditSectionActivity.this, 
R.string.edit_saved_successfully, Toast.LENGTH_LONG).show();
                             Utils.hideSoftKeyboard(EditSectionActivity.this);
                             finish();
                         } else if (result instanceof CaptchaResult) {
@@ -314,11 +311,8 @@
                                 editPreviewFragment.hide();
                             }
                         } else if (result instanceof SpamBlacklistEditResult) {
-                            Crouton.makeText(
-                                    EditSectionActivity.this,
-                                    
getString(R.string.editing_error_spamblacklist, ((SpamBlacklistEditResult) 
result).getDomain()),
-                                    Style.ALERT
-                            ).show();
+                            FeedbackUtil.showMessage(EditSectionActivity.this,
+                                    R.string.editing_error_spamblacklist);
                             progressDialog.dismiss();
                             editPreviewFragment.hide();
                         } else {
@@ -367,9 +361,10 @@
 
     /**
      * Processes API error codes encountered during editing, and handles them 
as appropriate.
-     * @param code The API error code to handle.
+     * @param e The ApiException to handle.
      */
-    private void handleEditingException(@Nullable String code) {
+    private void handleEditingException(@NonNull ApiException e) {
+        String code = e.getCode();
         if (app.getUserInfoStorage().isLoggedIn() && ("badtoken".equals(code)
                 || "assertuserfailed".equals(code))) {
             // looks like our session expired.
@@ -427,7 +422,7 @@
         } else {
             // an unknown error occurred, so just dismiss the progress dialog 
and show a message.
             progressDialog.dismiss();
-            Crouton.makeText(this, 
String.format(getString(R.string.edit_save_unknown_error), code), 
Style.ALERT).show();
+            FeedbackUtil.showError(this, e);
         }
     }
 
@@ -609,9 +604,7 @@
             } else {
                 message = getString(R.string.page_protected_other, 
pageProps.getEditProtectionStatus());
             }
-            Crouton.makeText(this,
-                    message,
-                    Style.INFO).show();
+            FeedbackUtil.showMessage(this, message);
         }
     }
 
diff --git a/wikipedia/src/main/java/org/wikipedia/login/LoginActivity.java 
b/wikipedia/src/main/java/org/wikipedia/login/LoginActivity.java
index 336df8b..02c6f82 100644
--- a/wikipedia/src/main/java/org/wikipedia/login/LoginActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/login/LoginActivity.java
@@ -10,17 +10,15 @@
 import android.view.inputmethod.EditorInfo;
 import android.widget.EditText;
 import android.widget.TextView;
-import android.widget.Toast;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import org.wikipedia.*;
 import org.wikipedia.activity.ActivityUtil;
 import org.wikipedia.activity.ThemedActionBarActivity;
 import org.wikipedia.analytics.LoginFunnel;
 import org.wikipedia.createaccount.CreateAccountActivity;
+import org.wikipedia.util.FeedbackUtil;
 
 public class LoginActivity extends ThemedActionBarActivity {
-    public static final int REQUEST_LOGIN = 1;
+    public static final int REQUEST_LOGIN = 100;
 
     public static final int RESULT_LOGIN_SUCCESS = 1;
     public static final int RESULT_LOGIN_FAIL = 2;
@@ -139,6 +137,8 @@
                 usernameText.setText(data.getStringExtra("username"));
                 passwordText.setText(data.getStringExtra("password"));
                 funnel.logCreateAccountSuccess();
+                FeedbackUtil.showMessage(this,
+                        R.string.create_account_account_created_toast);
                 doLogin();
             } else {
                 funnel.logCreateAccountFailure();
@@ -163,7 +163,7 @@
                     return;
                 }
                 progressDialog.dismiss();
-                Crouton.makeText(LoginActivity.this, 
R.string.login_error_no_network, Style.ALERT).show();
+                FeedbackUtil.showError(LoginActivity.this, caught);
             }
 
             @Override
@@ -176,7 +176,6 @@
                 progressDialog.dismiss();
                 if (result.getCode().equals("Success")) {
                     funnel.logSuccess();
-                    Toast.makeText(LoginActivity.this, 
R.string.login_success_toast, Toast.LENGTH_LONG).show();
 
                     Utils.hideSoftKeyboard(LoginActivity.this);
                     setResult(RESULT_LOGIN_SUCCESS);
@@ -223,13 +222,13 @@
                 Utils.setErrorPopup(usernameText, 
getString(R.string.login_error_illegal));
                 break;
             case "Blocked":
-                Crouton.makeText(this, R.string.login_error_blocked, 
Style.ALERT).show();
+                FeedbackUtil.showMessage(this, R.string.login_error_blocked);
                 break;
             case "Throttled":
-                Crouton.makeText(this, R.string.login_error_throttled, 
Style.ALERT).show();
+                FeedbackUtil.showMessage(this, R.string.login_error_throttled);
                 break;
             default:
-                Crouton.makeText(this, R.string.login_error_unknown, 
Style.ALERT).show();
+                FeedbackUtil.showMessage(this, R.string.login_error_unknown);
                 Log.d("Wikipedia", "Login failed with result " + result);
                 break;
         }
@@ -241,12 +240,6 @@
             progressDialog.dismiss();
         }
         super.onStop();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        Crouton.cancelAllCroutons();
     }
 
     @Override
diff --git 
a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchException.java 
b/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchException.java
deleted file mode 100644
index 0ddfa65..0000000
--- a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchException.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.wikipedia.nearby;
-
-public class NearbyFetchException extends Exception {
-    private final String code;
-    private final String info;
-
-    public NearbyFetchException(String code, String info) {
-        this.code = code;
-        this.info = info;
-    }
-
-    public String getCode() {
-        return code;
-    }
-
-    public String getMessage() {
-        return info;
-    }
-}
diff --git a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchTask.java 
b/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchTask.java
index 0ec8cac..f71a266 100644
--- a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchTask.java
+++ b/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFetchTask.java
@@ -3,7 +3,6 @@
 import android.content.Context;
 import android.location.Location;
 
-import org.json.JSONException;
 import org.json.JSONObject;
 import org.mediawiki.api.json.Api;
 import org.mediawiki.api.json.ApiException;
@@ -99,15 +98,8 @@
 
     @Override
     public NearbyResult processResult(ApiResult result) throws Throwable {
-
         try {
             JSONObject jsonObject = result.asObject();
-
-            if (jsonObject.has("error")) {
-                JSONObject errorJSON = jsonObject.optJSONObject("error");
-                throw new NearbyFetchException(errorJSON.optString("code"), 
errorJSON.optString("info"));
-            }
-
             return new NearbyResult(jsonObject);
         } catch (ApiException e) {
             // TODO: find a better way to deal with empty results
@@ -116,8 +108,6 @@
             } else {
                 throw e;
             }
-        } catch (JSONException e) {
-            throw new NearbyFetchException(e.toString(), e.getMessage());
         }
     }
 }
diff --git a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFragment.java 
b/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFragment.java
index 7f1fae6..410bbe4 100644
--- a/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFragment.java
+++ b/wikipedia/src/main/java/org/wikipedia/nearby/NearbyFragment.java
@@ -6,13 +6,11 @@
 import org.wikipedia.Utils;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.history.HistoryEntry;
-import org.mediawiki.api.json.ApiException;
 import org.wikipedia.page.PageActivity;
 import org.wikipedia.util.ApiUtil;
+import org.wikipedia.util.FeedbackUtil;
 
 import com.squareup.picasso.Picasso;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -49,7 +47,6 @@
 import android.widget.ListView;
 import android.widget.TextView;
 import android.widget.Toast;
-import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -336,18 +333,7 @@
                     if (!isAdded()) {
                         return;
                     }
-                    if (caught instanceof ApiException) {
-                        if (caught.getCause() instanceof UnknownHostException) 
{
-                            Crouton.makeText(getActivity(), 
R.string.nearby_no_network, Style.ALERT, nearbyContainer).show();
-                        } else {
-                            Crouton.makeText(getActivity(), 
R.string.error_network_error, Style.ALERT, nearbyContainer).show();
-                        }
-                    } else if (caught instanceof NearbyFetchException) {
-                        Log.e("Wikipedia", "Could not get list of nearby 
places: " + caught.toString());
-                        Crouton.makeText(getActivity(), 
R.string.nearby_server_error, Style.ALERT, nearbyContainer).show();
-                    } else {
-                        super.onCatch(caught);
-                    }
+                    FeedbackUtil.showError(getActivity(), caught);
                     setRefreshingState(false);
                 }
             }.execute();
diff --git a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java 
b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
index a335c41..5d548d3 100644
--- a/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/page/PageActivity.java
@@ -28,12 +28,11 @@
 import org.wikipedia.util.log.L;
 import org.wikipedia.views.ViewUtil;
 import org.wikipedia.views.WikiDrawerLayout;
+import org.wikipedia.zero.WikipediaZeroHandler;
 import org.wikipedia.zero.ZeroMessage;
 
 import com.squareup.otto.Bus;
 import com.squareup.otto.Subscribe;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 
 import android.app.SearchManager;
 import android.content.DialogInterface;
@@ -581,9 +580,9 @@
                 }
                 //is the new title the same as what's already being displayed?
                 if (!frag.getCurrentTab().getBackStack().isEmpty()
-                    && frag.getCurrentTab().getBackStack()
-                           .get(frag.getCurrentTab().getBackStack().size() - 
1).getTitle()
-                           .equals(title)) {
+                        && frag.getCurrentTab().getBackStack()
+                        .get(frag.getCurrentTab().getBackStack().size() - 
1).getTitle()
+                        .equals(title)) {
                     //if we have a section to scroll to, then pass it to the 
fragment
                     if (!TextUtils.isEmpty(title.getFragment())) {
                         frag.scrollToSection(title.getFragment());
@@ -709,18 +708,17 @@
             if (pausedStateOfZero && !latestWikipediaZeroDisposition) {
                 String title = getString(R.string.zero_charged_verbiage);
                 String verbiage = 
getString(R.string.zero_charged_verbiage_extended);
-                
makeWikipediaZeroCrouton(getResources().getColor(R.color.holo_red_dark),
-                                         
getResources().getColor(android.R.color.white),
-                                         title);
+                WikipediaZeroHandler.showZeroBanner(PageActivity.this, title,
+                        getResources().getColor(android.R.color.white),
+                        getResources().getColor(R.color.holo_red_dark));
                 fragmentNavdrawer.setupDynamicItems();
                 showDialogAboutZero(null, title, verbiage);
             } else if ((!pausedStateOfZero || 
!pausedMessageOfZero.equals(latestCarrierMessage))
                        && latestWikipediaZeroDisposition) {
                 String title = latestCarrierMessage.getMsg();
-                int fg = latestCarrierMessage.getFg();
-                int bg = latestCarrierMessage.getBg();
                 String verbiage = getString(R.string.zero_learn_more);
-                makeWikipediaZeroCrouton(bg, fg, title);
+                WikipediaZeroHandler.showZeroBanner(PageActivity.this, title,
+                        latestCarrierMessage.getFg(), 
latestCarrierMessage.getBg());
                 fragmentNavdrawer.setupDynamicItems();
                 showDialogAboutZero(ZERO_ON_NOTICE_PRESENTED, title, verbiage);
             }
@@ -731,23 +729,6 @@
                             ? R.string.zero_search_hint
                             : R.string.search_hint));
         }
-    }
-
-    private void makeWikipediaZeroCrouton(int bgcolor, int fgcolor, String 
verbiage) {
-        final int zeroTextSize = 20;
-        final float croutonHeight = 192.0f;
-        Style style = new Style.Builder()
-                .setBackgroundColorValue(bgcolor)
-                .setGravity(Gravity.CENTER)
-                // .setTextAppearance-driven font size is not being honored, 
so we'll do it manually
-                // Text size in library is in sp
-                .setTextSize(zeroTextSize)
-                .setTextColorValue(fgcolor)
-                // Height size in library is in px
-                .setHeight((int) Math.floor(croutonHeight * 
WikipediaApp.getInstance().getScreenDensity()))
-                .build();
-
-        Crouton.makeText(this, verbiage, style, 
R.id.content_fragment_container).show();
     }
 
     private void showDialogAboutZero(final String prefsKey, String title, 
String message) {
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java 
b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
index b8d2fe6..2fffa76 100755
--- a/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
+++ b/wikipedia/src/main/java/org/wikipedia/page/PageViewFragmentInternal.java
@@ -29,6 +29,7 @@
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.staticdata.MainPageNameData;
 import org.wikipedia.tooltip.ToolTipUtil;
+import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.ThrowableUtil;
 import org.wikipedia.views.ObservableWebView;
 import org.wikipedia.views.SwipeRefreshLayoutWithScroll;
@@ -63,7 +64,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import com.appenguin.onboarding.ToolTip;
 
@@ -640,7 +640,7 @@
         if (requestCode == PageActivity.ACTIVITY_REQUEST_EDIT_SECTION
             && resultCode == EditHandler.RESULT_REFRESH_PAGE) {
             pageLoadStrategy.backFromEditing(data);
-
+            FeedbackUtil.showMessage(getActivity(), 
R.string.edit_saved_successfully);
             // and reload the page...
             displayNewPage(model.getTitleOriginal(), model.getCurEntry(), 
false, false);
         }
@@ -882,7 +882,7 @@
             return;
         }
 
-        Toast.makeText(getActivity(), R.string.toast_saving_page, 
Toast.LENGTH_SHORT).show();
+        FeedbackUtil.showMessage(getActivity(), R.string.toast_saving_page);
         new SavePageTask(getActivity(), model.getTitle(), model.getPage()) {
             @Override
             public void onFinish(Boolean success) {
@@ -894,9 +894,9 @@
                 pageLoadStrategy.setSubState(SUBSTATE_PAGE_SAVED);
 
                 if (success) {
-                    Toast.makeText(getActivity(), R.string.toast_saved_page, 
Toast.LENGTH_LONG).show();
+                    FeedbackUtil.showMessage(getActivity(), 
R.string.toast_saved_page);
                 } else {
-                    Toast.makeText(getActivity(), 
R.string.toast_saved_page_missing_images, Toast.LENGTH_LONG).show();
+                    FeedbackUtil.showMessage(getActivity(), 
R.string.toast_saved_page_missing_images);
                 }
             }
         }.execute();
@@ -937,7 +937,7 @@
     public void refreshPage(boolean saveOnComplete) {
         this.saveOnComplete = saveOnComplete;
         if (saveOnComplete) {
-            Toast.makeText(getActivity(), R.string.toast_refresh_saved_page, 
Toast.LENGTH_LONG).show();
+            FeedbackUtil.showMessage(getActivity(), 
R.string.toast_refresh_saved_page);
         }
         model.setCurEntry(new HistoryEntry(model.getTitle(), 
HistoryEntry.SOURCE_HISTORY));
         displayNewPage(model.getTitle(), model.getCurEntry(), false, false);
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java 
b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java
index 94ba487..7b316e7 100644
--- a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryActivity.java
@@ -16,6 +16,7 @@
 import org.wikipedia.page.PageCache;
 import org.wikipedia.theme.Theme;
 import org.wikipedia.util.ApiUtil;
+import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.GradientUtil;
 import org.wikipedia.views.ViewUtil;
 
@@ -40,8 +41,6 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -247,12 +246,6 @@
     }
 
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        Crouton.cancelAllCroutons();
-    }
-
-    @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putBoolean("controlsShowing", controlsShowing);
@@ -390,7 +383,7 @@
             public void onCatch(Throwable caught) {
                 Log.e(TAG, "Failed to fetch gallery collection.", caught);
                 updateProgressBar(false, true, 0);
-                showError(getString(R.string.error_network_error));
+                FeedbackUtil.showError(GalleryActivity.this, caught);
             }
         }.execute();
     }
@@ -437,16 +430,6 @@
             // Sorry 2.3, you won't get custom transforms between pages.
             galleryPager.setPageTransformer(false, new 
GalleryPagerTransformer());
         }
-    }
-
-    /**
-     * Display an error message in the form of a Crouton.
-     * @param errorStr Error message to show.
-     */
-    public void showError(String errorStr) {
-        supportInvalidateOptionsMenu();
-        Crouton.makeText(GalleryActivity.this, errorStr, Style.ALERT, 
toolbarContainer)
-               .show();
     }
 
     private GalleryItem getCurrentItem() {
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java 
b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
index 909b044..6d2dd85 100644
--- 
a/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
+++ 
b/wikipedia/src/main/java/org/wikipedia/page/gallery/GalleryItemFragment.java
@@ -4,6 +4,7 @@
 import org.wikipedia.R;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.concurrency.SaneAsyncTask;
+import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.ShareUtils;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
@@ -23,7 +24,6 @@
 import android.widget.ImageView;
 import android.widget.MediaController;
 import android.widget.ProgressBar;
-import android.widget.Toast;
 import android.widget.VideoView;
 import com.squareup.picasso.Callback;
 import com.squareup.picasso.Picasso;
@@ -224,7 +224,7 @@
                     loadMedia();
                 } else {
                     updateProgressBar(false, true, 0);
-                    
parentActivity.showError(getString(R.string.error_network_error));
+                    FeedbackUtil.showMessage(getActivity(), 
R.string.error_network_error);
                 }
             }
             @Override
@@ -234,7 +234,7 @@
                     return;
                 }
                 updateProgressBar(false, true, 0);
-                
parentActivity.showError(getString(R.string.error_network_error));
+                FeedbackUtil.showError(getActivity(), caught);
             }
         }.execute();
     }
@@ -294,7 +294,8 @@
                 @Override
                 public boolean onError(MediaPlayer mp, int what, int extra) {
                     updateProgressBar(false, true, 0);
-                    
parentActivity.showError(getString(R.string.gallery_error_video_failed));
+                    FeedbackUtil.showMessage(getActivity(),
+                            R.string.gallery_error_video_failed);
                     videoView.setVisibility(View.GONE);
                     videoThumbnail.setVisibility(View.VISIBLE);
                     videoPlayButton.setVisibility(View.VISIBLE);
@@ -362,7 +363,7 @@
                            return;
                        }
                        updateProgressBar(false, true, 0);
-                       
parentActivity.showError(getString(R.string.gallery_error_draw_failed));
+                       FeedbackUtil.showMessage(getActivity(), 
R.string.gallery_error_draw_failed);
                    }
                });
     }
@@ -435,16 +436,14 @@
                 if (!isAdded()) {
                     return;
                 }
-                Toast.makeText(parentActivity, 
getString(R.string.gallery_save_success),
-                        Toast.LENGTH_SHORT).show();
+                FeedbackUtil.showMessage(parentActivity, 
R.string.gallery_save_success);
             }
             @Override
             public void onCatch(Throwable caught) {
                 if (!isAdded()) {
                     return;
                 }
-                
parentActivity.showError(String.format(getString(R.string.gallery_save_error),
-                                        caught.getLocalizedMessage()));
+                FeedbackUtil.showError(parentActivity, caught);
             }
         }.execute();
     }
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/linkpreview/LinkPreviewDialog.java 
b/wikipedia/src/main/java/org/wikipedia/page/linkpreview/LinkPreviewDialog.java
index 007b042..0b917c7 100755
--- 
a/wikipedia/src/main/java/org/wikipedia/page/linkpreview/LinkPreviewDialog.java
+++ 
b/wikipedia/src/main/java/org/wikipedia/page/linkpreview/LinkPreviewDialog.java
@@ -8,6 +8,7 @@
 import org.wikipedia.history.HistoryEntry;
 import org.wikipedia.page.PageActivity;
 import org.wikipedia.util.ApiUtil;
+import org.wikipedia.util.FeedbackUtil;
 
 import com.squareup.picasso.Callback;
 import com.squareup.picasso.Picasso;
@@ -40,9 +41,6 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import java.util.Map;
-
-import de.keyboardsurfer.android.widget.crouton.Crouton;
-import de.keyboardsurfer.android.widget.crouton.Style;
 
 public class LinkPreviewDialog extends DialogFragment implements 
DialogInterface.OnDismissListener {
     private static final String TAG = "LinkPreviewDialog";
@@ -144,7 +142,8 @@
                 if (result.size() > 0) {
                     layoutPreview((LinkPreviewContents) 
result.values().toArray()[0]);
                 } else {
-                    abortWithError(getString(R.string.error_network_error));
+                    FeedbackUtil.showMessage(getActivity(), 
R.string.error_network_error);
+                    dismiss();
                 }
             }
             @Override
@@ -154,7 +153,8 @@
                     return;
                 }
                 progressBar.setVisibility(View.GONE);
-                abortWithError(getString(R.string.error_network_error));
+                FeedbackUtil.showError(getActivity(), caught);
+                dismiss();
             }
         }.execute();
 
@@ -200,13 +200,6 @@
 
         gestureDetector = new GestureDetectorCompat(dialog.getContext(), new 
PreviewGestureListener());
         return dialog;
-    }
-
-    private void abortWithError(String error) {
-        if (getActivity() != null && getActivity().getWindow().getDecorView() 
!= null) {
-            Crouton.makeText(getActivity(), error, Style.ALERT).show();
-        }
-        dismiss();
     }
 
     private class PreviewGestureListener extends 
GestureDetector.SimpleOnGestureListener {
diff --git 
a/wikipedia/src/main/java/org/wikipedia/random/RandomArticleIdTask.java 
b/wikipedia/src/main/java/org/wikipedia/random/RandomArticleIdTask.java
index db12632..cb78334 100644
--- a/wikipedia/src/main/java/org/wikipedia/random/RandomArticleIdTask.java
+++ b/wikipedia/src/main/java/org/wikipedia/random/RandomArticleIdTask.java
@@ -1,6 +1,5 @@
 package org.wikipedia.random;
 
-import android.content.Context;
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.mediawiki.api.json.Api;
@@ -13,7 +12,7 @@
 public class RandomArticleIdTask extends ApiTask<PageTitle> {
     private Site site;
 
-    public RandomArticleIdTask(Api api, Site site, Context context) {
+    public RandomArticleIdTask(Api api, Site site) {
         super(SINGLE_THREAD, api);
         this.site = site;
     }
@@ -29,13 +28,8 @@
 
     @Override
     public PageTitle processResult(ApiResult result) throws Throwable {
-        try {
-            JSONArray results = 
result.asObject().optJSONObject("query").optJSONArray("random");
-            JSONObject random = (JSONObject)results.get(0);
-
-            return new PageTitle(null, random.getString("title"), site);
-        } catch (Exception e) {
-            return null;
-        }
+        JSONArray results = 
result.asObject().optJSONObject("query").optJSONArray("random");
+        JSONObject random = (JSONObject)results.get(0);
+        return new PageTitle(null, random.getString("title"), site);
     }
 }
\ No newline at end of file
diff --git a/wikipedia/src/main/java/org/wikipedia/random/RandomHandler.java 
b/wikipedia/src/main/java/org/wikipedia/random/RandomHandler.java
index 5c99697..21f9e96 100644
--- a/wikipedia/src/main/java/org/wikipedia/random/RandomHandler.java
+++ b/wikipedia/src/main/java/org/wikipedia/random/RandomHandler.java
@@ -20,6 +20,7 @@
 
     public interface RandomListener {
         void onRandomPageReceived(PageTitle title);
+        void onRandomPageFailed(Throwable caught);
     }
 
     public RandomHandler(View menuItem, View icon, View progressBar, 
RandomListener listener) {
@@ -48,7 +49,7 @@
         Handler randomHandler = new Handler(new Handler.Callback() {
             @Override
             public boolean handleMessage(Message msg) {
-                RandomArticleIdTask randomTask = new 
RandomArticleIdTask(app.getAPIForSite(app.getPrimarySite()), 
app.getPrimarySite(), app) {
+                RandomArticleIdTask randomTask = new 
RandomArticleIdTask(app.getAPIForSite(app.getPrimarySite()), 
app.getPrimarySite()) {
 
                     @Override
                     public void onBeforeExecute() {
@@ -62,12 +63,7 @@
                         }
                         setState(false);
                         Log.d("Wikipedia", "Random article title pulled: " + 
title);
-                        if (title != null) {
-                            randomListener.onRandomPageReceived(title);
-                        } else {
-                            // Pass null page to the listener, so it shows a 
network error.
-                            randomListener.onRandomPageReceived(null);
-                        }
+                        randomListener.onRandomPageReceived(title);
                     }
 
                     @Override
@@ -78,7 +74,7 @@
                         setState(false);
                         Log.d("Wikipedia", "Random article ID retrieval 
failed");
                         curRandomArticleIdTask = null;
-                        randomListener.onRandomPageReceived(null);
+                        randomListener.onRandomPageFailed(caught);
                     }
                 };
                 if (curRandomArticleIdTask != null) {
diff --git 
a/wikipedia/src/main/java/org/wikipedia/savedpages/RefreshPagesHandler.java 
b/wikipedia/src/main/java/org/wikipedia/savedpages/RefreshPagesHandler.java
index 8530ecc..b42f403 100644
--- a/wikipedia/src/main/java/org/wikipedia/savedpages/RefreshPagesHandler.java
+++ b/wikipedia/src/main/java/org/wikipedia/savedpages/RefreshPagesHandler.java
@@ -5,7 +5,6 @@
 import android.content.DialogInterface;
 import android.support.v7.app.AlertDialog;
 import android.util.Log;
-import android.widget.Toast;
 import org.wikipedia.R;
 import org.wikipedia.page.Section;
 
@@ -86,7 +85,6 @@
                         Log.d("Wikipedia", "Count is " + savedPagesCompleted + 
" of " + savedPages.size());
                         if (savedPagesCompleted == savedPages.size()) {
                             progressDialog.dismiss();
-                            Toast.makeText(context, 
R.string.saved_pages_refresh_pages_complete, Toast.LENGTH_LONG).show();
                         }
                         Log.d("Wikipedia", "refreshing end: " + 
savedPage.getTitle().getDisplayText());
                     }
diff --git 
a/wikipedia/src/main/java/org/wikipedia/savedpages/SavedPagesFragment.java 
b/wikipedia/src/main/java/org/wikipedia/savedpages/SavedPagesFragment.java
index af5ebd3..36f4b1b 100644
--- a/wikipedia/src/main/java/org/wikipedia/savedpages/SavedPagesFragment.java
+++ b/wikipedia/src/main/java/org/wikipedia/savedpages/SavedPagesFragment.java
@@ -28,7 +28,6 @@
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import com.squareup.picasso.Picasso;
 
@@ -38,6 +37,7 @@
 import org.wikipedia.history.HistoryEntry;
 import org.wikipedia.page.PageActivity;
 import org.wikipedia.pageimages.PageImage;
+import org.wikipedia.util.FeedbackUtil;
 
 import java.util.ArrayList;
 
@@ -134,7 +134,11 @@
                                 new DeleteSavedPageTask(getActivity(), page) {
                                     @Override
                                     public void onFinish(Boolean result) {
-                                        Toast.makeText(getActivity(), 
R.string.toast_saved_page_deleted, Toast.LENGTH_SHORT).show();
+                                        if (!isAdded()) {
+                                            return;
+                                        }
+                                        FeedbackUtil.showMessage(getActivity(),
+                                                
R.string.toast_saved_page_deleted);
                                     }
                                 }.execute();
                             }
@@ -351,8 +355,12 @@
                 new DeleteAllSavedPagesTask(getActivity()) {
                     @Override
                     public void onFinish(Void v) {
+                        if (!isAdded()) {
+                            return;
+                        }
                         entryFilter.setVisibility(View.GONE);
-                        Toast.makeText(getActivity(), 
R.string.toast_saved_page_deleted, Toast.LENGTH_SHORT).show();
+                        FeedbackUtil.showMessage(getActivity(),
+                                R.string.toast_saved_page_deleted);
                     }
                 }.execute();
             }
diff --git 
a/wikipedia/src/main/java/org/wikipedia/search/SearchResultsFragment.java 
b/wikipedia/src/main/java/org/wikipedia/search/SearchResultsFragment.java
index 8109c66..283ebf4 100644
--- a/wikipedia/src/main/java/org/wikipedia/search/SearchResultsFragment.java
+++ b/wikipedia/src/main/java/org/wikipedia/search/SearchResultsFragment.java
@@ -5,7 +5,7 @@
 import org.wikipedia.R;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.page.PageActivity;
-import org.wikipedia.util.ThrowableUtil;
+import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.views.WikiErrorView;
 
 import com.squareup.picasso.Picasso;
@@ -22,7 +22,6 @@
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import java.text.Collator;
 import java.util.ArrayList;
@@ -335,10 +334,8 @@
                 searchFragment.getFunnel().searchError(true, timeToDisplay);
                 ((PageActivity)getActivity()).updateProgressBar(false, true, 
0);
 
-                // since this is a follow-up search just toast
-                Toast.makeText(getActivity(),
-                               ThrowableUtil.getAppError(getActivity(), 
caught).getError(),
-                               Toast.LENGTH_SHORT).show();
+                // since this is a follow-up search just show a message
+                FeedbackUtil.showError(getView(), caught);
             }
         }.execute();
     }
diff --git a/wikipedia/src/main/java/org/wikipedia/settings/AboutActivity.java 
b/wikipedia/src/main/java/org/wikipedia/settings/AboutActivity.java
index 45f4fb7..147f5f8 100644
--- a/wikipedia/src/main/java/org/wikipedia/settings/AboutActivity.java
+++ b/wikipedia/src/main/java/org/wikipedia/settings/AboutActivity.java
@@ -1,6 +1,6 @@
 package org.wikipedia.settings;
 
-import android.content.Context;
+import android.app.Activity;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.text.Html;
@@ -11,7 +11,6 @@
 import android.widget.ImageView;
 import android.widget.ScrollView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 import org.wikipedia.BuildConfig;
 import org.wikipedia.R;
@@ -19,6 +18,7 @@
 import org.wikipedia.activity.ThemedActionBarActivity;
 import org.wikipedia.Utils;
 import org.wikipedia.WikipediaApp;
+import org.wikipedia.util.FeedbackUtil;
 
 public class AboutActivity extends ThemedActionBarActivity {
     private static final String KEY_SCROLL_X = "KEY_SCROLL_X";
@@ -93,11 +93,11 @@
     private static class AboutLogoClickListener implements 
View.OnClickListener {
         private static final int SECRET_CLICK_LIMIT = 7;
 
-        private final Context mContext;
+        private final Activity mActivity;
         private int mSecretClickCount;
 
-        public AboutLogoClickListener(Context context) {
-            mContext = context;
+        public AboutLogoClickListener(Activity activity) {
+            mActivity = activity;
         }
 
         @Override
@@ -118,23 +118,12 @@
         }
 
         private void showSettingEnabledMessage() {
-            showMessage(R.string.show_developer_settings_enabled);
+            FeedbackUtil.showMessage(mActivity, 
R.string.show_developer_settings_enabled);
         }
 
         private void showSettingAlreadyEnabledMessage() {
-            showMessage(R.string.show_developer_settings_already_enabled);
-        }
-
-        private void showMessage(int id) {
-            showMessage(getString(id));
-        }
-
-        private void showMessage(String message) {
-            Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
-        }
-
-        private String getString(int id) {
-            return mContext.getString(id);
+            FeedbackUtil.showMessage(mActivity,
+                    R.string.show_developer_settings_already_enabled);
         }
     }
 }
diff --git a/wikipedia/src/main/java/org/wikipedia/util/FeedbackUtil.java 
b/wikipedia/src/main/java/org/wikipedia/util/FeedbackUtil.java
new file mode 100644
index 0000000..55f5e25
--- /dev/null
+++ b/wikipedia/src/main/java/org/wikipedia/util/FeedbackUtil.java
@@ -0,0 +1,48 @@
+package org.wikipedia.util;
+
+import android.app.Activity;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+
+import org.wikipedia.R;
+import org.wikipedia.page.PageActivity;
+
+public final class FeedbackUtil {
+
+    public static void showError(View containerView, Throwable e) {
+        ThrowableUtil.AppError error = 
ThrowableUtil.getAppError(containerView.getContext(), e);
+        Snackbar.make(containerView, error.getError(), 
Snackbar.LENGTH_LONG).show();
+    }
+
+    private static void showMessage(View containerView, CharSequence text) {
+        Snackbar.make(containerView, text, Snackbar.LENGTH_LONG).show();
+    }
+
+    private static void showMessage(View containerView, int resId) {
+        showMessage(containerView, 
containerView.getResources().getString(resId));
+    }
+
+    public static void showMessage(Activity activity, int resId) {
+        showMessage(findBestView(activity), activity.getString(resId));
+    }
+
+    public static void showMessage(Activity activity, CharSequence text) {
+        showMessage(findBestView(activity), text);
+    }
+
+    public static void showError(Activity activity, Throwable e) {
+        showError(findBestView(activity), e);
+    }
+
+    private static View findBestView(Activity activity) {
+        if (activity instanceof PageActivity
+                && ((PageActivity) activity).getCurPageFragment() != null) {
+            return activity.findViewById(R.id.page_contents_container);
+        } else {
+            return activity.findViewById(android.R.id.content);
+        }
+    }
+
+    private FeedbackUtil() {
+    }
+}
\ No newline at end of file
diff --git 
a/wikipedia/src/main/java/org/wikipedia/zero/WikipediaZeroHandler.java 
b/wikipedia/src/main/java/org/wikipedia/zero/WikipediaZeroHandler.java
index 2402df7..a2f6505 100644
--- a/wikipedia/src/main/java/org/wikipedia/zero/WikipediaZeroHandler.java
+++ b/wikipedia/src/main/java/org/wikipedia/zero/WikipediaZeroHandler.java
@@ -1,10 +1,13 @@
 package org.wikipedia.zero;
 
+import org.wikipedia.R;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.events.WikipediaZeroStateChangeEvent;
 import org.wikipedia.random.RandomArticleIdTask;
 import org.mediawiki.api.json.ApiResult;
 import org.mediawiki.api.json.OnHeaderCheckListener;
+
+import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -14,13 +17,26 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
 import android.util.Log;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
 import java.net.URL;
 
 public class WikipediaZeroHandler extends BroadcastReceiver implements 
OnHeaderCheckListener {
-
-    private WikipediaApp app;
     private static final boolean WIKIPEDIA_ZERO_DEV_MODE_ON = true;
+    /**
+     * Size of the text, in sp, of the Zero banner text.
+     */
+    private static final int BANNER_TEXT_SIZE = 20;
+    /**
+     * Height of the Zero banner, in pixels, that will pop up from the bottom 
of the screen.
+     */
+    private static final int BANNER_HEIGHT = (int) (192 * 
WikipediaApp.getInstance().getScreenDensity());
 
     public WikipediaZeroHandler(WikipediaApp app) {
         this.app = app;
@@ -31,6 +47,7 @@
         }
     }
 
+    private WikipediaApp app;
     private boolean zeroEnabled = false;
     public boolean isZeroEnabled() {
         return zeroEnabled;
@@ -47,6 +64,20 @@
     private RandomArticleIdTask curRandomArticleIdTask;
     private static final int MESSAGE_ZERO_RND = 1;
     private static final int MESSAGE_ZERO_CS = 2;
+
+    public static void showZeroBanner(@NonNull Activity activity, @NonNull 
String text,
+                                      @ColorInt int foreColor, @ColorInt int 
backColor) {
+        Snackbar snackbar = 
Snackbar.make(activity.findViewById(R.id.page_contents_container), text,
+                Snackbar.LENGTH_LONG);
+        ViewGroup rootView = (ViewGroup) snackbar.getView();
+        TextView textView = (TextView) 
rootView.findViewById(R.id.snackbar_text);
+        rootView.setBackgroundColor(backColor);
+        textView.setTextColor(foreColor);
+        textView.setTextSize(BANNER_TEXT_SIZE);
+        textView.setGravity(Gravity.CENTER_HORIZONTAL);
+        rootView.setMinimumHeight(BANNER_HEIGHT);
+        snackbar.show();
+    }
 
     @Override
     public void onHeaderCheck(final ApiResult result, final URL apiURL) {
@@ -97,7 +128,7 @@
                 Handler wikipediaZeroRandomHandler = new Handler(new 
Handler.Callback() {
                     @Override
                     public boolean handleMessage(Message msg) {
-                        RandomArticleIdTask randomTask = new 
RandomArticleIdTask(app.getAPIForSite(app.getPrimarySite()), 
app.getPrimarySite(), context) {
+                        RandomArticleIdTask randomTask = new 
RandomArticleIdTask(app.getAPIForSite(app.getPrimarySite()), 
app.getPrimarySite()) {
                             @Override
                             public void onCatch(Throwable caught) {
                                 // oh snap

-- 
To view, visit https://gerrit.wikimedia.org/r/225896
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I4314a8d219a90415636cfaca867152b9251e551d
Gerrit-PatchSet: 10
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <[email protected]>
Gerrit-Reviewer: BearND <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to