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;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.os.CountDownTimer;
22import android.os.SystemClock;
23import android.view.LayoutInflater;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.MotionEvent;
27import android.widget.Button;
28import android.widget.TextView;
29import android.text.format.DateFormat;
30import android.text.TextUtils;
31import android.util.Log;
32import com.android.internal.R;
33import com.android.internal.telephony.IccCard;
34import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
35import com.android.internal.widget.LockPatternUtils;
36import com.android.internal.widget.LockPatternView;
37import com.android.internal.widget.LockPatternView.Cell;
38
39import java.util.List;
40import java.util.Date;
41
42/**
43 * This is the screen that shows the 9 circle unlock widget and instructs
44 * the user how to unlock their device, or make an emergency call.
45 */
46class PatternUnlockScreen extends LinearLayoutWithDefaultTouchRecepient
47        implements KeyguardScreen, KeyguardUpdateMonitor.InfoCallback,
48        KeyguardUpdateMonitor.SimStateCallback {
49
50    private static final boolean DEBUG = false;
51    private static final String TAG = "UnlockScreen";
52
53    // how long before we clear the wrong pattern
54    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
55
56    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
57    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
58
59    // how long we stay awake after the user hits the first dot.
60    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
61
62    // how many cells the user has to cross before we poke the wakelock
63    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
64
65    private int mFailedPatternAttemptsSinceLastTimeout = 0;
66    private int mTotalFailedPatternAttempts = 0;
67    private CountDownTimer mCountdownTimer = null;
68
69    private final LockPatternUtils mLockPatternUtils;
70    private final KeyguardUpdateMonitor mUpdateMonitor;
71    private final KeyguardScreenCallback mCallback;
72
73    /**
74     * whether there is a fallback option available when the pattern is forgotten.
75     */
76    private boolean mEnableFallback;
77
78    private String mDateFormatString;
79
80    private TextView mCarrier;
81    private TextView mDate;
82
83    // are we showing battery information?
84    private boolean mShowingBatteryInfo = false;
85
86    // last known plugged in state
87    private boolean mPluggedIn = false;
88
89    // last known battery level
90    private int mBatteryLevel = 100;
91
92    private String mNextAlarm = null;
93
94    private String mInstructions = null;
95    private TextView mStatus1;
96    private TextView mStatusSep;
97    private TextView mStatus2;
98
99
100    private LockPatternView mLockPatternView;
101
102    private ViewGroup mFooterNormal;
103    private ViewGroup mFooterForgotPattern;
104
105    /**
106     * Keeps track of the last time we poked the wake lock during dispatching
107     * of the touch event, initalized to something gauranteed to make us
108     * poke it when the user starts drawing the pattern.
109     * @see #dispatchTouchEvent(android.view.MotionEvent)
110     */
111    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
112
113    /**
114     * Useful for clearing out the wrong pattern after a delay
115     */
116    private Runnable mCancelPatternRunnable = new Runnable() {
117        public void run() {
118            mLockPatternView.clearPattern();
119        }
120    };
121
122    private Button mForgotPatternButton;
123    private Button mEmergencyAlone;
124    private Button mEmergencyTogether;
125    private int mCreationOrientation;
126
127    enum FooterMode {
128        Normal,
129        ForgotLockPattern,
130        VerifyUnlocked
131    }
132
133    private void updateFooter(FooterMode mode) {
134        switch (mode) {
135            case Normal:
136                mFooterNormal.setVisibility(View.VISIBLE);
137                mFooterForgotPattern.setVisibility(View.GONE);
138                break;
139            case ForgotLockPattern:
140                mFooterNormal.setVisibility(View.GONE);
141                mFooterForgotPattern.setVisibility(View.VISIBLE);
142                mForgotPatternButton.setVisibility(View.VISIBLE);
143                break;
144            case VerifyUnlocked:
145                mFooterNormal.setVisibility(View.GONE);
146                mFooterForgotPattern.setVisibility(View.GONE);
147        }
148    }
149
150    /**
151     * @param context The context.
152     * @param configuration
153     * @param lockPatternUtils Used to lookup lock pattern settings.
154     * @param updateMonitor Used to lookup state affecting keyguard.
155     * @param callback Used to notify the manager when we're done, etc.
156     * @param totalFailedAttempts The current number of failed attempts.
157     * @param enableFallback True if a backup unlock option is available when the user has forgotten
158     *        their pattern (e.g they have a google account so we can show them the account based
159     *        backup option).
160     */
161    PatternUnlockScreen(Context context,
162                 Configuration configuration, LockPatternUtils lockPatternUtils,
163                 KeyguardUpdateMonitor updateMonitor,
164                 KeyguardScreenCallback callback,
165                 int totalFailedAttempts) {
166        super(context);
167        mLockPatternUtils = lockPatternUtils;
168        mUpdateMonitor = updateMonitor;
169        mCallback = callback;
170        mTotalFailedPatternAttempts = totalFailedAttempts;
171        mFailedPatternAttemptsSinceLastTimeout =
172            totalFailedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT;
173
174        if (DEBUG) Log.d(TAG,
175            "UnlockScreen() ctor: totalFailedAttempts="
176                 + totalFailedAttempts + ", mFailedPat...="
177                 + mFailedPatternAttemptsSinceLastTimeout
178                 );
179
180        mCreationOrientation = configuration.orientation;
181
182        LayoutInflater inflater = LayoutInflater.from(context);
183        if (mCreationOrientation != Configuration.ORIENTATION_LANDSCAPE) {
184            inflater.inflate(R.layout.keyguard_screen_unlock_portrait, this, true);
185        } else {
186            inflater.inflate(R.layout.keyguard_screen_unlock_landscape, this, true);
187        }
188
189        mCarrier = (TextView) findViewById(R.id.carrier);
190        mDate = (TextView) findViewById(R.id.date);
191
192        mDateFormatString = getContext().getString(R.string.full_wday_month_day_no_year);
193        refreshTimeAndDateDisplay();
194
195        mStatus1 = (TextView) findViewById(R.id.status1);
196        mStatusSep = (TextView) findViewById(R.id.statusSep);
197        mStatus2 = (TextView) findViewById(R.id.status2);
198
199        resetStatusInfo();
200
201
202        mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern);
203
204        mFooterNormal = (ViewGroup) findViewById(R.id.footerNormal);
205        mFooterForgotPattern = (ViewGroup) findViewById(R.id.footerForgotPattern);
206
207        // emergency call buttons
208        final OnClickListener emergencyClick = new OnClickListener() {
209            public void onClick(View v) {
210                mCallback.takeEmergencyCallAction();
211            }
212        };
213
214        mEmergencyAlone = (Button) findViewById(R.id.emergencyCallAlone);
215        mEmergencyAlone.setFocusable(false); // touch only!
216        mEmergencyAlone.setOnClickListener(emergencyClick);
217        mEmergencyTogether = (Button) findViewById(R.id.emergencyCallTogether);
218        mEmergencyTogether.setFocusable(false);
219        mEmergencyTogether.setOnClickListener(emergencyClick);
220        refreshEmergencyButtonText();
221
222        mForgotPatternButton = (Button) findViewById(R.id.forgotPattern);
223        mForgotPatternButton.setText(R.string.lockscreen_forgot_pattern_button_text);
224        mForgotPatternButton.setOnClickListener(new OnClickListener() {
225
226            public void onClick(View v) {
227                mCallback.forgotPattern(true);
228            }
229        });
230
231        // make it so unhandled touch events within the unlock screen go to the
232        // lock pattern view.
233        setDefaultTouchRecepient(mLockPatternView);
234
235        mLockPatternView.setSaveEnabled(false);
236        mLockPatternView.setFocusable(false);
237        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
238
239        // stealth mode will be the same for the life of this screen
240        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled());
241
242        // vibrate mode will be the same for the life of this screen
243        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
244
245        // assume normal footer mode for now
246        updateFooter(FooterMode.Normal);
247
248        updateMonitor.registerInfoCallback(this);
249        updateMonitor.registerSimStateCallback(this);
250        setFocusableInTouchMode(true);
251
252        // Required to get Marquee to work.
253        mCarrier.setSelected(true);
254        mCarrier.setTextColor(0xffffffff);
255
256        // until we get an update...
257        mCarrier.setText(
258                LockScreen.getCarrierString(
259                        mUpdateMonitor.getTelephonyPlmn(),
260                        mUpdateMonitor.getTelephonySpn()));
261    }
262
263    private void refreshEmergencyButtonText() {
264        mLockPatternUtils.updateEmergencyCallButtonState(mEmergencyAlone);
265        mLockPatternUtils.updateEmergencyCallButtonState(mEmergencyTogether);
266    }
267
268    public void setEnableFallback(boolean state) {
269        if (DEBUG) Log.d(TAG, "setEnableFallback(" + state + ")");
270        mEnableFallback = state;
271    }
272
273    private void resetStatusInfo() {
274        mInstructions = null;
275        mShowingBatteryInfo = mUpdateMonitor.shouldShowBatteryInfo();
276        mPluggedIn = mUpdateMonitor.isDevicePluggedIn();
277        mBatteryLevel = mUpdateMonitor.getBatteryLevel();
278        mNextAlarm = mLockPatternUtils.getNextAlarm();
279        updateStatusLines();
280    }
281
282    private void updateStatusLines() {
283        if (mInstructions != null) {
284            // instructions only
285            mStatus1.setText(mInstructions);
286            if (TextUtils.isEmpty(mInstructions)) {
287                mStatus1.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
288            } else {
289                mStatus1.setCompoundDrawablesWithIntrinsicBounds(
290                        R.drawable.ic_lock_idle_lock, 0, 0, 0);
291            }
292
293            mStatus1.setVisibility(View.VISIBLE);
294            mStatusSep.setVisibility(View.GONE);
295            mStatus2.setVisibility(View.GONE);
296        } else if (mShowingBatteryInfo && mNextAlarm == null) {
297            // battery only
298            if (mPluggedIn) {
299              if (mBatteryLevel >= 100) {
300                mStatus1.setText(getContext().getString(R.string.lockscreen_charged));
301              } else {
302                  mStatus1.setText(getContext().getString(R.string.lockscreen_plugged_in, mBatteryLevel));
303              }
304            } else {
305                mStatus1.setText(getContext().getString(R.string.lockscreen_low_battery));
306            }
307            mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0);
308
309            mStatus1.setVisibility(View.VISIBLE);
310            mStatusSep.setVisibility(View.GONE);
311            mStatus2.setVisibility(View.GONE);
312
313        } else if (mNextAlarm != null && !mShowingBatteryInfo) {
314            // alarm only
315            mStatus1.setText(mNextAlarm);
316            mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0);
317
318            mStatus1.setVisibility(View.VISIBLE);
319            mStatusSep.setVisibility(View.GONE);
320            mStatus2.setVisibility(View.GONE);
321        } else if (mNextAlarm != null && mShowingBatteryInfo) {
322            // both battery and next alarm
323            mStatus1.setText(mNextAlarm);
324            mStatusSep.setText("|");
325            mStatus2.setText(getContext().getString(
326                    R.string.lockscreen_battery_short,
327                    Math.min(100, mBatteryLevel)));
328            mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_alarm, 0, 0, 0);
329            if (mPluggedIn) {
330                mStatus2.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_charging, 0, 0, 0);
331            } else {
332                mStatus2.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
333            }
334
335            mStatus1.setVisibility(View.VISIBLE);
336            mStatusSep.setVisibility(View.VISIBLE);
337            mStatus2.setVisibility(View.VISIBLE);
338        } else {
339            // nothing specific to show; show general instructions
340            mStatus1.setText(R.string.lockscreen_pattern_instructions);
341            mStatus1.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_lock_idle_lock, 0, 0, 0);
342
343            mStatus1.setVisibility(View.VISIBLE);
344            mStatusSep.setVisibility(View.GONE);
345            mStatus2.setVisibility(View.GONE);
346        }
347    }
348
349
350    private void refreshTimeAndDateDisplay() {
351        mDate.setText(DateFormat.format(mDateFormatString, new Date()));
352    }
353
354
355    @Override
356    public boolean dispatchTouchEvent(MotionEvent ev) {
357        // as long as the user is entering a pattern (i.e sending a touch
358        // event that was handled by this screen), keep poking the
359        // wake lock so that the screen will stay on.
360        final boolean result = super.dispatchTouchEvent(ev);
361        if (result &&
362                ((SystemClock.elapsedRealtime() - mLastPokeTime)
363                        >  (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
364            mLastPokeTime = SystemClock.elapsedRealtime();
365        }
366        return result;
367    }
368
369
370    // ---------- InfoCallback
371
372    /** {@inheritDoc} */
373    public void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, int batteryLevel) {
374        mShowingBatteryInfo = showBatteryInfo;
375        mPluggedIn = pluggedIn;
376        mBatteryLevel = batteryLevel;
377        updateStatusLines();
378    }
379
380    /** {@inheritDoc} */
381    public void onTimeChanged() {
382        refreshTimeAndDateDisplay();
383    }
384
385    /** {@inheritDoc} */
386    public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) {
387        mCarrier.setText(LockScreen.getCarrierString(plmn, spn));
388    }
389
390    /** {@inheritDoc} */
391    public void onRingerModeChanged(int state) {
392        // not currently used
393    }
394
395    // ---------- SimStateCallback
396
397    /** {@inheritDoc} */
398    public void onSimStateChanged(IccCard.State simState) {
399    }
400
401    @Override
402    protected void onAttachedToWindow() {
403        super.onAttachedToWindow();
404        if (LockPatternKeyguardView.DEBUG_CONFIGURATION) {
405            Log.v(TAG, "***** PATTERN ATTACHED TO WINDOW");
406            Log.v(TAG, "Cur orient=" + mCreationOrientation
407                    + ", new config=" + getResources().getConfiguration());
408        }
409        if (getResources().getConfiguration().orientation != mCreationOrientation) {
410            mCallback.recreateMe(getResources().getConfiguration());
411        }
412    }
413
414
415    /** {@inheritDoc} */
416    @Override
417    protected void onConfigurationChanged(Configuration newConfig) {
418        super.onConfigurationChanged(newConfig);
419        if (LockPatternKeyguardView.DEBUG_CONFIGURATION) {
420            Log.v(TAG, "***** PATTERN CONFIGURATION CHANGED");
421            Log.v(TAG, "Cur orient=" + mCreationOrientation
422                    + ", new config=" + getResources().getConfiguration());
423        }
424        if (newConfig.orientation != mCreationOrientation) {
425            mCallback.recreateMe(newConfig);
426        }
427    }
428
429    /** {@inheritDoc} */
430    public void onKeyboardChange(boolean isKeyboardOpen) {}
431
432    /** {@inheritDoc} */
433    public boolean needsInput() {
434        return false;
435    }
436
437    /** {@inheritDoc} */
438    public void onPause() {
439        if (mCountdownTimer != null) {
440            mCountdownTimer.cancel();
441            mCountdownTimer = null;
442        }
443    }
444
445    /** {@inheritDoc} */
446    public void onResume() {
447        // reset header
448        resetStatusInfo();
449
450        // reset lock pattern
451        mLockPatternView.enableInput();
452        mLockPatternView.setEnabled(true);
453        mLockPatternView.clearPattern();
454
455        // show "forgot pattern?" button if we have an alternate authentication method
456        mForgotPatternButton.setVisibility(mCallback.doesFallbackUnlockScreenExist()
457                ? View.VISIBLE : View.INVISIBLE);
458
459        // if the user is currently locked out, enforce it.
460        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
461        if (deadline != 0) {
462            handleAttemptLockout(deadline);
463        }
464
465        // the footer depends on how many total attempts the user has failed
466        if (mCallback.isVerifyUnlockOnly()) {
467            updateFooter(FooterMode.VerifyUnlocked);
468        } else if (mEnableFallback &&
469                (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
470            updateFooter(FooterMode.ForgotLockPattern);
471        } else {
472            updateFooter(FooterMode.Normal);
473        }
474
475        refreshEmergencyButtonText();
476    }
477
478    /** {@inheritDoc} */
479    public void cleanUp() {
480        mUpdateMonitor.removeCallback(this);
481    }
482
483    @Override
484    public void onWindowFocusChanged(boolean hasWindowFocus) {
485        super.onWindowFocusChanged(hasWindowFocus);
486        if (hasWindowFocus) {
487            // when timeout dialog closes we want to update our state
488            onResume();
489        }
490    }
491
492    private class UnlockPatternListener
493            implements LockPatternView.OnPatternListener {
494
495        public void onPatternStart() {
496            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
497        }
498
499        public void onPatternCleared() {
500        }
501
502        public void onPatternCellAdded(List<Cell> pattern) {
503            // To guard against accidental poking of the wakelock, look for
504            // the user actually trying to draw a pattern of some minimal length.
505            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
506                mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
507            } else {
508                // Give just a little extra time if they hit one of the first few dots
509                mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
510            }
511        }
512
513        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
514            if (mLockPatternUtils.checkPattern(pattern)) {
515                mLockPatternView
516                        .setDisplayMode(LockPatternView.DisplayMode.Correct);
517                mInstructions = "";
518                updateStatusLines();
519                mCallback.keyguardDone(true);
520                mCallback.reportSuccessfulUnlockAttempt();
521            } else {
522                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
523                    mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
524                }
525                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
526                if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
527                    mTotalFailedPatternAttempts++;
528                    mFailedPatternAttemptsSinceLastTimeout++;
529                    mCallback.reportFailedUnlockAttempt();
530                }
531                if (mFailedPatternAttemptsSinceLastTimeout >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
532                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
533                    handleAttemptLockout(deadline);
534                } else {
535                    // TODO mUnlockIcon.setVisibility(View.VISIBLE);
536                    mInstructions = getContext().getString(R.string.lockscreen_pattern_wrong);
537                    updateStatusLines();
538                    mLockPatternView.postDelayed(
539                            mCancelPatternRunnable,
540                            PATTERN_CLEAR_TIMEOUT_MS);
541                }
542            }
543        }
544    }
545
546    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
547        mLockPatternView.clearPattern();
548        mLockPatternView.setEnabled(false);
549        long elapsedRealtime = SystemClock.elapsedRealtime();
550        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
551
552            @Override
553            public void onTick(long millisUntilFinished) {
554                int secondsRemaining = (int) (millisUntilFinished / 1000);
555                mInstructions = getContext().getString(
556                        R.string.lockscreen_too_many_failed_attempts_countdown,
557                        secondsRemaining);
558                updateStatusLines();
559            }
560
561            @Override
562            public void onFinish() {
563                mLockPatternView.setEnabled(true);
564                mInstructions = getContext().getString(R.string.lockscreen_pattern_instructions);
565                updateStatusLines();
566                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
567                mFailedPatternAttemptsSinceLastTimeout = 0;
568                if (mEnableFallback) {
569                    updateFooter(FooterMode.ForgotLockPattern);
570                } else {
571                    updateFooter(FooterMode.Normal);
572                }
573            }
574        }.start();
575    }
576
577    public void onPhoneStateChanged(String newState) {
578        refreshEmergencyButtonText();
579    }
580}
581