Android Open Source - google-authenticator-android Authenticator Activity






From Project

Back to project page google-authenticator-android.

License

The source code is released under:

Apache License

If you think the Android project google-authenticator-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2009 Google Inc. All Rights Reserved.
 *//from  w  w  w  . j a v  a2 s .  com
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.apps.authenticator;

import com.google.android.apps.authenticator.AccountDb.OtpType;
import com.google.android.apps.authenticator.dataimport.ImportController;
import com.google.android.apps.authenticator.howitworks.IntroEnterPasswordActivity;
import com.google.android.apps.authenticator.testability.DependencyInjector;
import com.google.android.apps.authenticator.testability.TestableActivity;
import com.google.android.apps.authenticator2.R;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.text.ClipboardManager;
import android.text.Html;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.webkit.WebView;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.Serializable;
import java.util.ArrayList;

/**
 * The main activity that displays usernames and codes
 *
 * @author sweis@google.com (Steve Weis)
 * @author adhintz@google.com (Drew Hintz)
 * @author cemp@google.com (Cem Paya)
 * @author klyubin@google.com (Alex Klyubin)
 */
public class AuthenticatorActivity extends TestableActivity {

  /** The tag for log messages */
  private static final String LOCAL_TAG = "AuthenticatorActivity";
  private static final long VIBRATE_DURATION = 200L;

  /** Frequency (milliseconds) with which TOTP countdown indicators are updated. */
  private static final long TOTP_COUNTDOWN_REFRESH_PERIOD = 100;

  /**
   * Minimum amount of time (milliseconds) that has to elapse from the moment a HOTP code is
   * generated for an account until the moment the next code can be generated for the account.
   * This is to prevent the user from generating too many HOTP codes in a short period of time.
   */
  private static final long HOTP_MIN_TIME_INTERVAL_BETWEEN_CODES = 5000;

  /**
   * The maximum amount of time (milliseconds) for which a HOTP code is displayed after it's been
   * generated.
   */
  private static final long HOTP_DISPLAY_TIMEOUT = 2 * 60 * 1000;

  // @VisibleForTesting
  static final int DIALOG_ID_UNINSTALL_OLD_APP = 12;

  // @VisibleForTesting
  static final int DIALOG_ID_SAVE_KEY = 13;

  /**
   * Intent action to that tells this Activity to initiate the scanning of barcode to add an
   * account.
   */
  // @VisibleForTesting
  static final String ACTION_SCAN_BARCODE =
      AuthenticatorActivity.class.getName() + ".ScanBarcode";

  private View mContentNoAccounts;
  private View mContentAccountsPresent;
  private TextView mEnterPinPrompt;
  private ListView mUserList;
  private PinListAdapter mUserAdapter;
  private PinInfo[] mUsers = {};

  /** Counter used for generating TOTP verification codes. */
  private TotpCounter mTotpCounter;

  /** Clock used for generating TOTP verification codes. */
  private TotpClock mTotpClock;

  /**
   * Task that periodically notifies this activity about the amount of time remaining until
   * the TOTP codes refresh. The task also notifies this activity when TOTP codes refresh.
   */
  private TotpCountdownTask mTotpCountdownTask;

  /**
   * Phase of TOTP countdown indicators. The phase is in {@code [0, 1]} with {@code 1} meaning
   * full time step remaining until the code refreshes, and {@code 0} meaning the code is refreshing
   * right now.
   */
  private double mTotpCountdownPhase;
  private AccountDb mAccountDb;
  private OtpSource mOtpProvider;

  /**
   * Key under which the {@link #mOldAppUninstallIntent} is stored in the instance state
   * {@link Bundle}.
   */
  private static final String KEY_OLD_APP_UNINSTALL_INTENT = "oldAppUninstallIntent";

  /**
   * {@link Intent} for uninstalling the "old" app or {@code null} if not known/available.
   *
   * <p>
   * Note: this field is persisted in the instance state {@link Bundle}. We need to resolve to this
   * error-prone mechanism because showDialog on Eclair doesn't take parameters. Once Froyo is
   * the minimum targetted SDK, this contrived code can be removed.
   */
  private Intent mOldAppUninstallIntent;

  /** Whether the importing of data from the "old" app has been started and has not yet finished. */
  private boolean mDataImportInProgress;

  /**
   * Key under which the {@link #mSaveKeyDialogParams} is stored in the instance state
   * {@link Bundle}.
   */
  private static final String KEY_SAVE_KEY_DIALOG_PARAMS = "saveKeyDialogParams";

  /**
   * Parameters to the save key dialog (DIALOG_ID_SAVE_KEY).
   *
   * <p>
   * Note: this field is persisted in the instance state {@link Bundle}. We need to resolve to this
   * error-prone mechanism because showDialog on Eclair doesn't take parameters. Once Froyo is
   * the minimum targetted SDK, this contrived code can be removed.
   */
  private SaveKeyDialogParams mSaveKeyDialogParams;

  /**
   * Whether this activity is currently displaying a confirmation prompt in response to the
   * "save key" Intent.
   */
  private boolean mSaveKeyIntentConfirmationInProgress;

