/* * Copyright (C) 2014 The CyanogenMod Project * * 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.android.settings.applications; import android.app.Activity; import android.content.SharedPreferences; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Base64; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; import com.android.settings.R; import com.android.settings.cyanogenmod.ProtectedAccountView; import com.android.settings.cyanogenmod.ProtectedAccountView.OnNotifyAccountReset; import com.android.settings.fingerprint.FingerprintUiHelper; import com.android.settings.Utils; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; public class LockPatternActivity extends Activity implements OnNotifyAccountReset, FingerprintUiHelper.Callback { public static final String PATTERN_LOCK_PROTECTED_APPS = "pattern_lock_protected_apps"; public static final String RECREATE_PATTERN = "recreate_pattern_lock"; private static final String STATE_IS_ACCOUNT_VIEW = "isAccountView"; private static final String STATE_CONTINUE_ENABLED = "continueEnabled"; private static final String STATE_CONFIRMING = "confirming"; private static final String STATE_RETRY_PATTERN = "retrypattern"; private static final String STATE_RETRY = "retry"; private static final String STATE_PATTERN_HASH = "pattern_hash"; private static final String STATE_CREATE = "create"; private static String TIMEOUT_PREF_KEY = "retry_timeout"; private static final int MIN_PATTERN_SIZE = 4; private static final int MAX_PATTERN_RETRY = 5; private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; private static final long FAILED_ATTEMPT_RETRY = 30; private static final int MENU_RESET = 0; LockPatternView mLockPatternView; ProtectedAccountView mAccountView; ImageView mFingerprintIconView; TextView mPatternLockHeader; MenuItem mItem; Button mCancel; Button mContinue; byte[] mPatternHash; int mRetry = 0; boolean mCreate; boolean mRetryPattern = true; boolean mConfirming = false; boolean mFingerPrintSetUp = false; boolean mRetryLocked = false; private FingerprintManager mFingerprintManager; private FingerprintUiHelper mFingerPrintUiHelper; Runnable mCancelPatternRunnable = new Runnable() { public void run() { mLockPatternView.clearPattern(); mContinue.setEnabled(false); if (mCreate) { if (mConfirming) { mPatternLockHeader.setText(getResources() .getString(R.string.lockpattern_need_to_confirm)); } else { mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_recording_intro_header)); mCancel.setText(getResources().getString(R.string.cancel)); } } else { mPatternLockHeader.setText(mFingerPrintSetUp ? getResources().getString(R.string.pa_pattern_or_fingerprint_header) : getResources().getString(R.string.lockpattern_settings_enable_summary)); } } }; View.OnClickListener mCancelOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { if (mCreate && !mConfirming && !mRetryPattern) { // Retry mRetryPattern = true; resetPatternState(true); return; } setResult(RESULT_CANCELED); finish(); } }; View.OnClickListener mContinueOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { Button btn = (Button) v; if (mConfirming) { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); SharedPreferences.Editor editor = prefs.edit(); editor.putString(PATTERN_LOCK_PROTECTED_APPS, Base64.encodeToString(mPatternHash, Base64.DEFAULT)); editor.commit(); setResult(RESULT_OK); finish(); } else { mConfirming = true; mCancel.setText(getResources().getString(R.string.cancel)); mLockPatternView.clearPattern(); mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_need_to_confirm)); btn.setText(getResources().getString(R.string.lockpattern_confirm_button_text)); btn.setEnabled(false); } } }; @Override public boolean onCreateOptionsMenu(Menu menu) { menu.clear(); menu.add(0, MENU_RESET, 0, R.string.lockpattern_reset_button) .setIcon(R.drawable.ic_lockscreen_ime_white) .setAlphabeticShortcut('r') .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); mItem = menu.findItem(0); if (mRetryLocked) { mItem.setIcon(R.drawable.ic_settings_lockscreen_white); } return true; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(STATE_IS_ACCOUNT_VIEW, mAccountView.getVisibility() == View.VISIBLE); outState.putBoolean(STATE_CONTINUE_ENABLED, mContinue.isEnabled()); outState.putBoolean(STATE_CONFIRMING, mConfirming); outState.putBoolean(STATE_RETRY_PATTERN, mRetryPattern); outState.putInt(STATE_RETRY, mRetry); outState.putByteArray(STATE_PATTERN_HASH, mPatternHash); outState.putBoolean(STATE_CREATE, mCreate); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); if (savedInstanceState.getBoolean(STATE_IS_ACCOUNT_VIEW)) { switchToAccount(); } else { switchToPattern(false); mPatternHash = savedInstanceState.getByteArray(STATE_PATTERN_HASH); mConfirming = savedInstanceState.getBoolean(STATE_CONFIRMING); mRetryPattern = savedInstanceState.getBoolean(STATE_RETRY_PATTERN); mRetry = savedInstanceState.getInt(STATE_RETRY); mCreate = savedInstanceState.getBoolean(STATE_CREATE); mContinue.setEnabled(savedInstanceState.getBoolean(STATE_CONTINUE_ENABLED, mContinue.isEnabled())); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_RESET: if (mAccountView.getVisibility() == View.VISIBLE) { switchToPattern(false); } else { switchToAccount(); } return true; case android.R.id.home: setResult(RESULT_CANCELED); finish(); return true; default: return false; } } @Override public void onNotifyAccountReset() { switchToPattern(true); } private void switchToPattern(boolean reset) { if (isRetryLocked()) { return; } if (reset) { resetPatternState(false); } mPatternLockHeader.setText(mFingerPrintSetUp ? getResources().getString(R.string.pa_pattern_or_fingerprint_header) : getResources().getString(R.string.lockpattern_settings_enable_summary)); mItem.setIcon(R.drawable.ic_lockscreen_ime_white); mAccountView.clearFocusOnInput(); mAccountView.setVisibility(View.GONE); mLockPatternView.setVisibility(View.VISIBLE); } private void switchToAccount() { mPatternLockHeader.setText(getResources() .getString(R.string.lockpattern_settings_reset_summary)); if (mItem != null) { mItem.setIcon(R.drawable.ic_settings_lockscreen_white); } mAccountView.setVisibility(View.VISIBLE); mLockPatternView.setVisibility(View.GONE); } protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.patternlock); getActionBar().setDisplayHomeAsUpEnabled(true); mPatternLockHeader = (TextView) findViewById(R.id.pattern_lock_header); mCancel = (Button) findViewById(R.id.pattern_lock_btn_cancel); mCancel.setOnClickListener(mCancelOnClickListener); mContinue = (Button) findViewById(R.id.pattern_lock_btn_continue); mContinue.setOnClickListener(mContinueOnClickListener); mAccountView = (ProtectedAccountView) findViewById(R.id.lock_account_view); mAccountView.setOnNotifyAccountResetCb(this); mLockPatternView = (LockPatternView) findViewById(R.id.lock_pattern_view); mFingerprintIconView = (ImageView) findViewById(R.id.protected_apps_fingerprint_icon); resetPatternState(false); //Setup Pattern Lock View mLockPatternView.setFocusable(false); mLockPatternView.setOnPatternListener(new UnlockPatternListener()); mFingerprintManager = (FingerprintManager) getSystemService(FingerprintManager.class); if (mFingerprintManager.isHardwareDetected()) { if (mFingerprintManager.hasEnrolledFingerprints() && !mCreate) { mFingerPrintSetUp = true; mFingerPrintUiHelper = new FingerprintUiHelper(mFingerprintIconView, mPatternLockHeader, this, Utils.getCredentialOwnerUserId(this)); mFingerPrintUiHelper.setDarkIconography(true); mFingerPrintUiHelper.setIdleText(getString( R.string.pa_pattern_or_fingerprint_header)); } else { mFingerPrintSetUp = false; } } } private void resetPatternState(boolean clear) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String pattern = prefs.getString(PATTERN_LOCK_PROTECTED_APPS, null); mCreate = pattern == null || RECREATE_PATTERN.equals(getIntent().getAction()) || clear; mPatternHash = null; if (pattern != null) { mPatternHash = Base64.decode(pattern, Base64.DEFAULT); } mContinue.setEnabled(!mCreate); mCancel.setVisibility(mCreate ? View.VISIBLE : View.GONE); mCancel.setText(getResources().getString(R.string.cancel)); mContinue.setVisibility(mCreate ? View.VISIBLE : View.GONE); mPatternLockHeader.setText(mCreate ? getResources().getString(R.string.lockpattern_recording_intro_header) : (mFingerPrintSetUp ? getResources().getString(R.string.pa_pattern_or_fingerprint_header) : getResources().getString(R.string.lockpattern_settings_enable_summary))); mLockPatternView.clearPattern(); invalidateOptionsMenu(); } @Override public void onAuthenticated() { setResult(RESULT_OK); finish(); } @Override public void onFingerprintIconVisibilityChanged(boolean visible) { } private class UnlockPatternListener implements LockPatternView.OnPatternListener { public void onPatternStart() { mLockPatternView.removeCallbacks(mCancelPatternRunnable); mPatternLockHeader.setText(getResources().getText( R.string.lockpattern_recording_inprogress)); mContinue.setEnabled(false); } public void onPatternCleared() { } public void onPatternDetected(List<LockPatternView.Cell> pattern) { //Check inserted Pattern if (mCreate) { if (pattern.size() < MIN_PATTERN_SIZE) { mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_recording_incorrect_too_short, LockPatternUtils.MIN_LOCK_PATTERN_SIZE)); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); mCancel.setText(getResources() .getString(R.string.lockpattern_retry_button_text)); mRetryPattern = false; return; } if (mConfirming) { if (Arrays.equals(mPatternHash, patternToHash(pattern))) { mContinue.setText(getResources() .getString(R.string.lockpattern_confirm_button_text)); mContinue.setEnabled(true); mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_pattern_confirmed_header)); } else { mContinue.setEnabled(false); mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_need_to_unlock_wrong)); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); } } else { //Save pattern, user needs to redraw to confirm mCancel.setText(getResources() .getString(R.string.lockpattern_retry_button_text)); mRetryPattern = false; mPatternHash = patternToHash(pattern); mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_pattern_entered_header)); mContinue.setEnabled(true); } } else { //Check against existing pattern if (Arrays.equals(mPatternHash, patternToHash(pattern))) { setResult(RESULT_OK); finish(); } else { mRetry++; mPatternLockHeader.setText(getResources().getString( R.string.lockpattern_need_to_unlock_wrong)); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); if (mRetry >= MAX_PATTERN_RETRY) { setPatternTimeout(); mLockPatternView.removeCallbacks(mCancelPatternRunnable); Toast.makeText(getApplicationContext(), getResources().getString( R.string.lockpattern_too_many_failed_confirmation_attempts, FAILED_ATTEMPT_RETRY), Toast.LENGTH_SHORT).show(); switchToAccount(); } } } } public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {} } /* * Generate an SHA-1 hash for the pattern. Not the most secure, but it is * at least a second level of protection. First level is that the file * is in a location only readable by the system process. * @param pattern the gesture pattern. * @return the hash of the pattern in a byte array. */ public byte[] patternToHash(List<LockPatternView.Cell> pattern) { if (pattern == null) { return null; } final int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { LockPatternView.Cell cell = pattern.get(i); res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); } try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] hash = md.digest(res); return hash; } catch (NoSuchAlgorithmException nsa) { return res; } } @Override protected void onPause() { if (mFingerPrintSetUp) { mFingerPrintUiHelper.stopListening(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mFingerPrintSetUp) { mPatternLockHeader.setText(getString(R.string.pa_pattern_or_fingerprint_header)); mFingerPrintUiHelper.startListening(); } if (isRetryLocked()) { invalidateOptionsMenu(); switchToAccount(); } } private boolean isRetryLocked() { long time = System.currentTimeMillis(); SharedPreferences prefs = getSharedPreferences(getPackageName(), MODE_PRIVATE); long retryTime = prefs.getLong(TIMEOUT_PREF_KEY, 0); mRetryLocked = (time - retryTime) < (FAILED_ATTEMPT_RETRY * 1000); return mRetryLocked; } private void setPatternTimeout() { SharedPreferences prefs = getSharedPreferences(getPackageName(), MODE_PRIVATE); prefs.edit().putLong(TIMEOUT_PREF_KEY, System.currentTimeMillis()).apply(); } }