Java tutorial
/* * 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; } } }