chromium/components/externalauth/android/java/src/org/chromium/components/externalauth/UserRecoverableErrorHandler.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.externalauth;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;

import com.google.android.gms.common.GoogleApiAvailability;

import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordUserAction;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Handles situations where Google Play Services encounters a user-recoverable
 * error. This is typically because Google Play Services requires an upgrade.
 * Three "canned" handlers are provided, with suggested use cases as described
 * below.
 * <br>
 * <strong>Silent</strong>: use this handler if the dependency is purely for
 * convenience and a (potentially suboptimal) fallback is available.
 * <br>
 * <strong>SystemNotification</strong>: use this handler if the dependency is
 * for a feature that the user isn't actively trying to access interactively.
 * To avoid excessively nagging the user, only one notification will ever be
 * shown during the lifetime of the process.
 * <br>
 * <strong>ModalDialog</strong>: use this handler if the dependency is
 * for a feature that the user is actively trying to access interactively where
 * the feature cannot function (or would be severely impaired) unless the
 * dependency is satisfied. The dialog will be presented as many times as the
 * user tries to access the feature.
 * <br>
 * If none of these behaviors is suitable, a new behavior can be defined by
 * subclassing this class.
 */
public abstract class UserRecoverableErrorHandler {
    /**
     * Handles the specified error code from Google Play Services.
     * This method must only be called on the UI thread.
     * This method asserts that it is being called on the UI thread, then calls
     * {@link #handle(Context, int)}.
     * @param context the context in which the error was encountered
     * @param errorCode the error code from Google Play Services
     */
    public final void handleError(final Context context, final int errorCode) {
        ThreadUtils.assertOnUiThread();
        handle(context, errorCode);
    }

    /**
     * This method is invoked by {@link #handleError(Context, int)} to do the
     * work appropriate for the subclass on the UI thread.
     * @param context the context in which the error was encountered
     * @param errorCode the error code from Google Play Services
     */
    protected abstract void handle(final Context context, final int errorCode);

    /** A handler that does nothing. */
    public static final class Silent extends UserRecoverableErrorHandler {
        @Override
        protected final void handle(final Context context, final int errorCode) {}
    }

    /**
     * A handler that displays a System Notification. To avoid repeatedly
     * nagging the user, this is done at most one time per application
     * lifecycle. The system notification is shown by calling
     * {@link GoogleApiAvailability#showErrorNotification(int, Context)}.
     * @see GoogleApiAvailability#showErrorNotification(Context, int)
     */
    public static final class SystemNotification extends UserRecoverableErrorHandler {
        /**
         * Tracks whether the notification has yet been shown, used to ensure
         * that the notification is shown at most one time per application
         * lifecycle.
         */
        private static final AtomicBoolean sNotificationShown = new AtomicBoolean(false);

        @Override
        protected void handle(final Context context, final int errorCode) {
            if (!sNotificationShown.getAndSet(true)) {
                return;
            }
            GoogleApiAvailability.getInstance().showErrorNotification(context, errorCode);
        }
    }

    /**
     * A handler that displays a modal dialog. Unlike
     * {@link SystemNotification}, this handler will take action every time it
     * is invoked.
     * @see GoogleApiAvailability#getErrorDialog(Activity, int, int,
     * android.content.DialogInterface.OnCancelListener)
     */
    public static class ModalDialog extends UserRecoverableErrorHandler {
        private static final int NO_RESPONSE_REQUIRED = -1;

        /**
         * This class receives cancel and dismiss events from error dialog and records UMA action
         * when user accepts the option presented in the dialog (usually it means pressing "Update"
         * button). It's not possible to use less obscure ways (like setOnClickListener) here,
         * because the error dialog is created by Google Play Services.
         */
        private static class DialogUserActionRecorder
                implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
            private boolean mCancelled;

            @Override
            public void onCancel(DialogInterface dialogInterface) {
                mCancelled = true;
            }

            @Override
            public void onDismiss(DialogInterface dialogInterface) {
                if (mCancelled) return;
                // Dialog is being dismissed without being cancelled - user accepted dialog action.
                RecordUserAction.record("Signin_Android_GmsUserRecoverableDialogAccepted");
            }

            public static void createAndAttachToDialog(Dialog dialog) {
                DialogUserActionRecorder actionRecorder = new DialogUserActionRecorder();
                dialog.setOnDismissListener(actionRecorder);
                dialog.setOnCancelListener(actionRecorder);
            }
        }

        /**
         * The activity from which to start the dialog and any subsequent
         * actions, and the activity which will receive the response from those
         * actions.
         */
        private final Activity mActivity;

        /** The modal dialog that is shown to the user. */
        private Dialog mDialog;

        /** Whether the dialog can be canceled by the user. */
        private final boolean mCancelable;

        /** Last error code from Google Play Services. */
        private int mErrorCode;

        /**
         * Create a new Modal Dialog handler for the specified activity and error code. The
         * specified activity may be used to launch the dialog via
         * {@link Activity#startActivityForResult(android.content.Intent, int)} and also to receive
         * the result via Activity's protected onActivityResult method.
         *
         * @param activity the activity to use
         * @param cancelable whether the dialog can be canceled by the user
         */
        public ModalDialog(Activity activity, boolean cancelable) {
            mActivity = activity;
            mCancelable = cancelable;
        }

        /**
         * Displays the dialog in a modal manner using
         * {@link GoogleApiAvailability#getErrorDialog(Activity, int, int)}.
         * @param context the context in which the error was encountered
         * @param errorCode the error code from Google Play Services
         */
        @Override
        protected final void handle(final Context context, final int errorCode) {
            // Assume old dialogs generated by the same error handler are obsolete when an error
            // with a different error code is encountered.
            if (mErrorCode != errorCode) {
                cancelDialog();
            }
            if (mDialog == null) {
                mDialog =
                        GoogleApiAvailability.getInstance()
                                .getErrorDialog(mActivity, errorCode, NO_RESPONSE_REQUIRED);
                mErrorCode = errorCode;

                DialogUserActionRecorder.createAndAttachToDialog(mDialog);
            }
            // This can happen if |errorCode| is ConnectionResult.SERVICE_INVALID.
            if (mDialog != null && !mDialog.isShowing()) {
                mDialog.setCancelable(mCancelable);
                mDialog.show();
                RecordUserAction.record("Signin_Android_GmsUserRecoverableDialogShown");
            }
        }

        /** Cancels the dialog. */
        public void cancelDialog() {
            if (mDialog != null) {
                mDialog.cancel();
                mDialog = null;
            }
        }

        /** Checks whether dialog is being shown. */
        public boolean isShowing() {
            return mDialog != null && mDialog.isShowing();
        }
    }
}