  private static final String OTP_SCHEME = "otpauth";
  private static final String TOTP = "totp"; // time-based
  private static final String HOTP = "hotp"; // counter-based
  private static final String SECRET_PARAM = "secret";
  private static final String COUNTER_PARAM = "counter";
  // @VisibleForTesting
  public static final int CHECK_KEY_VALUE_ID = 0;
  // @VisibleForTesting
  public static final int RENAME_ID = 1;
  // @VisibleForTesting
  public static final int REMOVE_ID = 2;
  // @VisibleForTesting
  static final int COPY_TO_CLIPBOARD_ID = 3;
  // @VisibleForTesting
  static final int SCAN_REQUEST = 31337;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mAccountDb = DependencyInjector.getAccountDb();
    mOtpProvider = DependencyInjector.getOtpProvider();

    // Use a different (longer) title from the one that's declared in the manifest (and the one that
    // the Android launcher displays).
    setTitle(R.string.app_name);

    mTotpCounter = mOtpProvider.getTotpCounter();
    mTotpClock = mOtpProvider.getTotpClock();

    setContentView(R.layout.main);

    // restore state on screen rotation
    Object savedState = getLastNonConfigurationInstance();
    if (savedState != null) {
      mUsers = (PinInfo[]) savedState;
      // Re-enable the Get Code buttons on all HOTP accounts, otherwise they'll stay disabled.
      for (PinInfo account : mUsers) {
        if (account.isHotp) {
          account.hotpCodeGenerationAllowed = true;
        }
      }
    }

    if (savedInstanceState != null) {
      mOldAppUninstallIntent = savedInstanceState.getParcelable(KEY_OLD_APP_UNINSTALL_INTENT);
      mSaveKeyDialogParams =
          (SaveKeyDialogParams) savedInstanceState.getSerializable(KEY_SAVE_KEY_DIALOG_PARAMS);
    }

    mUserList = (ListView) findViewById(R.id.user_list);
    mContentNoAccounts = findViewById(R.id.content_no_accounts);
    mContentAccountsPresent = findViewById(R.id.content_accounts_present);
    mContentNoAccounts.setVisibility((mUsers.length > 0) ? View.GONE : View.VISIBLE);
    mContentAccountsPresent.setVisibility((mUsers.length > 0) ? View.VISIBLE : View.GONE);
    TextView noAccountsPromptDetails = (TextView) findViewById(R.id.details);
    noAccountsPromptDetails.setText(
        Html.fromHtml(getString(R.string.welcome_page_details)));

