1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.policy.impl.keyguard_obsolete;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.os.CountDownTimer;
22import android.os.SystemClock;
23import android.security.KeyStore;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.MotionEvent;
27import android.widget.Button;
28import android.util.Log;
29import com.android.internal.R;
30import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
31import com.android.internal.widget.LockPatternUtils;
32import com.android.internal.widget.LockPatternView;
33import com.android.internal.widget.LockPatternView.Cell;
34
35import java.util.List;
36
37/**
38 * This is the screen that shows the 9 circle unlock widget and instructs
39 * the user how to unlock their device, or make an emergency call.
40 */
41class PatternUnlockScreen extends LinearLayoutWithDefaultTouchRecepient
42        implements KeyguardScreen {
43
44    private static final boolean DEBUG = false;
45    private static final String TAG = "UnlockScreen";
46
47    // how long before we clear the wrong pattern
48    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
49
50    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
51    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
52
53    // how long we stay awake after the user hits the first dot.
54    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
55
56    // how many cells the user has to cross before we poke the wakelock
57    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
58
59    private int mFailedPatternAttemptsSinceLastTimeout = 0;
60    private int mTotalFailedPatternAttempts = 0;
61    private CountDownTimer mCountdownTimer = null;
62
63    private LockPatternUtils mLockPatternUtils;
64    private KeyguardUpdateMonitor mUpdateMonitor;
65    private KeyguardScreenCallback mCallback;
66
67    /**
68     * whether there is a fallback option available when the pattern is forgotten.
69     */
70    private boolean mEnableFallback;
71
72    private KeyguardStatusViewManager mKeyguardStatusViewManager;
73    private LockPatternView mLockPatternView;
74
75    /**
76     * Keeps track of the last time we poked the wake lock during dispatching
77     * of the touch event, initalized to something gauranteed to make us
78     * poke it when the user starts drawing the pattern.
79     * @see #dispatchTouchEvent(android.view.MotionEvent)
80     */
81    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
82
83    /**
84     * Useful for clearing out the wrong pattern after a delay
85     */
86    private Runnable mCancelPatternRunnable = new Runnable() {
87        public void run() {
88            mLockPatternView.clearPattern();
89        }
90    };
91
92    private final OnClickListener mForgotPatternClick = new OnClickListener() {
93        public void onClick(View v) {
94            mCallback.forgotPattern(true);
95        }
96    };
97
98    private Button mForgotPatternButton;
99    private int mCreationOrientation;
100
101    enum FooterMode {
102        Normal,
103        ForgotLockPattern,
104        VerifyUnlocked
105    }
106
107    private void hideForgotPatternButton() {
108        mForgotPatternButton.setVisibility(View.GONE);
109    }
110
111    private void showForgotPatternButton() {
112        mForgotPatternButton.setVisibility(View.VISIBLE);
113    }
114
115    private void updateFooter(FooterMode mode) {
116        switch (mode) {
117            case Normal:
118                if (DEBUG) Log.d(TAG, "mode normal");
119                hideForgotPatternButton();
120                break;
121            case ForgotLockPattern:
122                if (DEBUG) Log.d(TAG, "mode ForgotLockPattern");
123                showForgotPatternButton();
124                break;
125            case VerifyUnlocked:
126                if (DEBUG) Log.d(TAG, "mode VerifyUnlocked");
127                hideForgotPatternButton();
128        }
129    }
130
131    /**
132     * @param context The context.
133     * @param configuration
134     * @param lockPatternUtils Used to lookup lock pattern settings.
135     * @param updateMonitor Used to lookup state affecting keyguard.
136     * @param callback Used to notify the manager when we're done, etc.
137     * @param totalFailedAttempts The current number of failed attempts.
138     * @param enableFallback True if a backup unlock option is available when the user has forgotten
139     *        their pattern (e.g they have a google account so we can show them the account based
140     *        backup option).
141     */
142    PatternUnlockScreen(Context context,
143                 Configuration configuration, LockPatternUtils lockPatternUtils,
144                 KeyguardUpdateMonitor updateMonitor,
145                 KeyguardScreenCallback callback,
146                 int totalFailedAttempts) {
147        super(context);
148        mLockPatternUtils = lockPatternUtils;
149        mUpdateMonitor = updateMonitor;
150        mCallback = callback;
151        mTotalFailedPatternAttempts = totalFailedAttempts;
152        mFailedPatternAttemptsSinceLastTimeout =
153            totalFailedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT;
154
155        if (DEBUG) Log.d(TAG,
156            "UnlockScreen() ctor: totalFailedAttempts="
157                 + totalFailedAttempts + ", mFailedPat...="
158                 + mFailedPatternAttemptsSinceLastTimeout
159                 );
160
161        mCreationOrientation = configuration.orientation;
162
163        LayoutInflater inflater = LayoutInflater.from(context);
164
165        if (mCreationOrientation != Configuration.ORIENTATION_LANDSCAPE) {
166            Log.d(TAG, "portrait mode");
167            inflater.inflate(R.layout.keyguard_screen_unlock_portrait, this, true);
168        } else {
169            Log.d(TAG, "landscape mode");
170            inflater.inflate(R.layout.keyguard_screen_unlock_landscape, this, true);
171        }
172
173        mKeyguardStatusViewManager = new KeyguardStatusViewManager(this, mUpdateMonitor,
174                mLockPatternUtils, mCallback, true);
175
176        mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern);
177
178        mForgotPatternButton = (Button) findViewById(R.id.forgotPatternButton);
179        mForgotPatternButton.setText(R.string.lockscreen_forgot_pattern_button_text);
180        mForgotPatternButton.setOnClickListener(mForgotPatternClick);
181
182        // make it so unhandled touch events within the unlock screen go to the
183        // lock pattern view.
184        setDefaultTouchRecepient(mLockPatternView);
185
186        mLockPatternView.setSaveEnabled(false);
187        mLockPatternView.setFocusable(false);
188        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
189
190        // stealth mode will be the same for the life of this screen
191        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled());
192
193        // vibrate mode will be the same for the life of this screen
194        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
195
196        // assume normal footer mode for now
197        updateFooter(FooterMode.Normal);
198
199        setFocusableInTouchMode(true);
200    }
201
202    public void setEnableFallback(boolean state) {
203        if (DEBUG) Log.d(TAG, "setEnableFallback(" + state + ")");
204        mEnableFallback = state;
205    }
206
207    @Override
208    public boolean dispatchTouchEvent(MotionEvent ev) {
209        // as long as the user is entering a pattern (i.e sending a touch
210        // event that was handled by this screen), keep poking the
211        // wake lock so that the screen will stay on.
212        final boolean result = super.dispatchTouchEvent(ev);
213        if (result &&
214                ((SystemClock.elapsedRealtime() - mLastPokeTime)
215                        >  (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
216            mLastPokeTime = SystemClock.elapsedRealtime();
217        }
218        return result;
219    }
220
221    @Override
222    protected void onAttachedToWindow() {
223        super.onAttachedToWindow();
224        if (LockPatternKeyguardView.DEBUG_CONFIGURATION) {
225            Log.v(TAG, "***** PATTERN ATTACHED TO WINDOW");
226            Log.v(TAG, "Cur orient=" + mCreationOrientation
227                    + ", new config=" + getResources().getConfiguration());
228        }
229        if (getResources().getConfiguration().orientation != mCreationOrientation) {
230            mCallback.recreateMe(getResources().getConfiguration());
231        }
232    }
233
234
235    /** {@inheritDoc} */
236    @Override
237    protected void onConfigurationChanged(Configuration newConfig) {
238        super.onConfigurationChanged(newConfig);
239        if (LockPatternKeyguardView.DEBUG_CONFIGURATION) {
240            Log.v(TAG, "***** PATTERN CONFIGURATION CHANGED");
241            Log.v(TAG, "Cur orient=" + mCreationOrientation
242                    + ", new config=" + getResources().getConfiguration());
243        }
244        if (newConfig.orientation != mCreationOrientation) {
245            mCallback.recreateMe(newConfig);
246        }
247    }
248
249    /** {@inheritDoc} */
250    public void onKeyboardChange(boolean isKeyboardOpen) {}
251
252    /** {@inheritDoc} */
253    public boolean needsInput() {
254        return false;
255    }
256
257    /** {@inheritDoc} */
258    public void onPause() {
259        if (mCountdownTimer != null) {
260            mCountdownTimer.cancel();
261            mCountdownTimer = null;
262        }
263        mKeyguardStatusViewManager.onPause();
264    }
265
266    /** {@inheritDoc} */
267    public void onResume() {
268        // reset status
269        mKeyguardStatusViewManager.onResume();
270
271        // reset lock pattern
272        mLockPatternView.enableInput();
273        mLockPatternView.setEnabled(true);
274        mLockPatternView.clearPattern();
275
276        // show "forgot pattern?" button if we have an alternate authentication method
277        if (mCallback.doesFallbackUnlockScreenExist()) {
278            showForgotPatternButton();
279        } else {
280            hideForgotPatternButton();
281        }
282
283        // if the user is currently locked out, enforce it.
284        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
285        if (deadline != 0) {
286            handleAttemptLockout(deadline);
287        }
288
289        // the footer depends on how many total attempts the user has failed
290        if (mCallback.isVerifyUnlockOnly()) {
291            updateFooter(FooterMode.VerifyUnlocked);
292        } else if (mEnableFallback &&
293                (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
294            updateFooter(FooterMode.ForgotLockPattern);
295        } else {
296            updateFooter(FooterMode.Normal);
297        }
298
299    }
300
301    /** {@inheritDoc} */
302    public void cleanUp() {
303        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
304        mUpdateMonitor.removeCallback(this);
305        mLockPatternUtils = null;
306        mUpdateMonitor = null;
307        mCallback = null;
308        mLockPatternView.setOnPatternListener(null);
309    }
310
311    @Override
312    public void onWindowFocusChanged(boolean hasWindowFocus) {
313        super.onWindowFocusChanged(hasWindowFocus);
314        if (hasWindowFocus) {
315            // when timeout dialog closes we want to update our state
316            onResume();
317        }
318    }
319
320    private class UnlockPatternListener
321            implements LockPatternView.OnPatternListener {
322
323        public void onPatternStart() {
324            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
325        }
326
327        public void onPatternCleared() {
328        }
329
330        public void onPatternCellAdded(List<Cell> pattern) {
331            // To guard against accidental poking of the wakelock, look for
332            // the user actually trying to draw a pattern of some minimal length.
333            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
334                mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
335            } else {
336                // Give just a little extra time if they hit one of the first few dots
337                mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
338            }
339        }
340
341        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
342            if (mLockPatternUtils.checkPattern(pattern)) {
343                mLockPatternView
344                        .setDisplayMode(LockPatternView.DisplayMode.Correct);
345                mKeyguardStatusViewManager.setInstructionText("");
346                mKeyguardStatusViewManager.updateStatusLines(true);
347                mCallback.keyguardDone(true);
348                mCallback.reportSuccessfulUnlockAttempt();
349                KeyStore.getInstance().password(LockPatternUtils.patternToString(pattern));
350            } else {
351                boolean reportFailedAttempt = false;
352                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
353                    mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
354                }
355                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
356                if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
357                    mTotalFailedPatternAttempts++;
358                    mFailedPatternAttemptsSinceLastTimeout++;
359                    reportFailedAttempt = true;
360                }
361                if (mFailedPatternAttemptsSinceLastTimeout
362                        >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
363                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
364                    handleAttemptLockout(deadline);
365                } else {
366                    // TODO mUnlockIcon.setVisibility(View.VISIBLE);
367                    mKeyguardStatusViewManager.setInstructionText(
368                            getContext().getString(R.string.lockscreen_pattern_wrong));
369                    mKeyguardStatusViewManager.updateStatusLines(true);
370                    mLockPatternView.postDelayed(
371                            mCancelPatternRunnable,
372                            PATTERN_CLEAR_TIMEOUT_MS);
373                }
374
375                // Because the following can result in cleanUp() being called on this screen,
376                // member variables reset in cleanUp() shouldn't be accessed after this call.
377                if (reportFailedAttempt) {
378                    mCallback.reportFailedUnlockAttempt();
379                }
380            }
381        }
382    }
383
384    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
385        mLockPatternView.clearPattern();
386        mLockPatternView.setEnabled(false);
387        long elapsedRealtime = SystemClock.elapsedRealtime();
388        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
389
390            @Override
391            public void onTick(long millisUntilFinished) {
392                int secondsRemaining = (int) (millisUntilFinished / 1000);
393                mKeyguardStatusViewManager.setInstructionText(getContext().getString(
394                        R.string.lockscreen_too_many_failed_attempts_countdown,
395                        secondsRemaining));
396                mKeyguardStatusViewManager.updateStatusLines(true);
397            }
398
399            @Override
400            public void onFinish() {
401                mLockPatternView.setEnabled(true);
402                mKeyguardStatusViewManager.setInstructionText(getContext().getString(
403                        R.string.lockscreen_pattern_instructions));
404                mKeyguardStatusViewManager.updateStatusLines(true);
405                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
406                mFailedPatternAttemptsSinceLastTimeout = 0;
407                if (mEnableFallback) {
408                    updateFooter(FooterMode.ForgotLockPattern);
409                } else {
410                    updateFooter(FooterMode.Normal);
411                }
412            }
413        }.start();
414    }
415
416}
417