content: /*
   * Copyright (c) 2016 Aitor Viana Sanchez
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
   * associated documentation files (the "Software"), to deal in the Software without restriction,
   * including without limitation the rights to use, copy, modify, merge, publish, distribute,
   * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
   * is furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in all copies or
   * substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
   * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
   * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY WHETHER IN AN
   * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
   */
  
  package com.aitorvs.android.fingerlock;
  
  import android.app.Dialog;
  import android.content.Context;
  import android.content.DialogInterface;
  import android.os.Bundle;
  import android.support.annotation.IntRange;
  import android.support.annotation.NonNull;
  import android.support.annotation.Nullable;
  import android.support.v4.app.DialogFragment;
  import android.support.v4.app.Fragment;
  import android.support.v4.app.FragmentActivity;
  import android.support.v4.content.ContextCompat;
  import android.text.TextUtils;
  import android.util.Log;
  import android.view.KeyEvent;
  import android.view.View;
  import android.view.inputmethod.EditorInfo;
  import android.view.inputmethod.InputMethodManager;
  import android.widget.CheckBox;
  import android.widget.EditText;
  import android.widget.ImageView;
  import android.widget.TextView;
  
  import com.afollestad.materialdialogs.DialogAction;
  import com.afollestad.materialdialogs.MaterialDialog;
  import com.afollestad.materialdialogs.internal.MDTintHelper;
  import com.aitorvs.android.fingerlock.dialog.R;
  
  /**
   * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password
   * authentication if fingerprint is not available.
   */
  @SuppressWarnings("ResourceType")
  public class FingerprintDialog extends DialogFragment
          implements TextView.OnEditorActionListener, FingerLockResultCallback {
  
      // Tag to pass fragment request code argument
      private static final String ARG_REQUEST_CODE = "request_code";
  
      // Tag to pass fragment cancelable argument
      private static final String ARG_CANCELABLE = "cancelable";
  
      // Tag to pass fragment key name argument
      private static final String ARG_KEY_NAME = "key_name";
  
      // TAG to put/get params inside bundles
      private static final String TAG_STAGE = "stage";
  
      // fingerlock library object
      private FingerLockApi.FingerLockImpl mFingerLock;
  
      // reference to the caller context
      private Context mContext;
  
      public interface Callback {
          void onFingerprintDialogAuthenticated();
  
          void onFingerprintDialogVerifyPassword(FingerprintDialog dialog, String password);
  
          void onFingerprintDialogStageUpdated(FingerprintDialog dialog, Stage stage);
  
          void onFingerprintDialogCancelled();
      }
  
      static final long ERROR_TIMEOUT_MILLIS = 1600;
      static final long SUCCESS_DELAY_MILLIS = 1300;
      static final String TAG = FingerprintDialog.class.getSimpleName();
  
      private View mFingerprintContent;
      private View mBackupContent;
      private EditText mPassword;
      private CheckBox mUseFingerprintFutureCheckBox;
      private TextView mPasswordDescriptionTextView;
      private TextView mNewFingerprintEnrolledTextView;
      private ImageView mFingerprintIcon;
      private TextView mFingerprintStatus;
      private static InputMethodManager mInputMethodManager;
  
      private Stage mLastStage;
      private Stage mStage = Stage.FINGERPRINT;
      private Callback mCallback;
  
      public FingerprintDialog() {
      }
  
      @Override
      public void onSaveInstanceState(Bundle outState) {
          super.onSaveInstanceState(outState);
          outState.putSerializable(TAG_STAGE, mStage);
      }
  
      @NonNull
      @Override
      public Dialog onCreateDialog(Bundle savedInstanceState) {
          if (getArguments() == null || !getArguments().containsKey(ARG_KEY_NAME))
              throw new IllegalStateException("FingerprintDialog must be shown with show(Activity, String, int).");
          else if (savedInstanceState != null)
              mStage = (Stage) savedInstanceState.getSerializable(TAG_STAGE);
          setCancelable(getArguments().getBoolean(ARG_CANCELABLE, true));
  
          // create the FingerLock library instance
          mFingerLock = FingerLockApi.create();
  
          MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
                  .title(R.string.sign_in)
                  .customView(R.layout.fingerprint_dialog_container, false)
                  .positiveText(android.R.string.cancel)
                  .negativeText(R.string.use_password)
                  .autoDismiss(false)
                  .cancelable(getArguments().getBoolean(ARG_CANCELABLE, true))
                  .onPositive(new MaterialDialog.SingleButtonCallback() {
                      @Override
                      public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) {
                          materialDialog.cancel();
                      }
                  })
                  .onNegative(new MaterialDialog.SingleButtonCallback() {
                      @Override
                      public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) {
                          if (mStage == Stage.FINGERPRINT) {
                              goToBackup(materialDialog);
                          } else {
                              verifyPassword();
                          }
                      }
                  }).build();
  
          final View v = dialog.getCustomView();
          assert v != null;
          mFingerprintContent = v.findViewById(R.id.fingerprint_container);
          mBackupContent = v.findViewById(R.id.backup_container);
          mPassword = (EditText) v.findViewById(R.id.password);
          mPassword.setOnEditorActionListener(this);
          mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description);
          mUseFingerprintFutureCheckBox = (CheckBox) v.findViewById(R.id.use_fingerprint_in_future_check);
          mNewFingerprintEnrolledTextView = (TextView) v.findViewById(R.id.new_fingerprint_enrolled_description);
          mFingerprintIcon = (ImageView) v.findViewById(R.id.fingerprint_icon);
          mFingerprintStatus = (TextView) v.findViewById(R.id.fingerprint_status);
          mFingerprintStatus.setText(R.string.initializing);
  
          return dialog;
      }
  
      @Override
      public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
          super.onViewCreated(view, savedInstanceState);
          updateStage(null);
      }
  
      @Override
      public void onAttach(Context context) {
          super.onAttach(context);
          if (!(context instanceof Callback)) {
              throw new IllegalStateException("Components showing a FingerprintDialog must implement FingerprintDialog.Callback.");
          }
          mCallback = (Callback) context;
          mContext = context;
      }
  
      @Override
      public void onResume() {
          super.onResume();
          Bundle arguments = getArguments();
  
          if (arguments != null) {
              String keyName = arguments.getString(ARG_KEY_NAME, "");
              mFingerLock.register(mContext, keyName, this);
          }
          if (BuildConfig.DEBUG) Log.d(TAG, "onResume: called");
      }
  
      @Override
      public void onPause() {
          super.onPause();
          mFingerLock.unregister(this);
          if (BuildConfig.DEBUG) Log.d(TAG, "onPause: called");
      }
  
      @Override
      public void onCancel(DialogInterface dialog) {
          super.onCancel(dialog);
          if (mCallback != null)
              mCallback.onFingerprintDialogCancelled();
      }
  
      /**
       * Switches to backup (password) screen. This either can happen when fingerprint is not
       * available or the user chooses to use the password authentication method by pressing the
       * button. This can also happen when the user had too many fingerprint attempts.
       */
      private void goToBackup(MaterialDialog dialog) {
          mStage = Stage.PASSWORD;
          updateStage(dialog);
          mPassword.requestFocus();
          // Show the keyboard.
          mPassword.postDelayed(mShowKeyboardRunnable, 500);
          // Fingerprint is not used anymore. Stop listening for it.
          mFingerLock.stop();
      }
  
      private void toggleButtonsEnabled(boolean enabled) {
          MaterialDialog dialog = (MaterialDialog) getDialog();
          dialog.getActionButton(DialogAction.POSITIVE).setEnabled(enabled);
          dialog.getActionButton(DialogAction.NEGATIVE).setEnabled(enabled);
      }
  
      private void verifyPassword() {
          toggleButtonsEnabled(false);
          mCallback.onFingerprintDialogVerifyPassword(this, mPassword.getText().toString());
      }
  
      public void notifyPasswordValidation(boolean valid) {
          final MaterialDialog dialog = (MaterialDialog) getDialog();
          final View positive = dialog.getActionButton(DialogAction.POSITIVE);
          final View negative = dialog.getActionButton(DialogAction.NEGATIVE);
          toggleButtonsEnabled(true);
  
          if (valid) {
              if (mStage == Stage.KEY_INVALIDATED &&
                      mUseFingerprintFutureCheckBox.isChecked()) {
                  // Re-create the key so that fingerprints including new ones are validated.
                  mFingerLock.recreateKey(this);
                  mStage = Stage.FINGERPRINT;
              }
              mPassword.setText("");
              mCallback.onFingerprintDialogAuthenticated();
              dismiss();
          } else {
              mPasswordDescriptionTextView.setText(R.string.invalid_password);
              final int red = ContextCompat.getColor(getActivity(), R.color.material_red_500);
              MDTintHelper.setTint(mPassword, red);
              ((TextView) positive).setTextColor(red);
              ((TextView) negative).setTextColor(red);
          }
      }
  
      private final Runnable mShowKeyboardRunnable = new Runnable() {
          @Override
          public void run() {
              mInputMethodManager.showSoftInput(mPassword, 0);
          }
      };
  
      private void updateStage(@Nullable MaterialDialog dialog) {
          if (mLastStage == null || (mLastStage != mStage && mCallback != null)) {
              mLastStage = mStage;
              mCallback.onFingerprintDialogStageUpdated(this, mStage);
          }
          if (dialog == null)
              dialog = (MaterialDialog) getDialog();
          if (dialog == null) return;
          switch (mStage) {
              case FINGERPRINT:
                  dialog.setActionButton(DialogAction.POSITIVE, android.R.string.cancel);
                  dialog.setActionButton(DialogAction.NEGATIVE, R.string.use_password);
                  mFingerprintContent.setVisibility(View.VISIBLE);
                  mBackupContent.setVisibility(View.GONE);
                  break;
              case KEY_INVALIDATED:
                  // Intentional fall through
              case PASSWORD:
                  dialog.setActionButton(DialogAction.POSITIVE, android.R.string.cancel);
                  dialog.setActionButton(DialogAction.NEGATIVE, android.R.string.ok);
                  mFingerprintContent.setVisibility(View.GONE);
                  mBackupContent.setVisibility(View.VISIBLE);
                  if (mStage == Stage.KEY_INVALIDATED) {
                      // Fingerprint is not used anymore. Stop listening for it.
                      mFingerLock.stop();
                      mPasswordDescriptionTextView.setVisibility(View.GONE);
                      mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE);
                      mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE);
                  }
                  break;
          }
      }
  
      @Override
      public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
          if (actionId == EditorInfo.IME_ACTION_GO) {
              verifyPassword();
              return true;
          }
          return false;
      }
  
      /**
       * Enumeration to indicate which authentication method the user is trying to authenticate with.
       */
      public enum Stage {
          FINGERPRINT,
          KEY_INVALIDATED,
          PASSWORD
      }
  
      private void showError(CharSequence error) {
          if (getActivity() == null) return;
          mFingerprintIcon.setImageResource(R.drawable.ic_fingerprint_error);
          mFingerprintStatus.setText(error);
          mFingerprintStatus.setTextColor(ContextCompat.getColor(getActivity(), R.color.warning_color));
          mFingerprintStatus.removeCallbacks(mResetErrorTextRunnable);
          mFingerprintStatus.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
      }
  
      Runnable mResetErrorTextRunnable = new Runnable() {
          @Override
          public void run() {
              if (getActivity() == null) return;
              mFingerprintStatus.setTextColor(ColorAttr.getColor(getActivity(), android.R.attr.textColorSecondary));
              mFingerprintStatus.setText(getResources().getString(R.string.fingerprint_hint));
              mFingerprintIcon.setImageResource(R.drawable.ic_fp_40px);
          }
      };
  
      // FingerLock callbacks
  
      @Override
      public void onFingerLockError(@FingerLock.FingerLockErrorState int errorType, Exception e) {
          switch (errorType) {
  
              case FingerLock.FINGERPRINT_ERROR_HELP:
                  showError(e.getMessage());
                  break;
              case FingerLock.FINGERPRINT_NOT_RECOGNIZED:
                  showError(getResources().getString(R.string.fingerprint_not_recognized));
                  break;
              case FingerLock.FINGERPRINT_NOT_SUPPORTED:
                  goToBackup(null);
                  break;
              case FingerLock.FINGERPRINT_REGISTRATION_NEEDED:
                  mPasswordDescriptionTextView.setText(R.string.no_fingerprints_registered);
                  goToBackup(null);
                  break;
              case FingerLock.FINGERPRINT_PERMISSION_DENIED:
              case FingerLock.FINGERPRINT_UNRECOVERABLE_ERROR:
                  showError(e.getMessage());
                  mFingerprintIcon.postDelayed(new Runnable() {
                      @Override
                      public void run() {
                          goToBackup(null);
                      }
                  }, ERROR_TIMEOUT_MILLIS);
                  break;
          }
      }
  
      @Override
      public void onFingerLockAuthenticationSucceeded() {
          toggleButtonsEnabled(false);
          mFingerprintStatus.removeCallbacks(mResetErrorTextRunnable);
          mFingerprintIcon.setImageResource(R.drawable.ic_fingerprint_success);
          mFingerprintStatus.setTextColor(ContextCompat.getColor(getActivity(), R.color.success_color));
          mFingerprintStatus.setText(getResources().getString(R.string.fingerprint_success));
          mFingerprintIcon.postDelayed(new Runnable() {
              @Override
              public void run() {
                  mCallback.onFingerprintDialogAuthenticated();
                  dismiss();
              }
          }, SUCCESS_DELAY_MILLIS);
      }
  
      @Override
      public void onFingerLockReady() {
          mFingerLock.start();
      }
  
      @Override
      public void onFingerLockScanning(boolean invalidKey) {
          mFingerprintStatus.setText(R.string.fingerprint_hint);
          if (invalidKey)
              mStage = Stage.KEY_INVALIDATED;
          updateStage(null);
  
      }
  
      /**
       * Creates a builder for the {@link FingerprintDialog} dialog
       */
      public static class Builder {
  
          private String keyName;
          private int requestCode = -1;
          private boolean cancelable = true;
          private FragmentActivity context;
  
          /**
           * Set the caller context.
           *
           * @param context caller {@link Context}. Shall be {@link android.app.Activity} or
           *                {@link Fragment} and implement {@link Callback} interface
           * @param <T>
           * @return This Builder object to allow for chaining of calls to set methods
           */
          public <T extends FragmentActivity & Callback> Builder with(@NonNull T context) {
              this.context = context;
              return this;
          }
  
          /**
           * Set the keyname
           *
           * @param keyName key string
           * @return This Builder object to allow for chaining of calls to set methods
           */
          public Builder setKeyName(@NonNull String keyName) {
              this.keyName = keyName;
              return this;
          }
  
          /**
           * Set the request code
           *
           * @param requestCode positive integer number
           * @return This Builder object to allow for chaining of calls to set methods
           */
          public Builder setRequestCode(@IntRange(from = 0, to = Integer.MAX_VALUE) int requestCode) {
              this.requestCode = requestCode;
              return this;
          }
  
          /**
           * Set whether the dialog is cancelable or not
           *
           * @param cancelable <code>true</code> if cancelable (default = true)
           * @return This Builder object to allow for chaining of calls to set methods
           */
          public Builder setCancelable(boolean cancelable) {
              this.cancelable = cancelable;
              return this;
          }
  
          /**
           * Call this method to show and get the {@link FingerprintDialog} reference
           *
           * @param <T>
           * @return {@link FingerprintDialog} dialog
           */
          @Nullable
          public <T extends FragmentActivity & Callback> FingerprintDialog show() {
              if (context == null || TextUtils.isEmpty(this.keyName) || requestCode < 0) {
                  return null;
              }
              FingerprintDialog dialog = getVisible(context);
              if (dialog != null)
                  dialog.dismiss();
              dialog = new FingerprintDialog();
              Bundle args = new Bundle();
              args.putString(ARG_KEY_NAME, keyName);
              args.putInt(ARG_REQUEST_CODE, requestCode);
              args.putBoolean(ARG_CANCELABLE, cancelable);
              dialog.setArguments(args);
              dialog.show(context.getSupportFragmentManager(), TAG);
              mInputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
              return dialog;
          }
  
          private <T extends FragmentActivity> FingerprintDialog getVisible(T context) {
              Fragment frag = context.getSupportFragmentManager().findFragmentByTag(TAG);
              if (frag != null && frag instanceof FingerprintDialog)
                  return (FingerprintDialog) frag;
              return null;
          }
      }
  }