    findViewById(R.id.how_it_works_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        displayHowItWorksInstructions();
      }
    });
    findViewById(R.id.add_account_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        addAccount();
      }
    });
    mEnterPinPrompt = (TextView) findViewById(R.id.enter_pin_prompt);

    mUserAdapter = new PinListAdapter(this, R.layout.user_row, mUsers);

    mUserList.setVisibility(View.GONE);
    mUserList.setAdapter(mUserAdapter);
    mUserList.setOnItemClickListener(new OnItemClickListener(){
        @Override
        public void onItemClick(AdapterView<?> unusedParent, View row,
                                int unusedPosition, long unusedId) {
            NextOtpButtonListener clickListener = (NextOtpButtonListener) row.getTag();
            View nextOtp = row.findViewById(R.id.next_otp);
            if ((clickListener != null) && nextOtp.isEnabled()){
                clickListener.onClick(row);
            }
            mUserList.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    });

    if (savedInstanceState == null) {
      // This is the first time this Activity is starting (i.e., not restoring previous state which
      // was saved, for example, due to orientation change)
      DependencyInjector.getOptionalFeatures().onAuthenticatorActivityCreated(this);
      importDataFromOldAppIfNecessary();
      handleIntent(getIntent());
    }
  }

  /**
   * Reacts to the {@link Intent} that started this activity or arrived to this activity without
   * restarting it (i.e., arrived via {@link #onNewIntent(Intent)}). Does nothing if the provided
   * intent is {@code null}.
   */
  private void handleIntent(Intent intent) {
    if (intent == null) {
      return;
    }

    String action = intent.getAction();
    if (action == null) {
      return;
    }

    if (ACTION_SCAN_BARCODE.equals(action)) {
      scanBarcode();
    } else if (intent.getData() != null) {
      interpretScanResult(intent.getData(), true);
    }
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putParcelable(KEY_OLD_APP_UNINSTALL_INTENT, mOldAppUninstallIntent);
    outState.putSerializable(KEY_SAVE_KEY_DIALOG_PARAMS, mSaveKeyDialogParams);
  }

  @Override
  public Object onRetainNonConfigurationInstance() {
    return mUsers;  // save state of users and currently displayed PINs
  }

  // Because this activity is marked as singleTop, new launch intents will be
  // delivered via this API instead of onResume().
  // Override here to catch otpauth:// URL being opened from QR code reader.
  @Override
  protected void onNewIntent(Intent intent) {
    Log.i(getString(R.string.app_name), LOCAL_TAG + ": onNewIntent");
    handleIntent(intent);
  }

  @Override
  protected void onStart() {
    super.onStart();

    updateCodesAndStartTotpCountdownTask();
  }

  @Override
  protected void onResume() {
    super.onResume();
    Log.i(getString(R.string.app_name), LOCAL_TAG + ": onResume");

    importDataFromOldAppIfNecessary();
  }

  @Override
  protected void onStop() {
    stopTotpCountdownTask();

    super.onStop();
  }

  private void updateCodesAndStartTotpCountdownTask() {
    stopTotpCountdownTask();

    mTotpCountdownTask =
        new TotpCountdownTask(mTotpCounter, mTotpClock, TOTP_COUNTDOWN_REFRESH_PERIOD);
    mTotpCountdownTask.setListener(new TotpCountdownTask.Listener() {
      @Override
      public void onTotpCountdown(long millisRemaining) {
        if (isFinishing()) {
          // No need to reach to this even because the Activity is finishing anyway
          return;
        }
        setTotpCountdownPhaseFromTimeTillNextValue(millisRemaining);
      }

      @Override
      public void onTotpCounterValueChanged() {
        if (isFinishing()) {
          // No need to reach to this even because the Activity is finishing anyway
          return;
        }
        refreshVerificationCodes();
      }
    });

    mTotpCountdownTask.startAndNotifyListener();
  }

  private void stopTotpCountdownTask() {
    if (mTotpCountdownTask != null) {
      mTotpCountdownTask.stop();
      mTotpCountdownTask = null;
    }
  }

  /** Display list of user emails and updated pin codes. */
  protected void refreshUserList() {
    refreshUserList(false);
  }

  private void setTotpCountdownPhase(double phase) {
    mTotpCountdownPhase = phase;
    updateCountdownIndicators();
  }

  private void setTotpCountdownPhaseFromTimeTillNextValue(long millisRemaining) {
    setTotpCountdownPhase(
        ((double) millisRemaining) / Utilities.secondsToMillis(mTotpCounter.getTimeStep()));
  }

  private void refreshVerificationCodes() {
    refreshUserList();
    setTotpCountdownPhase(1.0);
  }

  private void updateCountdownIndicators() {
    for (int i = 0, len = mUserList.getChildCount(); i < len; i++) {
      View listEntry = mUserList.getChildAt(i);
      CountdownIndicator indicator =
          (CountdownIndicator) listEntry.findViewById(R.id.countdown_icon);
      if (indicator != null) {
        indicator.setPhase(mTotpCountdownPhase);
      }
    }
  }

  /**
   * Display list of user emails and updated pin codes.
   *
   * @param isAccountModified if true, force full refresh
   */
  // @VisibleForTesting
  public void refreshUserList(boolean isAccountModified) {
    ArrayList<String> usernames = new ArrayList<String>();
    mAccountDb.getNames(usernames);

    int userCount = usernames.size();

    if (userCount > 0) {
      boolean newListRequired = isAccountModified || mUsers.length != userCount;
      if (newListRequired) {
        mUsers = new PinInfo[userCount];
      }

      for (int i = 0; i < userCount; ++i) {
        String user = usernames.get(i);
        try {
          computeAndDisplayPin(user, i, false);
        } catch (OtpSourceException ignored) {}
      }

      if (newListRequired) {
        // Make the list display the data from the newly created array of accounts
        // This forces the list to scroll to top.
        mUserAdapter = new PinListAdapter(this, R.layout.user_row, mUsers);
        mUserList.setAdapter(mUserAdapter);
      }

      mUserAdapter.notifyDataSetChanged();

      if (mUserList.getVisibility() != View.VISIBLE) {
        mUserList.setVisibility(View.VISIBLE);
        registerForContextMenu(mUserList);
      }
    } else {
      mUsers = new PinInfo[0]; // clear any existing user PIN state
      mUserList.setVisibility(View.GONE);
    }

    // Display the list of accounts if there are accounts, otherwise display a
    // different layout explaining the user how this app works and providing the user with an easy
    // way to add an account.
    mContentNoAccounts.setVisibility((mUsers.length > 0) ? View.GONE : View.VISIBLE);
    mContentAccountsPresent.setVisibility((mUsers.length > 0) ? View.VISIBLE : View.GONE);
  }

  /**
   * Computes the PIN and saves it in mUsers. This currently runs in the UI
   * thread so it should not take more than a second or so. If necessary, we can
   * move the computation to a background thread.
   *
   * @param user the user email to display with the PIN
   * @param position the index for the screen of this user and PIN
   * @param computeHotp true if we should increment counter and display new hotp
   */
  public void computeAndDisplayPin(String user, int position,
      boolean computeHotp) throws OtpSourceException {

    PinInfo currentPin;
    if (mUsers[position] != null) {
      currentPin = mUsers[position]; // existing PinInfo, so we'll update it
    } else {
      currentPin = new PinInfo();
      currentPin.pin = getString(R.string.empty_pin);
      currentPin.hotpCodeGenerationAllowed = true;
    }

    OtpType type = mAccountDb.getType(user);
    currentPin.isHotp = (type == OtpType.HOTP);

    currentPin.user = user;

    if (!currentPin.isHotp || computeHotp) {
      // Always safe to recompute, because this code path is only
      // reached if the account is:
      // - Time-based, in which case getNextCode() does not change state.
      // - Counter-based (HOTP) and computeHotp is true.
      currentPin.pin = mOtpProvider.getNextCode(user);
      currentPin.hotpCodeGenerationAllowed = true;
    }

    mUsers[position] = currentPin;
  }

  /**
   * Parses a secret value from a URI. The format will be:
   *
   * otpauth://totp/user@example.com?secret=FFF...
   * otpauth://hotp/user@example.com?secret=FFF...&counter=123
   *
   * @param uri The URI containing the secret key
   * @param confirmBeforeSave a boolean to indicate if the user should be
   *                          prompted for confirmation before updating the otp
   *                          account information.
   */
  private void parseSecret(Uri uri, boolean confirmBeforeSave) {
    final String scheme = uri.getScheme().toLowerCase();
    final String path = uri.getPath();
    final String authority = uri.getAuthority();
    final String user;
    final String secret;
    final OtpType type;
    final Integer counter;

    if (!OTP_SCHEME.equals(scheme)) {
      Log.e(getString(R.string.app_name), LOCAL_TAG + ": Invalid or missing scheme in uri");
      showDialog(Utilities.INVALID_QR_CODE);
      return;
    }

    if (TOTP.equals(authority)) {
      type = OtpType.TOTP;
      counter = AccountDb.DEFAULT_HOTP_COUNTER; // only interesting for HOTP
    } else if (HOTP.equals(authority)) {
      type = OtpType.HOTP;
      String counterParameter = uri.getQueryParameter(COUNTER_PARAM);
      if (counterParameter != null) {
        try {
          counter = Integer.parseInt(counterParameter);
        } catch (NumberFormatException e) {
          Log.e(getString(R.string.app_name), LOCAL_TAG + ": Invalid counter in uri");
          showDialog(Utilities.INVALID_QR_CODE);
          return;
        }
      } else {
        counter = AccountDb.DEFAULT_HOTP_COUNTER;
      }
    } else {
      Log.e(getString(R.string.app_name), LOCAL_TAG + ": Invalid or missing authority in uri");
      showDialog(Utilities.INVALID_QR_CODE);
      return;
    }

    user = validateAndGetUserInPath(path);
    if (user == null) {
      Log.e(getString(R.string.app_name), LOCAL_TAG + ": Missing user id in uri");
      showDialog(Utilities.INVALID_QR_CODE);
      return;
    }

    secret = uri.getQueryParameter(SECRET_PARAM);

    if (secret == null || secret.length() == 0) {
      Log.e(getString(R.string.app_name), LOCAL_TAG +
          ": Secret key not found in URI");
      showDialog(Utilities.INVALID_SECRET_IN_QR_CODE);
      return;
    }

    if (AccountDb.getSigningOracle(secret) == null) {
      Log.e(getString(R.string.app_name), LOCAL_TAG + ": Invalid secret key");
      showDialog(Utilities.INVALID_SECRET_IN_QR_CODE);
      return;
    }

    if (secret.equals(mAccountDb.getSecret(user)) &&
        counter == mAccountDb.getCounter(user) &&
        type == mAccountDb.getType(user)) {
      return;  // nothing to update.
    }

    if (confirmBeforeSave) {
      mSaveKeyDialogParams = new SaveKeyDialogParams(user, secret, type, counter);
      showDialog(DIALOG_ID_SAVE_KEY);
    } else {
      saveSecretAndRefreshUserList(user, secret, null, type, counter);
    }
  }

  private static String validateAndGetUserInPath(String path) {
    if (path == null || !path.startsWith("/")) {
      return null;
    }
    // path is "/user", so remove leading "/", and trailing white spaces
    String user = path.substring(1).trim();
    if (user.length() == 0) {
      return null; // only white spaces.
    }
    return user;
  }

  /**
   * Saves the secret key to local storage on the phone and updates the displayed account list.
   *
   * @param user the user email address. When editing, the new user email.
   * @param secret the secret key
   * @param originalUser If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   */
  private void saveSecretAndRefreshUserList(String user, String secret,
      String originalUser, OtpType type, Integer counter) {
    if (saveSecret(this, user, secret, originalUser, type, counter)) {
      refreshUserList(true);
    }
  }

  /**
   * Saves the secret key to local storage on the phone.
   *
   * @param user the user email address. When editing, the new user email.
   * @param secret the secret key
   * @param originalUser If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   *
   * @return {@code true} if the secret was saved, {@code false} otherwise.
   */
  static boolean saveSecret(Context context, String user, String secret,
                         String originalUser, OtpType type, Integer counter) {
    if (originalUser == null) {  // new user account
      originalUser = user;
    }
    if (secret != null) {
      AccountDb accountDb = DependencyInjector.getAccountDb();
      accountDb.update(user, secret, originalUser, type, counter);
      DependencyInjector.getOptionalFeatures().onAuthenticatorActivityAccountSaved(context, user);
      // TODO: Consider having a display message that activities can call and it
      //       will present a toast with a uniform duration, and perhaps update
      //       status messages (presuming we have a way to remove them after they
      //       are stale).
      Toast.makeText(context, R.string.secret_saved, Toast.LENGTH_LONG).show();
      ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE))
        .vibrate(VIBRATE_DURATION);
      return true;
    } else {
      Log.e(LOCAL_TAG, "Trying to save an empty secret key");
      Toast.makeText(context, R.string.error_empty_secret, Toast.LENGTH_LONG).show();
      return false;
    }
  }

  /** Converts user list ordinal id to user email */
  private String idToEmail(long id) {
    return mUsers[(int) id].user;
  }

  @Override
  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
    String user = idToEmail(info.id);
    OtpType type = mAccountDb.getType(user);
    menu.setHeaderTitle(user);
    menu.add(0, COPY_TO_CLIPBOARD_ID, 0, R.string.copy_to_clipboard);
    // Option to display the check-code is only available for HOTP accounts.
    if (type == OtpType.HOTP) {
      menu.add(0, CHECK_KEY_VALUE_ID, 0, R.string.check_code_menu_item);
    }
    menu.add(0, RENAME_ID, 0, R.string.rename);
    menu.add(0, REMOVE_ID, 0, R.string.context_menu_remove_account);
  }

  @Override
  public boolean onContextItemSelected(MenuItem item) {
    AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
    Intent intent;
    final String user = idToEmail(info.id); // final so listener can see value
    switch (item.getItemId()) {
      case COPY_TO_CLIPBOARD_ID:
        ClipboardManager clipboard =
          (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
        clipboard.setText(mUsers[(int) info.id].pin);
        return true;
      case CHECK_KEY_VALUE_ID:
        intent = new Intent(Intent.ACTION_VIEW);
        intent.setClass(this, CheckCodeActivity.class);
        intent.putExtra("user", user);
        startActivity(intent);
        return true;
      case RENAME_ID:
        final Context context = this; // final so listener can see value
        final View frame = getLayoutInflater().inflate(R.layout.rename,
            (ViewGroup) findViewById(R.id.rename_root));
        final EditText nameEdit = (EditText) frame.findViewById(R.id.rename_edittext);
        nameEdit.setText(user);
        new AlertDialog.Builder(this)
        .setTitle(String.format(getString(R.string.rename_message), user))
        .setView(frame)
        .setPositiveButton(R.string.submit,
            this.getRenameClickListener(context, user, nameEdit))
        .setNegativeButton(R.string.cancel, null)
        .show();
        return true;
      case REMOVE_ID:
        // Use a WebView to display the prompt because it contains non-trivial markup, such as list
        View promptContentView =
            getLayoutInflater().inflate(R.layout.remove_account_prompt, null, false);
        WebView webView = (WebView) promptContentView.findViewById(R.id.web_view);
        webView.setBackgroundColor(Color.TRANSPARENT);
        // Make the WebView use the same font size as for the mEnterPinPrompt field
        double pixelsPerDip =
            TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()) / 10d;
        webView.getSettings().setDefaultFontSize(
            (int) (mEnterPinPrompt.getTextSize() / pixelsPerDip));
        Utilities.setWebViewHtml(
            webView,
            "<html><body style=\"background-color: transparent;\" text=\"white\">"
                + getString(
                    mAccountDb.isGoogleAccount(user)
                        ? R.string.remove_google_account_dialog_message
                        : R.string.remove_account_dialog_message)
                + "</body></html>");

        new AlertDialog.Builder(this)
          .setTitle(getString(R.string.remove_account_dialog_title, user))
          .setView(promptContentView)
          .setIcon(android.R.drawable.ic_dialog_alert)
          .setPositiveButton(R.string.remove_account_dialog_button_remove,
              new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                  mAccountDb.delete(user);
                  refreshUserList(true);
                }
              }
          )
          .setNegativeButton(R.string.cancel, null)
          .show();
        return true;
      default:
        return super.onContextItemSelected(item);
    }
  }

  private DialogInterface.OnClickListener getRenameClickListener(final Context context,
      final String user, final EditText nameEdit) {
    return new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int whichButton) {
        String newName = nameEdit.getText().toString();
        if (newName != user) {
          if (mAccountDb.nameExists(newName)) {
            Toast.makeText(context, R.string.error_exists, Toast.LENGTH_LONG).show();
          } else {
            saveSecretAndRefreshUserList(newName,
                mAccountDb.getSecret(user), user, mAccountDb.getType(user),
                mAccountDb.getCounter(user));
          }
        }
      }
    };
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
  }

  @Override
  public boolean onMenuItemSelected(int featureId, MenuItem item) {
    switch (item.getItemId()) {
      case R.id.add_account:
        addAccount();
        return true;
      case R.id.how_it_works:
        displayHowItWorksInstructions();
        return true;
      case R.id.settings:
        showSettings();
        return true;
    }

    return super.onMenuItemSelected(featureId, item);
  }

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    Log.i(getString(R.string.app_name), LOCAL_TAG + ": onActivityResult");
    if (requestCode == SCAN_REQUEST && resultCode == Activity.RESULT_OK) {
      // Grab the scan results and convert it into a URI
      String scanResult = (intent != null) ? intent.getStringExtra("SCAN_RESULT") : null;
      Uri uri = (scanResult != null) ? Uri.parse(scanResult) : null;
      interpretScanResult(uri, false);
    }
  }

  private void displayHowItWorksInstructions() {
    startActivity(new Intent(this, IntroEnterPasswordActivity.class));
  }

  private void addAccount() {
    DependencyInjector.getOptionalFeatures().onAuthenticatorActivityAddAccount(this);
  }

  private void scanBarcode() {
    Intent intentScan = new Intent("com.google.zxing.client.android.SCAN");
    intentScan.putExtra("SCAN_MODE", "QR_CODE_MODE");
    intentScan.putExtra("SAVE_HISTORY", false);
    try {
      startActivityForResult(intentScan, SCAN_REQUEST);
    } catch (ActivityNotFoundException error) {
      showDialog(Utilities.DOWNLOAD_DIALOG);
    }
  }

  public static Intent getLaunchIntentActionScanBarcode(Context context) {
    return new Intent(AuthenticatorActivity.ACTION_SCAN_BARCODE)
        .setComponent(new ComponentName(context, AuthenticatorActivity.class));
  }

  private void showSettings() {
    Intent intent = new Intent();
    intent.setClass(this, SettingsActivity.class);
    startActivity(intent);
  }

  /**
   * Interprets the QR code that was scanned by the user.  Decides whether to
   * launch the key provisioning sequence or the OTP seed setting sequence.
   *
   * @param scanResult a URI holding the contents of the QR scan result
   * @param confirmBeforeSave a boolean to indicate if the user should be
   *                          prompted for confirmation before updating the otp
   *                          account information.
   */
  private void interpretScanResult(Uri scanResult, boolean confirmBeforeSave) {
    if (DependencyInjector.getOptionalFeatures().interpretScanResult(this, scanResult)) {
      // Scan result consumed by an optional component
      return;
    }
    // The scan result is expected to be a URL that adds an account.

    // If confirmBeforeSave is true, the user has to confirm/reject the action.
    // We need to ensure that new results are accepted only if the previous ones have been
    // confirmed/rejected by the user. This is to prevent the attacker from sending multiple results
    // in sequence to confuse/DoS the user.
    if (confirmBeforeSave) {
      if (mSaveKeyIntentConfirmationInProgress) {
        Log.w(LOCAL_TAG, "Ignoring save key Intent: previous Intent not yet confirmed by user");
        return;
      }
      // No matter what happens below, we'll show a prompt which, once dismissed, will reset the
      // flag below.
      mSaveKeyIntentConfirmationInProgress = true;
    }

    // Sanity check
    if (scanResult == null) {
      showDialog(Utilities.INVALID_QR_CODE);
      return;
    }

    // See if the URL is an account setup URL containing a shared secret
    if (OTP_SCHEME.equals(scanResult.getScheme()) && scanResult.getAuthority() != null) {
      parseSecret(scanResult, confirmBeforeSave);
    } else {
      showDialog(Utilities.INVALID_QR_CODE);
    }
  }

  /**
   * This method is deprecated in SDK level 8, but we have to use it because the
   * new method, which replaces this one, does not exist before SDK level 8
   */
  @Override
  protected Dialog onCreateDialog(final int id) {
    Dialog dialog = null;
    switch(id) {
      /**
       * Prompt to download ZXing from Market. If Market app is not installed,
       * such as on a development phone, open the HTTPS URI for the ZXing apk.
       */
      case Utilities.DOWNLOAD_DIALOG:
        AlertDialog.Builder dlBuilder = new AlertDialog.Builder(this);
        dlBuilder.setTitle(R.string.install_dialog_title);
        dlBuilder.setMessage(R.string.install_dialog_message);
        dlBuilder.setIcon(android.R.drawable.ic_dialog_alert);
        dlBuilder.setPositiveButton(R.string.install_button,
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int whichButton) {
                Intent intent = new Intent(Intent.ACTION_VIEW,
                                           Uri.parse(Utilities.ZXING_MARKET));
                try {
                  startActivity(intent);
                }
                catch (ActivityNotFoundException e) { // if no Market app
                  intent = new Intent(Intent.ACTION_VIEW,
                                      Uri.parse(Utilities.ZXING_DIRECT));
                  startActivity(intent);
                }
              }
            }
        );
        dlBuilder.setNegativeButton(R.string.cancel, null);
        dialog = dlBuilder.create();
        break;

      case DIALOG_ID_SAVE_KEY:
        final SaveKeyDialogParams saveKeyDialogParams = mSaveKeyDialogParams;
        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.save_key_message)
            .setMessage(saveKeyDialogParams.user)
            .setIcon(android.R.drawable.ic_dialog_alert)
            .setPositiveButton(R.string.ok,
                new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                  saveSecretAndRefreshUserList(
                      saveKeyDialogParams.user,
                      saveKeyDialogParams.secret,
                      null,
                      saveKeyDialogParams.type,
                      saveKeyDialogParams.counter);
                }
              })
            .setNegativeButton(R.string.cancel, null)
            .create();
        // Ensure that whenever this dialog is to be displayed via showDialog, it displays the
        // correct (latest) user/account name. If this dialog is not explicitly removed after it's
        // been dismissed, then next time showDialog is invoked, onCreateDialog will not be invoked
        // and the dialog will display the previous user/account name instead of the current one.
        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
          @Override
          public void onDismiss(DialogInterface dialog) {
            removeDialog(id);
            onSaveKeyIntentConfirmationPromptDismissed();
          }
        });
        break;

      case Utilities.INVALID_QR_CODE:
        dialog = createOkAlertDialog(R.string.error_title, R.string.error_qr,
            android.R.drawable.ic_dialog_alert);
        markDialogAsResultOfSaveKeyIntent(dialog);
        break;

      case Utilities.INVALID_SECRET_IN_QR_CODE:
        dialog = createOkAlertDialog(
            R.string.error_title, R.string.error_uri, android.R.drawable.ic_dialog_alert);
        markDialogAsResultOfSaveKeyIntent(dialog);
        break;

      case DIALOG_ID_UNINSTALL_OLD_APP:
        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.dataimport_import_succeeded_uninstall_dialog_title)
            .setMessage(
                DependencyInjector.getOptionalFeatures().appendDataImportLearnMoreLink(
                    this,
                    getString(R.string.dataimport_import_succeeded_uninstall_dialog_prompt)))
            .setCancelable(true)
            .setPositiveButton(
                R.string.button_uninstall_old_app,
                new DialogInterface.OnClickListener() {
                  @Override
                  public void onClick(DialogInterface dialog, int whichButton) {
                    startActivity(mOldAppUninstallIntent);
                  }
                })
            .setNegativeButton(R.string.cancel, null)
            .create();
        break;

      default:
        dialog =
            DependencyInjector.getOptionalFeatures().onAuthenticatorActivityCreateDialog(this, id);
        if (dialog == null) {
          dialog = super.onCreateDialog(id);
        }
        break;
    }
    return dialog;
  }

  private void markDialogAsResultOfSaveKeyIntent(Dialog dialog) {
    dialog.setOnDismissListener(new OnDismissListener() {
      @Override
      public void onDismiss(DialogInterface dialog) {
        onSaveKeyIntentConfirmationPromptDismissed();
      }
    });
  }

  /**
   * Invoked when a user-visible confirmation prompt for the Intent to add a new account has been
   * dimissed.
   */
  private void onSaveKeyIntentConfirmationPromptDismissed() {
    mSaveKeyIntentConfirmationInProgress = false;
  }

  /**
   * Create dialog with supplied ids; icon is not set if iconId is 0.
   */
  private Dialog createOkAlertDialog(int titleId, int messageId, int iconId) {
    return new AlertDialog.Builder(this)
        .setTitle(titleId)
        .setMessage(messageId)
        .setIcon(iconId)
        .setPositiveButton(R.string.ok, null)
        .create();
  }

  /**
   * A tuple of user, OTP value, and type, that represents a particular user.
   *
   * @author adhintz@google.com (Drew Hintz)
   */
  private static class PinInfo {
    private String pin; // calculated OTP, or a placeholder if not calculated
    private String user;
    private boolean isHotp = false; // used to see if button needs to be displayed

    /** HOTP only: Whether code generation is allowed for this account. */
    private boolean hotpCodeGenerationAllowed;
  }


  /** Scale to use for the text displaying the PIN numbers. */
  private static final float PIN_TEXT_SCALEX_NORMAL = 1.0f;
  /** Underscores are shown slightly smaller. */
  private static final float PIN_TEXT_SCALEX_UNDERSCORE = 0.87f;

  /**
   * Listener for the Button that generates the next OTP value.
   *
   * @author adhintz@google.com (Drew Hintz)
   */
  private class NextOtpButtonListener implements OnClickListener {
    private final Handler mHandler = new Handler();
    private final PinInfo mAccount;

    private NextOtpButtonListener(PinInfo account) {
      mAccount = account;
    }

    @Override
    public void onClick(View v) {
      int position = findAccountPositionInList();
      if (position == -1) {
        throw new RuntimeException("Account not in list: " + mAccount);
      }

      try {
        computeAndDisplayPin(mAccount.user, position, true);
      } catch (OtpSourceException e) {
        DependencyInjector.getOptionalFeatures().onAuthenticatorActivityGetNextOtpFailed(
            AuthenticatorActivity.this, mAccount.user, e);
        return;
      }

      final String pin = mAccount.pin;

      // Temporarily disable code generation for this account
      mAccount.hotpCodeGenerationAllowed = false;
      mUserAdapter.notifyDataSetChanged();
      // The delayed operation below will be invoked once code generation is yet again allowed for
      // this account. The delay is in wall clock time (monotonically increasing) and is thus not
      // susceptible to system time jumps.
      mHandler.postDelayed(
          new Runnable() {
            @Override
            public void run() {
              mAccount.hotpCodeGenerationAllowed = true;
              mUserAdapter.notifyDataSetChanged();
            }
          },
          HOTP_MIN_TIME_INTERVAL_BETWEEN_CODES);
      // The delayed operation below will hide this OTP to prevent the user from seeing this OTP
      // long after it's been generated (and thus hopefully used).
      mHandler.postDelayed(
          new Runnable() {
            @Override
            public void run() {
              if (!pin.equals(mAccount.pin)) {
                return;
              }
              mAccount.pin = getString(R.string.empty_pin);
              mUserAdapter.notifyDataSetChanged();
            }
          },
          HOTP_DISPLAY_TIMEOUT);
    }

    /**
     * Gets the position in the account list of the account this listener is associated with.
     *
     * @return {@code 0}-based position or {@code -1} if the account is not in the list.
     */
    private int findAccountPositionInList() {
      for (int i = 0, len = mUsers.length; i < len; i++) {
        if (mUsers[i] == mAccount) {
          return i;
        }
      }

      return -1;
    }
  }

  /**
   * Displays the list of users and the current OTP values.
   *
   * @author adhintz@google.com (Drew Hintz)
   */
  private class PinListAdapter extends ArrayAdapter<PinInfo>  {

    public PinListAdapter(Context context, int userRowId, PinInfo[] items) {
      super(context, userRowId, items);
    }

    /**
     * Displays the user and OTP for the specified position. For HOTP, displays
     * the button for generating the next OTP value; for TOTP, displays the countdown indicator.
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent){
     LayoutInflater inflater = getLayoutInflater();
     PinInfo currentPin = getItem(position);

     View row;
     if (convertView != null) {
       // Reuse an existing view
       row = convertView;
     } else {
       // Create a new view
       row = inflater.inflate(R.layout.user_row, null);
     }
     TextView pinView = (TextView) row.findViewById(R.id.pin_value);
     TextView userView = (TextView) row.findViewById(R.id.current_user);
     View buttonView = row.findViewById(R.id.next_otp);
     CountdownIndicator countdownIndicator =
         (CountdownIndicator) row.findViewById(R.id.countdown_icon);

     if (currentPin.isHotp) {
       buttonView.setVisibility(View.VISIBLE);
       buttonView.setEnabled(currentPin.hotpCodeGenerationAllowed);
       ((ViewGroup) row).setDescendantFocusability(
           ViewGroup.FOCUS_BLOCK_DESCENDANTS); // makes long press work
       NextOtpButtonListener clickListener = new NextOtpButtonListener(currentPin);
       buttonView.setOnClickListener(clickListener);
       row.setTag(clickListener);

       countdownIndicator.setVisibility(View.GONE);
     } else { // TOTP, so no button needed
       buttonView.setVisibility(View.GONE);
       buttonView.setOnClickListener(null);
       row.setTag(null);

       countdownIndicator.setVisibility(View.VISIBLE);
       countdownIndicator.setPhase(mTotpCountdownPhase);
     }

     if (getString(R.string.empty_pin).equals(currentPin.pin)) {
       pinView.setTextScaleX(PIN_TEXT_SCALEX_UNDERSCORE); // smaller gap between underscores
     } else {
       pinView.setTextScaleX(PIN_TEXT_SCALEX_NORMAL);
     }
     pinView.setText(currentPin.pin);
     userView.setText(currentPin.user);

     return row;
    }
  }

  private void importDataFromOldAppIfNecessary() {
    if (mDataImportInProgress) {
      return;
    }
    mDataImportInProgress = true;
    DependencyInjector.getDataImportController().start(this, new ImportController.Listener() {
      @Override
      public void onOldAppUninstallSuggested(Intent uninstallIntent) {
        if (isFinishing()) {
          return;
        }

        mOldAppUninstallIntent = uninstallIntent;
        showDialog(DIALOG_ID_UNINSTALL_OLD_APP);
      }

      @Override
      public void onDataImported() {
        if (isFinishing()) {
          return;
        }

        refreshUserList(true);

        DependencyInjector.getOptionalFeatures().onDataImportedFromOldApp(
            AuthenticatorActivity.this);
      }

      @Override
      public void onFinished() {
        if (isFinishing()) {
          return;
        }

        mDataImportInProgress = false;
      }
    });
  }

  /**
   * Parameters to the {@link AuthenticatorActivity#DIALOG_ID_SAVE_KEY} dialog.
   */
  private static class SaveKeyDialogParams implements Serializable {
    private final String user;
    private final String secret;
    private final OtpType type;
    private final Integer counter;

    private SaveKeyDialogParams(String user, String secret, OtpType type, Integer counter) {
      this.user = user;
      this.secret = secret;
      this.type = type;
      this.counter = counter;
    }
  }
}




