/* * Copyright (C) 2008 The Android Open Source 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.internal.policy.impl; import android.content.Context; import android.os.CountDownTimer; import android.os.SystemClock; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.MotionEvent; import android.widget.Button; import android.widget.TextView; import android.text.format.DateFormat; import android.text.TextUtils; import android.util.Log; import com.android.internal.R; import com.android.internal.telephony.IccCard; import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; import com.android.internal.widget.LockPatternView.Cell; import java.util.List; import java.util.Date; /** * This is the screen that shows the 9 circle unlock widget and instructs * the user how to unlock their device, or make an emergency call. */ class UnlockScreen extends LinearLayoutWithDefaultTouchRecepient implements KeyguardScreen, KeyguardUpdateMonitor.ConfigurationChangeCallback, KeyguardUpdateMonitor.InfoCallback, KeyguardUpdateMonitor.SimStateCallback { private static final boolean DEBUG = false; private static final String TAG = "UnlockScreen"; // how long before we clear the wrong pattern private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; // how long we stay awake once the user is ready to enter a pattern private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; // how many cells the user has to cross before we poke the wakelock private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; private int mFailedPatternAttemptsSinceLastTimeout = 0; private int mTotalFailedPatternAttempts = 0; private CountDownTimer mCountdownTimer = null; private final LockPatternUtils mLockPatternUtils; private final KeyguardUpdateMonitor mUpdateMonitor; private final KeyguardScreenCallback mCallback; /** * whether there is a fallback option available when the pattern is forgotten. */ private boolean mEnableFallback; private boolean mCreatedInPortrait; private String mDateFormatString; private TextView mCarrier; private TextView mDate; // are we showing battery information? private boolean mShowingBatteryInfo = false; // last known plugged in state private boolean mPluggedIn = false; // last known battery level private int mBatteryLevel = 100; private String mNextAlarm = null; private String mInstructions = null; private TextView mStatus1; private TextView mStatusSep; private TextView mStatus2; private LockPatternView mLockPatternView; private ViewGroup mFooterNormal; private ViewGroup mFooterForgotPattern; /** * Keeps track of the last time we poked the wake lock during dispatching * of the touch event, initalized to something gauranteed to make us * poke it when the user starts drawing the pattern. * @see #dispatchTouchEvent(android.view.MotionEvent) */ private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; /** * Useful for clearing out the wrong pattern after a delay */ private Runnable mCancelPatternRunnable = new Runnable() { public void run() { mLockPatternView.clearPattern(); } }; private Button mForgotPatternButton; enum FooterMode { Normal, ForgotLockPattern, VerifyUnlocked } private void updateFooter(FooterMode mode) { switch (mode) { case Normal: mFooterNormal.setVisibility(View.VISIBLE); mFooterForgotPattern.setVisibility(View.GONE); break; case ForgotLockPattern: mFooterNormal.setVisibility(View.GONE); mFooterForgotPattern.setVisibility(View.VISIBLE); mForgotPatternButton.setVisibility(View.VISIBLE); break; case VerifyUnlocked: mFooterNormal.setVisibility(View.GONE); mFooterForgotPattern.setVisibility(View.GONE); } } /** * @param context The context. * @param lockPatternUtils Used to lookup lock pattern settings. * @param updateMonitor Used to lookup state affecting keyguard. * @param callback Used to notify the manager when we're done, etc. * @param totalFailedAttempts The current number of failed attempts. * @param enableFallback True if a backup unlock option is available when the user has forgotten * their pattern (e.g they have a google account so we can show them the account based * backup option). */ UnlockScreen(Context context, LockPatternUtils lockPatternUtils, KeyguardUpdateMonitor updateMonitor, KeyguardScreenCallback callback, int totalFailedAttempts) { super(context); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = updateMonitor; mCallback = callback; mTotalFailedPatternAttempts = totalFailedAttempts; mFailedPatternAttemptsSinceLastTimeout = totalFailedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; if (DEBUG) Log.d(TAG, "UnlockScreen() ctor: totalFailedAttempts=" + totalFailedAttempts + ", mFailedPat...=" + mFailedPatternAttemptsSinceLastTimeout ); if (mUpdateMonitor.isInPortrait()) { LayoutInflater.from(context).inflate(R.layout.keyguard_screen_unlock_portrait, this, true); } else { LayoutInflater.from(context).inflate(R.layout.keyguard_screen_unlock_landscape, this, true); } mCarrier = (TextView) findViewById(R.id.carrier); mDate = (TextView) findViewById(R.id.date); mDateFormatString = getContext().getString(R.string.full_wday_month_day_no_year); refreshTimeAndDateDisplay(); mStatus1 = (TextView) findViewById(R.id.status1); mStatusSep = (TextView) findViewById(R.id.statusSep); mStatus2 = (TextView) findViewById(R.id.status2); resetStatusInfo(); mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); mFooterNormal = (ViewGroup) findViewById(R.id.footerNormal); mFooterForgotPattern = (ViewGroup) findViewById(R.id.footerForgotPattern); // emergency call buttons final OnClickListener emergencyClick = new OnClickListener() { public void onClick(View v) { mCallback.takeEmergencyCallAction(); } }; Button emergencyAlone = (Button) findViewById(R.id.emergencyCallAlone); emergencyAlone.setFocusable(false); // touch only! emergencyAlone.setOnClickListener(emergencyClick); Button emergencyTogether = (Button) findViewById(R.id.emergencyCallTogether); emergencyTogether.setFocusable(false); emergencyTogether.setOnClickListener(emergencyClick); mForgotPatternButton = (Button) findViewById(R.id.forgotPattern); mForgotPatternButton.setText(R.string.lockscreen_forgot_pattern_button_text); mForgotPatternButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mCallback.forgotPattern(true); } }); // make it so unhandled touch events within the unlock screen go to the // lock pattern view. setDefaultTouchRecepient(mLockPatternView); mLockPatternView.setSaveEnabled(false); mLockPatternView.setFocusable(false); mLockPatternView.setOnPatternListener(new UnlockPatternListener()); // stealth mode will be the same for the life of this screen mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); // vibrate mode will be the same for the life of this screen mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); // assume normal footer mode for now updateFooter(FooterMode.Normal); mCreatedInPortrait = updateMonitor.isInPortrait(); updateMonitor.registerInfoCallback(this); updateMonitor.registerSimStateCallback(this); updateMonitor.registerConfigurationChangeCallback(this); setFocusableInTouchMode(true); // Required to get Marquee to work. mCarrier.setSelected(true); mCarrier.setTextColor(0xffffffff); // until we get an update... mCarrier.setText( LockScreen.getCarrierString( mUpdateMonitor.getTelephonyPlmn(), mUpdateMonitor.getTelephonySpn())); } public void setEnableFallback(boolean state) { if (DEBUG) Log.d(TAG, "setEnableFallback(" + state + ")"); mEnableFallback = state; } private void resetStatusInfo() { mInstructions = null; mShowingBatteryInfo = mUpdateMonitor.shouldShowBatteryInfo(); mPluggedIn = mUpdateMonitor.isDevicePluggedIn(); mBatteryLevel = mUpdateMonitor.getBatteryLevel(); mNextAlarm = mLockPatternUtils.getNextAlarm(); updateStatusLines(); } private void updateStatusLines() { if (mInstructions != null) { // instructions only mStatus1.setText(mInstructions); if (TextUtils.isEmpty(mInstructions)) { mStatus1.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } else { mStatus1.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_lock_idle_lock, 0, 0, 0); } mStatus1.setVisibility(View.VISIBLE); mStatusSep.setVisibility(View.GONE); mStatus2.setVisibility(View.GONE); } else if (mShowingBatteryInfo && mNextAlarm == null) { // battery only if (mPluggedIn) { if (mBatteryLevel >= 100) { mStatus1.setText(getContext().getString(R.string.lockscreen_charged)); } else { mStatus1.setText(getContext().getString(R.string.lockscreen_plugged_in, mBatteryLevel)); } } else { mStatus1.setText(getContext().getString(R.string.lockscreen_low_battery)); } mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0); mStatus1.setVisibility(View.VISIBLE); mStatusSep.setVisibility(View.GONE); mStatus2.setVisibility(View.GONE); } else if (mNextAlarm != null && !mShowingBatteryInfo) { // alarm only mStatus1.setText(mNextAlarm); mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0); mStatus1.setVisibility(View.VISIBLE); mStatusSep.setVisibility(View.GONE); mStatus2.setVisibility(View.GONE); } else if (mNextAlarm != null && mShowingBatteryInfo) { // both battery and next alarm mStatus1.setText(mNextAlarm); mStatusSep.setText("|"); mStatus2.setText(getContext().getString( R.string.lockscreen_battery_short, Math.min(100, mBatteryLevel))); mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0); if (mPluggedIn) { mStatus2.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0); } else { mStatus2.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } mStatus1.setVisibility(View.VISIBLE); mStatusSep.setVisibility(View.VISIBLE); mStatus2.setVisibility(View.VISIBLE); } else { // nothing specific to show; show general instructions mStatus1.setText(R.string.lockscreen_pattern_instructions); mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_lock, 0, 0, 0); mStatus1.setVisibility(View.VISIBLE); mStatusSep.setVisibility(View.GONE); mStatus2.setVisibility(View.GONE); } } private void refreshTimeAndDateDisplay() { mDate.setText(DateFormat.format(mDateFormatString, new Date())); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { // as long as the user is entering a pattern (i.e sending a touch // event that was handled by this screen), keep poking the // wake lock so that the screen will stay on. final boolean result = super.dispatchTouchEvent(ev); if (result && ((SystemClock.elapsedRealtime() - mLastPokeTime) > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { mLastPokeTime = SystemClock.elapsedRealtime(); } return result; } // ---------- InfoCallback /** {@inheritDoc} */ public void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, int batteryLevel) { mShowingBatteryInfo = showBatteryInfo; mPluggedIn = pluggedIn; mBatteryLevel = batteryLevel; updateStatusLines(); } /** {@inheritDoc} */ public void onTimeChanged() { refreshTimeAndDateDisplay(); } /** {@inheritDoc} */ public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) { mCarrier.setText(LockScreen.getCarrierString(plmn, spn)); } /** {@inheritDoc} */ public void onRingerModeChanged(int state) { // not currently used } // ---------- SimStateCallback /** {@inheritDoc} */ public void onSimStateChanged(IccCard.State simState) { } /** {@inheritDoc} */ public void onOrientationChange(boolean inPortrait) { if (inPortrait != mCreatedInPortrait) { mCallback.recreateMe(); } } /** {@inheritDoc} */ public void onKeyboardChange(boolean isKeyboardOpen) {} /** {@inheritDoc} */ public boolean needsInput() { return false; } /** {@inheritDoc} */ public void onPause() { if (mCountdownTimer != null) { mCountdownTimer.cancel(); mCountdownTimer = null; } } /** {@inheritDoc} */ public void onResume() { // reset header resetStatusInfo(); // reset lock pattern mLockPatternView.enableInput(); mLockPatternView.setEnabled(true); mLockPatternView.clearPattern(); // show "forgot pattern?" button if we have an alternate authentication method mForgotPatternButton.setVisibility(mCallback.doesFallbackUnlockScreenExist() ? View.VISIBLE : View.INVISIBLE); // if the user is currently locked out, enforce it. long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); if (deadline != 0) { handleAttemptLockout(deadline); } // the footer depends on how many total attempts the user has failed if (mCallback.isVerifyUnlockOnly()) { updateFooter(FooterMode.VerifyUnlocked); } else if (mEnableFallback && (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { updateFooter(FooterMode.ForgotLockPattern); } else { updateFooter(FooterMode.Normal); } } /** {@inheritDoc} */ public void cleanUp() { mUpdateMonitor.removeCallback(this); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (hasWindowFocus) { // when timeout dialog closes we want to update our state onResume(); } } private class UnlockPatternListener implements LockPatternView.OnPatternListener { public void onPatternStart() { mLockPatternView.removeCallbacks(mCancelPatternRunnable); } public void onPatternCleared() { } public void onPatternCellAdded(List<Cell> pattern) { // To guard against accidental poking of the wakelock, look for // the user actually trying to draw a pattern of some minimal length. if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); } } public void onPatternDetected(List<LockPatternView.Cell> pattern) { if (mLockPatternUtils.checkPattern(pattern)) { mLockPatternView .setDisplayMode(LockPatternView.DisplayMode.Correct); mInstructions = ""; updateStatusLines(); mCallback.keyguardDone(true); } else { if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); } mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { mTotalFailedPatternAttempts++; mFailedPatternAttemptsSinceLastTimeout++; mCallback.reportFailedPatternAttempt(); } if (mFailedPatternAttemptsSinceLastTimeout >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); handleAttemptLockout(deadline); return; } // TODO mUnlockIcon.setVisibility(View.VISIBLE); mInstructions = getContext().getString(R.string.lockscreen_pattern_wrong); updateStatusLines(); mLockPatternView.postDelayed( mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); } } } private void handleAttemptLockout(long elapsedRealtimeDeadline) { mLockPatternView.clearPattern(); mLockPatternView.setEnabled(false); long elapsedRealtime = SystemClock.elapsedRealtime(); mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { @Override public void onTick(long millisUntilFinished) { int secondsRemaining = (int) (millisUntilFinished / 1000); mInstructions = getContext().getString( R.string.lockscreen_too_many_failed_attempts_countdown, secondsRemaining); updateStatusLines(); } @Override public void onFinish() { mLockPatternView.setEnabled(true); mInstructions = getContext().getString(R.string.lockscreen_pattern_instructions); updateStatusLines(); // TODO mUnlockIcon.setVisibility(View.VISIBLE); mFailedPatternAttemptsSinceLastTimeout = 0; if (mEnableFallback) { updateFooter(FooterMode.ForgotLockPattern); } else { updateFooter(FooterMode.Normal); } } }.start(); } }