/*
* 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();
}
}