Java Source Code List

com.google.android.apps.authenticator.AccountDb.java
com.google.android.apps.authenticator.AddOtherAccountActivity.java
com.google.android.apps.authenticator.AuthenticatorActivity.java
com.google.android.apps.authenticator.AuthenticatorApplication.java
com.google.android.apps.authenticator.Base32String.java
com.google.android.apps.authenticator.CheckCodeActivity.java
com.google.android.apps.authenticator.CountdownIndicator.java
com.google.android.apps.authenticator.EnterKeyActivity.java
com.google.android.apps.authenticator.FileUtilities.java
com.google.android.apps.authenticator.HexEncoding.java
com.google.android.apps.authenticator.MarketBuildOptionalFeatures.java
com.google.android.apps.authenticator.OptionalFeatures.java
com.google.android.apps.authenticator.OtpGenerationNotPermittedException.java
com.google.android.apps.authenticator.OtpProvider.java
com.google.android.apps.authenticator.OtpSourceException.java
com.google.android.apps.authenticator.OtpSource.java
com.google.android.apps.authenticator.PasscodeGenerator.java
com.google.android.apps.authenticator.Preconditions.java
com.google.android.apps.authenticator.RunOnThisLooperThreadExecutor.java
com.google.android.apps.authenticator.SettingsAboutActivity.java
com.google.android.apps.authenticator.SettingsActivity.java
com.google.android.apps.authenticator.TotpClock.java
com.google.android.apps.authenticator.TotpCountdownTask.java
com.google.android.apps.authenticator.TotpCounter.java
com.google.android.apps.authenticator.UserRowView.java
com.google.android.apps.authenticator.Utilities.java
com.google.android.apps.authenticator.dataimport.ExportServiceBasedImportController.java
com.google.android.apps.authenticator.dataimport.ImportController.java
com.google.android.apps.authenticator.dataimport.Importer.java
com.google.android.apps.authenticator.howitworks.IntroEnterCodeActivity.java
com.google.android.apps.authenticator.howitworks.IntroEnterPasswordActivity.java
com.google.android.apps.authenticator.howitworks.IntroVerifyDeviceActivity.java
com.google.android.apps.authenticator.testability.DependencyInjector.java
com.google.android.apps.authenticator.testability.HttpClientFactory.java
com.google.android.apps.authenticator.testability.SharedPreferencesRenamingDelegatingContext.java
com.google.android.apps.authenticator.testability.StartActivityListener.java
com.google.android.apps.authenticator.testability.TestableActivity.java
com.google.android.apps.authenticator.testability.TestablePreferenceActivity.java
com.google.android.apps.authenticator.timesync.AboutActivity.java
com.google.android.apps.authenticator.timesync.NetworkTimeProvider.java
com.google.android.apps.authenticator.timesync.SettingsTimeCorrectionActivity.java
com.google.android.apps.authenticator.timesync.SyncNowActivity.java
com.google.android.apps.authenticator.timesync.SyncNowController.java
com.google.android.apps.authenticator.wizard.WizardPageActivity.java