diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index e7e750db6b25e..c2e1b965b314b 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7001,11 +7001,6 @@ - - - - - diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index d6be31339b2ed..50558547a6a9d 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -2583,33 +2583,6 @@ please see styles_device_defaults.xml. @drawable/ic_media_route_holo_light - - - - - diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 74424593a0a47..dac33527f23f4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1431,9 +1431,6 @@ - - - diff --git a/packages/Keyguard/res/drawable-hdpi/ic_done_wht.png b/packages/Keyguard/res/drawable-hdpi/ic_done_wht.png new file mode 100644 index 0000000000000..82c01ef5cda7d Binary files /dev/null and b/packages/Keyguard/res/drawable-hdpi/ic_done_wht.png differ diff --git a/packages/Keyguard/res/drawable-hdpi/ic_input_delete.png b/packages/Keyguard/res/drawable-hdpi/ic_input_delete.png deleted file mode 100644 index 5d638bd511ed7..0000000000000 Binary files a/packages/Keyguard/res/drawable-hdpi/ic_input_delete.png and /dev/null differ diff --git a/packages/Keyguard/res/drawable-mdpi/ic_done_wht.png b/packages/Keyguard/res/drawable-mdpi/ic_done_wht.png new file mode 100644 index 0000000000000..8c169301911a7 Binary files /dev/null and b/packages/Keyguard/res/drawable-mdpi/ic_done_wht.png differ diff --git a/packages/Keyguard/res/drawable-mdpi/ic_input_delete.png b/packages/Keyguard/res/drawable-mdpi/ic_input_delete.png deleted file mode 100644 index 47c8708782a27..0000000000000 Binary files a/packages/Keyguard/res/drawable-mdpi/ic_input_delete.png and /dev/null differ diff --git a/packages/Keyguard/res/drawable-xhdpi/ic_done_wht.png b/packages/Keyguard/res/drawable-xhdpi/ic_done_wht.png new file mode 100644 index 0000000000000..6a4d8a7b01ea9 Binary files /dev/null and b/packages/Keyguard/res/drawable-xhdpi/ic_done_wht.png differ diff --git a/packages/Keyguard/res/drawable-xhdpi/ic_input_delete.png b/packages/Keyguard/res/drawable-xhdpi/ic_input_delete.png deleted file mode 100644 index 8b822d9e37c25..0000000000000 Binary files a/packages/Keyguard/res/drawable-xhdpi/ic_input_delete.png and /dev/null differ diff --git a/packages/Keyguard/res/drawable-xxhdpi/ic_done_wht.png b/packages/Keyguard/res/drawable-xxhdpi/ic_done_wht.png new file mode 100644 index 0000000000000..4c04ba28b58aa Binary files /dev/null and b/packages/Keyguard/res/drawable-xxhdpi/ic_done_wht.png differ diff --git a/packages/Keyguard/res/drawable-xxxhdpi/ic_done_wht.png b/packages/Keyguard/res/drawable-xxxhdpi/ic_done_wht.png new file mode 100644 index 0000000000000..bd6c4df14a888 Binary files /dev/null and b/packages/Keyguard/res/drawable-xxxhdpi/ic_done_wht.png differ diff --git a/packages/Keyguard/res/drawable/ic_backspace_24dp.xml b/packages/Keyguard/res/drawable/ic_backspace_24dp.xml new file mode 100644 index 0000000000000..9e5016d991da7 --- /dev/null +++ b/packages/Keyguard/res/drawable/ic_backspace_24dp.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ripple_drawable.xml b/packages/Keyguard/res/drawable/ripple_drawable.xml similarity index 100% rename from packages/SystemUI/res/drawable/ripple_drawable.xml rename to packages/Keyguard/res/drawable/ripple_drawable.xml diff --git a/packages/Keyguard/res/layout-land/keyguard_simple_host_view.xml b/packages/Keyguard/res/layout-land/keyguard_simple_host_view.xml deleted file mode 100644 index 49b42123ce68d..0000000000000 --- a/packages/Keyguard/res/layout-land/keyguard_simple_host_view.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/Keyguard/res/layout/keyguard_bouncer.xml b/packages/Keyguard/res/layout/keyguard_bouncer.xml index 489c5f5a82867..c85f04c432c80 100644 --- a/packages/Keyguard/res/layout/keyguard_bouncer.xml +++ b/packages/Keyguard/res/layout/keyguard_bouncer.xml @@ -16,7 +16,8 @@ + android:layout_height="match_parent" + android:background="@android:color/transparent" > + + + + + + diff --git a/packages/Keyguard/res/layout/keyguard_pin_view.xml b/packages/Keyguard/res/layout/keyguard_pin_view.xml index a8e330be7537e..a33f95bd5663d 100644 --- a/packages/Keyguard/res/layout/keyguard_pin_view.xml +++ b/packages/Keyguard/res/layout/keyguard_pin_view.xml @@ -18,215 +18,202 @@ --> - + android:layout_height="match_parent" + androidprv:layout_maxWidth="@dimen/keyguard_security_width" + androidprv:layout_maxHeight="@dimen/keyguard_security_max_height" + android:orientation="vertical" + android:contentDescription="@string/keyguard_accessibility_pin_unlock" + > + - - - + android:id="@+id/keyguard_bouncer_frame" + android:layout_width="match_parent" + android:layout_height="0dp" + android:orientation="vertical" + android:layout_weight="1" + android:layoutDirection="ltr" + > + + + - - - - - - - - - - - - - - - - - - - - - + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_alignParentBottom="true" + android:background="#28FFFFFF" + /> + + + + + + + + + + + + + + + + + + + + + + android:id="@+id/keyguard_selector_fade_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="bottom|center_horizontal" + android:gravity="center_horizontal"/> diff --git a/packages/Keyguard/res/layout/keyguard_sim_pin_view.xml b/packages/Keyguard/res/layout/keyguard_sim_pin_view.xml index e167817b99fe6..4f35daf1b23ba 100644 --- a/packages/Keyguard/res/layout/keyguard_sim_pin_view.xml +++ b/packages/Keyguard/res/layout/keyguard_sim_pin_view.xml @@ -18,213 +18,205 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/res-auto" + android:id="@+id/keyguard_sim_pin_view" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + androidprv:layout_maxWidth="@dimen/keyguard_security_width" + androidprv:layout_maxHeight="@dimen/keyguard_security_max_height" + android:gravity="center_horizontal" + android:contentDescription="@string/keyguard_accessibility_pin_unlock"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:src="@drawable/ic_lockscreen_sim"/> + android:layout_width="match_parent" + android:layout_height="wrap_content" + /> - - - - - - - - - - - - - - - - - - - - - - - - - + android:id="@+id/keyguard_bouncer_frame" + android:layout_width="match_parent" + android:layout_height="0dp" + android:orientation="vertical" + android:layout_weight="1" + android:layoutDirection="ltr" + > + + + + + + + + + + + + + + + + + + + + + + + + + + android:id="@+id/keyguard_selector_fade_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="bottom|center_horizontal" + android:gravity="center_horizontal"/> diff --git a/packages/Keyguard/res/layout/keyguard_sim_puk_view.xml b/packages/Keyguard/res/layout/keyguard_sim_puk_view.xml index ac798caefec6e..41484e223a330 100644 --- a/packages/Keyguard/res/layout/keyguard_sim_puk_view.xml +++ b/packages/Keyguard/res/layout/keyguard_sim_puk_view.xml @@ -19,212 +19,203 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/res-auto" + android:id="@+id/keyguard_sim_puk_view" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + androidprv:layout_maxWidth="@dimen/keyguard_security_width" + androidprv:layout_maxHeight="@dimen/keyguard_security_max_height" + android:gravity="center_horizontal"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:src="@drawable/ic_lockscreen_sim"/> + android:layout_width="match_parent" + android:layout_height="wrap_content" + /> - - - - - - - - - - - - - - - - - - - - - - - - - + android:id="@+id/keyguard_bouncer_frame" + android:layout_width="match_parent" + android:layout_height="0dp" + android:orientation="vertical" + android:layout_weight="1" + android:layoutDirection="ltr" + > + + + + + + + + + + + + + + + + + + + + + + + + + + android:id="@+id/keyguard_selector_fade_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="bottom|center_horizontal" + android:gravity="center_horizontal"/> diff --git a/packages/Keyguard/res/layout-port/keyguard_simple_host_view.xml b/packages/Keyguard/res/layout/keyguard_simple_host_view.xml similarity index 94% rename from packages/Keyguard/res/layout-port/keyguard_simple_host_view.xml rename to packages/Keyguard/res/layout/keyguard_simple_host_view.xml index ed600b0520e02..4336e1cf75509 100644 --- a/packages/Keyguard/res/layout-port/keyguard_simple_host_view.xml +++ b/packages/Keyguard/res/layout/keyguard_simple_host_view.xml @@ -29,7 +29,8 @@ - + + ABC DEF diff --git a/packages/Keyguard/res/values/attrs.xml b/packages/Keyguard/res/values/attrs.xml index 2410b6ae33270..91001400912cd 100644 --- a/packages/Keyguard/res/values/attrs.xml +++ b/packages/Keyguard/res/values/attrs.xml @@ -134,6 +134,10 @@ + + + + diff --git a/packages/Keyguard/res/values/dimens.xml b/packages/Keyguard/res/values/dimens.xml index 4b113ff896b6c..375cf32fe5a2e 100644 --- a/packages/Keyguard/res/values/dimens.xml +++ b/packages/Keyguard/res/values/dimens.xml @@ -119,6 +119,9 @@ 400dp + + 460dp + 8dp @@ -164,6 +167,12 @@ 32dp + + 9dp + + + 8dp + 10dp diff --git a/packages/Keyguard/res/values/styles.xml b/packages/Keyguard/res/values/styles.xml index 11142cf686b1b..cdd64892dbee4 100644 --- a/packages/Keyguard/res/values/styles.xml +++ b/packages/Keyguard/res/values/styles.xml @@ -19,36 +19,20 @@ - + - - - diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java index aae92e81b4618..c49c318aa55b2 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -17,12 +17,9 @@ package com.android.keyguard; import android.content.Context; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.CountDownTimer; import android.os.SystemClock; -import android.text.Editable; -import android.text.TextWatcher; import android.util.AttributeSet; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -38,9 +35,8 @@ import com.android.internal.widget.LockPatternUtils; * Base class for PIN and password unlock screens. */ public abstract class KeyguardAbsKeyInputView extends LinearLayout - implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + implements KeyguardSecurityView { protected KeyguardSecurityCallback mCallback; - protected TextView mPasswordEntry; protected LockPatternUtils mLockPatternUtils; protected SecurityMessageDisplay mSecurityMessageDisplay; protected View mEcaView; @@ -70,9 +66,7 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout public void reset() { // start fresh - mPasswordEntry.setText(""); - mPasswordEntry.requestFocus(); - + resetPasswordText(false /* animate */); // if the user is currently locked out, enforce it. long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); if (shouldLockout(deadline)) { @@ -93,34 +87,6 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout @Override protected void onFinishInflate() { mLockPatternUtils = new LockPatternUtils(mContext); - - mPasswordEntry = (TextView) findViewById(getPasswordTextViewId()); - mPasswordEntry.setOnEditorActionListener(this); - mPasswordEntry.addTextChangedListener(this); - - // Set selected property on so the view can send accessibility events. - mPasswordEntry.setSelected(true); - - // Poke the wakelock any time the text is selected or modified - mPasswordEntry.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mCallback.userActivity(0); // TODO: customize timeout for text? - } - }); - - mPasswordEntry.addTextChangedListener(new TextWatcher() { - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - public void afterTextChanged(Editable s) { - if (mCallback != null) { - mCallback.userActivity(0); - } - } - }); mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); mEcaView = findViewById(R.id.keyguard_selector_fade_container); View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); @@ -129,12 +95,6 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout } } - @Override - protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - // send focus to the password field - return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); - } - /* * Override this if you have a different string for "wrong password" * @@ -145,7 +105,7 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout } protected void verifyPasswordAndUnlock() { - String entry = mPasswordEntry.getText().toString(); + String entry = getPasswordText(); if (mLockPatternUtils.checkPassword(entry)) { mCallback.reportUnlockAttempt(true); mCallback.dismiss(true); @@ -162,12 +122,16 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout } mSecurityMessageDisplay.setMessage(getWrongPasswordStringId(), true); } - mPasswordEntry.setText(""); + resetPasswordText(true /* animate */); } + protected abstract void resetPasswordText(boolean animate); + protected abstract String getPasswordText(); + protected abstract void setPasswordEntryEnabled(boolean enabled); + // Prevent user from using the PIN/Password entry until scheduled deadline. protected void handleAttemptLockout(long elapsedRealtimeDeadline) { - mPasswordEntry.setEnabled(false); + setPasswordEntryEnabled(false); long elapsedRealtime = SystemClock.elapsedRealtime(); new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { @@ -192,17 +156,6 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout return false; } - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Check if this was the result of hitting the enter key - if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE - || actionId == EditorInfo.IME_ACTION_NEXT) { - verifyPasswordAndUnlock(); - return true; - } - return false; - } - @Override public boolean needsInput() { return false; @@ -223,21 +176,6 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout return mCallback; } - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - if (mCallback != null) { - mCallback.userActivity(KeyguardConstants.DIGIT_PRESS_WAKE_MILLIS); - } - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - } - // Cause a VIRTUAL_KEY vibration public void doHapticKeyClick() { if (mEnableHaptics) { diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java index 1f3c17692c7b6..ec5d040eb681c 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java @@ -28,8 +28,7 @@ import android.widget.TextView.OnEditorActionListener; /** * Displays a PIN pad for unlocking. */ -public class KeyguardPINView extends KeyguardAbsKeyInputView - implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { +public class KeyguardPINView extends KeyguardPinBasedInputView { private final AppearAnimationUtils mAppearAnimationUtils; private ViewGroup mKeyguardBouncerFrame; @@ -49,12 +48,12 @@ public class KeyguardPINView extends KeyguardAbsKeyInputView } protected void resetState() { + super.resetState(); if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); } else { mSecurityMessageDisplay.setMessage(R.string.kg_pin_instructions, false); } - mPasswordEntry.setEnabled(true); } @Override @@ -72,54 +71,6 @@ public class KeyguardPINView extends KeyguardAbsKeyInputView mRow2 = (ViewGroup) findViewById(R.id.row2); mRow3 = (ViewGroup) findViewById(R.id.row3); mDivider = findViewById(R.id.divider); - final View ok = findViewById(R.id.key_enter); - if (ok != null) { - ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - doHapticKeyClick(); - if (mPasswordEntry.isEnabled()) { - verifyPasswordAndUnlock(); - } - } - }); - ok.setOnHoverListener(new LiftToActivateListener(getContext())); - } - - // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, - // not a separate view - View pinDelete = findViewById(R.id.delete_button); - if (pinDelete != null) { - pinDelete.setVisibility(View.VISIBLE); - pinDelete.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - // check for time-based lockouts - if (mPasswordEntry.isEnabled()) { - CharSequence str = mPasswordEntry.getText(); - if (str.length() > 0) { - mPasswordEntry.setText(str.subSequence(0, str.length()-1)); - } - } - doHapticKeyClick(); - } - }); - pinDelete.setOnLongClickListener(new View.OnLongClickListener() { - public boolean onLongClick(View v) { - // check for time-based lockouts - if (mPasswordEntry.isEnabled()) { - mPasswordEntry.setText(""); - } - doHapticKeyClick(); - return true; - } - }); - } - - mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); - mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER - | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - - mPasswordEntry.requestFocus(); } @Override diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java index 0c385da9cd5a0..9f820f8da6f58 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java @@ -17,15 +17,19 @@ package com.android.keyguard; import android.content.Context; +import android.graphics.Rect; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.text.method.TextKeyListener; import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import java.util.List; @@ -40,6 +44,7 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView private final boolean mShowImeAtScreenOn; InputMethodManager mImm; + private TextView mPasswordEntry; public KeyguardPasswordView(Context context) { this(context, null); @@ -81,6 +86,12 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView mImm.hideSoftInputFromWindow(getWindowToken(), 0); } + @Override + public void reset() { + super.reset(); + mPasswordEntry.requestFocus(); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -90,9 +101,12 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView mImm = (InputMethodManager) getContext().getSystemService( Context.INPUT_METHOD_SERVICE); + mPasswordEntry = (TextView) findViewById(getPasswordTextViewId()); mPasswordEntry.setKeyListener(TextKeyListener.getInstance()); mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + mPasswordEntry.setOnEditorActionListener(this); + mPasswordEntry.addTextChangedListener(this); // Poke the wakelock any time the text is selected or modified mPasswordEntry.setOnClickListener(new OnClickListener() { @@ -101,6 +115,9 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView } }); + // Set selected property on so the view can send accessibility events. + mPasswordEntry.setSelected(true); + mPasswordEntry.addTextChangedListener(new TextWatcher() { public void onTextChanged(CharSequence s, int start, int before, int count) { } @@ -142,6 +159,27 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView } } + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // send focus to the password field + return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); + } + + @Override + protected void resetPasswordText(boolean animate) { + mPasswordEntry.setText(""); + } + + @Override + protected String getPasswordText() { + return mPasswordEntry.getText().toString(); + } + + @Override + protected void setPasswordEntryEnabled(boolean enabled) { + mPasswordEntry.setEnabled(enabled); + } + /** * Method adapted from com.android.inputmethod.latin.Utils * @@ -205,4 +243,30 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView setAlpha(0); animate().alpha(1).withLayer().setDuration(200); } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (mCallback != null) { + mCallback.userActivity(KeyguardConstants.DIGIT_PRESS_WAKE_MILLIS); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Check if this was the result of hitting the enter key + if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT) { + verifyPasswordAndUnlock(); + return true; + } + return false; + } } diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java new file mode 100644 index 0000000000000..1ef873fb46cd5 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2014 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.keyguard; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; + +/** + * A Pin based Keyguard input view + */ +public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView + implements View.OnKeyListener { + + protected PasswordTextView mPasswordEntry; + private View mOkButton; + private View mDeleteButton; + private View mButton0; + private View mButton1; + private View mButton2; + private View mButton3; + private View mButton4; + private View mButton5; + private View mButton6; + private View mButton7; + private View mButton8; + private View mButton9; + + public KeyguardPinBasedInputView(Context context) { + this(context, null); + } + + public KeyguardPinBasedInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void reset() { + mPasswordEntry.requestFocus(); + super.reset(); + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // send focus to the password field + return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); + } + + protected void resetState() { + mPasswordEntry.setEnabled(true); + } + + @Override + protected void setPasswordEntryEnabled(boolean enabled) { + mPasswordEntry.setEnabled(enabled); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (KeyEvent.isConfirmKey(keyCode)) { + performClick(mOkButton); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + performClick(mDeleteButton); + return true; + } + if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + int number = keyCode - KeyEvent.KEYCODE_0 ; + performNumberClick(number); + return true; + } + return super.onKeyDown(keyCode, event); + } + + private void performClick(View view) { + view.performClick(); + } + + private void performNumberClick(int number) { + switch (number) { + case 0: + performClick(mButton0); + break; + case 1: + performClick(mButton1); + break; + case 2: + performClick(mButton2); + break; + case 3: + performClick(mButton3); + break; + case 4: + performClick(mButton4); + break; + case 5: + performClick(mButton5); + break; + case 6: + performClick(mButton6); + break; + case 7: + performClick(mButton7); + break; + case 8: + performClick(mButton8); + break; + case 9: + performClick(mButton9); + break; + } + } + + @Override + protected void resetPasswordText(boolean animate) { + mPasswordEntry.reset(animate); + } + + @Override + protected String getPasswordText() { + return mPasswordEntry.getText(); + } + + @Override + protected void onFinishInflate() { + mPasswordEntry = (PasswordTextView) findViewById(getPasswordTextViewId()); + mPasswordEntry.setOnKeyListener(this); + + // Set selected property on so the view can send accessibility events. + mPasswordEntry.setSelected(true); + + // Poke the wakelock any time the text is selected or modified + mPasswordEntry.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mCallback.userActivity(0); // TODO: customize timeout for text? + } + }); + + mOkButton = findViewById(R.id.key_enter); + if (mOkButton != null) { + mOkButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + doHapticKeyClick(); + if (mPasswordEntry.isEnabled()) { + verifyPasswordAndUnlock(); + } + } + }); + mOkButton.setOnHoverListener(new LiftToActivateListener(getContext())); + } + + mDeleteButton = findViewById(R.id.delete_button); + if (mDeleteButton != null) { + mDeleteButton.setVisibility(View.VISIBLE); + mDeleteButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + mPasswordEntry.deleteLastChar(); + } + doHapticKeyClick(); + } + }); + mDeleteButton.setOnLongClickListener(new View.OnLongClickListener() { + public boolean onLongClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + resetPasswordText(true /* animate */); + } + doHapticKeyClick(); + return true; + } + }); + } + mButton0 = findViewById(R.id.key0); + mButton1 = findViewById(R.id.key1); + mButton2 = findViewById(R.id.key2); + mButton3 = findViewById(R.id.key3); + mButton4 = findViewById(R.id.key4); + mButton5 = findViewById(R.id.key5); + mButton6 = findViewById(R.id.key6); + mButton7 = findViewById(R.id.key7); + mButton8 = findViewById(R.id.key8); + mButton9 = findViewById(R.id.key9); + + mPasswordEntry.requestFocus(); + super.onFinishInflate(); + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + onKeyDown(keyCode, event); + return true; + } + return false; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java index 09c4e7ce7d43b..cc90bcb6cce0a 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java @@ -38,8 +38,7 @@ import android.widget.TextView.OnEditorActionListener; /** * Displays a PIN pad for unlocking. */ -public class KeyguardSimPinView extends KeyguardAbsKeyInputView - implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { +public class KeyguardSimPinView extends KeyguardPinBasedInputView { private static final String LOG_TAG = "KeyguardSimPinView"; private static final boolean DEBUG = KeyguardConstants.DEBUG; public static final String TAG = "KeyguardSimPinView"; @@ -58,8 +57,8 @@ public class KeyguardSimPinView extends KeyguardAbsKeyInputView } public void resetState() { + super.resetState(); mSecurityMessageDisplay.setMessage(R.string.kg_sim_pin_instructions, true); - mPasswordEntry.setEnabled(true); } private String getPinPasswordErrorMessage(int attemptsRemaining) { @@ -94,46 +93,6 @@ public class KeyguardSimPinView extends KeyguardAbsKeyInputView protected void onFinishInflate() { super.onFinishInflate(); - final View ok = findViewById(R.id.key_enter); - if (ok != null) { - ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - doHapticKeyClick(); - verifyPasswordAndUnlock(); - } - }); - } - - // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, - // not a separate view - View pinDelete = findViewById(R.id.delete_button); - if (pinDelete != null) { - pinDelete.setVisibility(View.VISIBLE); - pinDelete.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - CharSequence str = mPasswordEntry.getText(); - if (str.length() > 0) { - mPasswordEntry.setText(str.subSequence(0, str.length()-1)); - } - doHapticKeyClick(); - } - }); - pinDelete.setOnLongClickListener(new View.OnLongClickListener() { - public boolean onLongClick(View v) { - mPasswordEntry.setText(""); - doHapticKeyClick(); - return true; - } - }); - } - - mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); - mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER - | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - - mPasswordEntry.requestFocus(); - mSecurityMessageDisplay.setTimeout(0); // don't show ownerinfo/charging status by default if (mEcaView instanceof EmergencyCarrierArea) { ((EmergencyCarrierArea) mEcaView).setCarrierTextVisible(true); @@ -220,12 +179,12 @@ public class KeyguardSimPinView extends KeyguardAbsKeyInputView @Override protected void verifyPasswordAndUnlock() { - String entry = mPasswordEntry.getText().toString(); + String entry = mPasswordEntry.getText(); if (entry.length() < 4) { // otherwise, display a message to the user, and don't submit. mSecurityMessageDisplay.setMessage(R.string.kg_invalid_sim_pin_hint, true); - mPasswordEntry.setText(""); + resetPasswordText(true); mCallback.userActivity(0); return; } @@ -233,7 +192,7 @@ public class KeyguardSimPinView extends KeyguardAbsKeyInputView getSimUnlockProgressDialog().show(); if (mCheckSimPinThread == null) { - mCheckSimPinThread = new CheckSimPin(mPasswordEntry.getText().toString()) { + mCheckSimPinThread = new CheckSimPin(mPasswordEntry.getText()) { void onSimCheckResponse(final int result, final int attemptsRemaining) { post(new Runnable() { public void run() { @@ -262,7 +221,7 @@ public class KeyguardSimPinView extends KeyguardAbsKeyInputView if (DEBUG) Log.d(LOG_TAG, "verifyPasswordAndUnlock " + " CheckSimPin.onSimCheckResponse: " + result + " attemptsRemaining=" + attemptsRemaining); - mPasswordEntry.setText(""); + resetPasswordText(true /* animate */); } mCallback.userActivity(0); mCheckSimPinThread = null; diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java index 6215d3475bd17..b956e99e60859 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java @@ -39,8 +39,7 @@ import com.android.internal.telephony.PhoneConstants; /** * Displays a PIN pad for entering a PUK (Pin Unlock Kode) provided by a carrier. */ -public class KeyguardSimPukView extends KeyguardAbsKeyInputView - implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { +public class KeyguardSimPukView extends KeyguardPinBasedInputView { private static final String LOG_TAG = "KeyguardSimPukView"; private static final boolean DEBUG = KeyguardConstants.DEBUG; public static final String TAG = "KeyguardSimPukView"; @@ -85,7 +84,7 @@ public class KeyguardSimPukView extends KeyguardAbsKeyInputView msg = R.string.kg_invalid_confirm_pin_hint; } } - mPasswordEntry.setText(null); + resetPasswordText(true); if (msg != 0) { mSecurityMessageDisplay.setMessage(msg, true); } @@ -126,8 +125,8 @@ public class KeyguardSimPukView extends KeyguardAbsKeyInputView } public void resetState() { + super.resetState(); mStateMachine.reset(); - mPasswordEntry.setEnabled(true); } @Override @@ -145,46 +144,6 @@ public class KeyguardSimPukView extends KeyguardAbsKeyInputView protected void onFinishInflate() { super.onFinishInflate(); - final View ok = findViewById(R.id.key_enter); - if (ok != null) { - ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - doHapticKeyClick(); - verifyPasswordAndUnlock(); - } - }); - } - - // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, - // not a separate view - View pinDelete = findViewById(R.id.delete_button); - if (pinDelete != null) { - pinDelete.setVisibility(View.VISIBLE); - pinDelete.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - CharSequence str = mPasswordEntry.getText(); - if (str.length() > 0) { - mPasswordEntry.setText(str.subSequence(0, str.length()-1)); - } - doHapticKeyClick(); - } - }); - pinDelete.setOnLongClickListener(new View.OnLongClickListener() { - public boolean onLongClick(View v) { - mPasswordEntry.setText(""); - doHapticKeyClick(); - return true; - } - }); - } - - mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); - mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER - | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - - mPasswordEntry.requestFocus(); - mSecurityMessageDisplay.setTimeout(0); // don't show ownerinfo/charging status by default if (mEcaView instanceof EmergencyCarrierArea) { ((EmergencyCarrierArea) mEcaView).setCarrierTextVisible(true); @@ -276,7 +235,7 @@ public class KeyguardSimPukView extends KeyguardAbsKeyInputView private boolean checkPuk() { // make sure the puk is at least 8 digits long. if (mPasswordEntry.getText().length() == 8) { - mPukText = mPasswordEntry.getText().toString(); + mPukText = mPasswordEntry.getText(); return true; } return false; @@ -286,14 +245,14 @@ public class KeyguardSimPukView extends KeyguardAbsKeyInputView // make sure the PIN is between 4 and 8 digits int length = mPasswordEntry.getText().length(); if (length >= 4 && length <= 8) { - mPinText = mPasswordEntry.getText().toString(); + mPinText = mPasswordEntry.getText(); return true; } return false; } public boolean confirmPin() { - return mPinText.equals(mPasswordEntry.getText().toString()); + return mPinText.equals(mPasswordEntry.getText()); } private void updateSim() { diff --git a/packages/Keyguard/src/com/android/keyguard/NumPadKey.java b/packages/Keyguard/src/com/android/keyguard/NumPadKey.java index 532670f6a5d04..2d20b02bc9a4e 100644 --- a/packages/Keyguard/src/com/android/keyguard/NumPadKey.java +++ b/packages/Keyguard/src/com/android/keyguard/NumPadKey.java @@ -18,44 +18,54 @@ package com.android.keyguard; import android.content.Context; import android.content.res.TypedArray; -import android.text.SpannableStringBuilder; -import android.text.style.TextAppearanceSpan; +import android.graphics.drawable.Drawable; +import android.os.Debug; +import android.os.PowerManager; +import android.os.SystemClock; import android.util.AttributeSet; import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.View; -import android.widget.Button; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; import com.android.internal.widget.LockPatternUtils; -public class NumPadKey extends Button { +public class NumPadKey extends ViewGroup { // list of "ABC", etc per digit, starting with '0' static String sKlondike[]; - int mDigit = -1; - int mTextViewResId; - TextView mTextView = null; - boolean mEnableHaptics; + private int mDigit = -1; + private int mTextViewResId; + private PasswordTextView mTextView; + private TextView mDigitText; + private TextView mKlondikeText; + private boolean mEnableHaptics; + private PowerManager mPM; private View.OnClickListener mListener = new View.OnClickListener() { @Override public void onClick(View thisView) { - if (mTextView == null) { - if (mTextViewResId > 0) { - final View v = NumPadKey.this.getRootView().findViewById(mTextViewResId); - if (v != null && v instanceof TextView) { - mTextView = (TextView) v; - } + if (mTextView == null && mTextViewResId > 0) { + final View v = NumPadKey.this.getRootView().findViewById(mTextViewResId); + if (v != null && v instanceof PasswordTextView) { + mTextView = (PasswordTextView) v; } } - // check for time-based lockouts if (mTextView != null && mTextView.isEnabled()) { - mTextView.append(String.valueOf(mDigit)); + mTextView.append(Character.forDigit(mDigit, 10)); } + userActivity(); doHapticKeyClick(); } }; + public void userActivity() { + mPM.userActivity(SystemClock.uptimeMillis(), false); + } + public NumPadKey(Context context) { this(context, null); } @@ -66,10 +76,16 @@ public class NumPadKey extends Button { public NumPadKey(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + setFocusable(true); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumPadKey); - mDigit = a.getInt(R.styleable.NumPadKey_digit, mDigit); - setTextViewResId(a.getResourceId(R.styleable.NumPadKey_textView, 0)); + + try { + mDigit = a.getInt(R.styleable.NumPadKey_digit, mDigit); + mTextViewResId = a.getResourceId(R.styleable.NumPadKey_textView, 0); + } finally { + a.recycle(); + } setOnClickListener(mListener); setOnHoverListener(new LiftToActivateListener(context)); @@ -77,26 +93,32 @@ public class NumPadKey extends Button { mEnableHaptics = new LockPatternUtils(context).isTactileFeedbackEnabled(); - SpannableStringBuilder builder = new SpannableStringBuilder(); - builder.append(String.valueOf(mDigit)); + mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.keyguard_num_pad_key, this, true); + + mDigitText = (TextView) findViewById(R.id.digit_text); + mDigitText.setText(Integer.toString(mDigit)); + mKlondikeText = (TextView) findViewById(R.id.klondike_text); + if (mDigit >= 0) { if (sKlondike == null) { - sKlondike = context.getResources().getStringArray( - R.array.lockscreen_num_pad_klondike); + sKlondike = getResources().getStringArray(R.array.lockscreen_num_pad_klondike); } if (sKlondike != null && sKlondike.length > mDigit) { - final String extra = sKlondike[mDigit]; - final int extraLen = extra.length(); - if (extraLen > 0) { - builder.append(" "); - builder.append(extra); - builder.setSpan( - new TextAppearanceSpan(context, R.style.TextAppearance_NumPadKey_Klondike), - builder.length()-extraLen, builder.length(), 0); + String klondike = sKlondike[mDigit]; + final int len = klondike.length(); + if (len > 0) { + mKlondikeText.setText(klondike); + } else { + mKlondikeText.setVisibility(View.INVISIBLE); } } } - setText(builder); + + setBackground(mContext.getDrawable(R.drawable.ripple_drawable)); + setContentDescription(mDigitText.getText().toString() + mKlondikeText.getText().toString()); } @Override @@ -107,13 +129,27 @@ public class NumPadKey extends Button { ObscureSpeechDelegate.sAnnouncedHeadset = false; } - public void setTextView(TextView tv) { - mTextView = tv; + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + measureChildren(widthMeasureSpec, heightMeasureSpec); } - public void setTextViewResId(int resId) { - mTextView = null; - mTextViewResId = resId; + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int digitHeight = mDigitText.getMeasuredHeight(); + int klondikeHeight = mKlondikeText.getMeasuredHeight(); + int totalHeight = digitHeight + klondikeHeight; + int top = getHeight() / 2 - totalHeight / 2; + int centerX = getWidth() / 2; + int left = centerX - mDigitText.getMeasuredWidth() / 2; + int bottom = top + digitHeight; + mDigitText.layout(left, top, left + mDigitText.getMeasuredWidth(), bottom); + top = (int) (bottom - klondikeHeight * 0.35f); + bottom = top + klondikeHeight; + + left = centerX - mKlondikeText.getMeasuredWidth() / 2; + mKlondikeText.layout(left, top, left + mKlondikeText.getMeasuredWidth(), bottom); } // Cause a VIRTUAL_KEY vibration diff --git a/packages/Keyguard/src/com/android/keyguard/PasswordTextView.java b/packages/Keyguard/src/com/android/keyguard/PasswordTextView.java new file mode 100644 index 0000000000000..f3ba3a778a2a4 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/PasswordTextView.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2014 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.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.PowerManager; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.Stack; + +/** + * A View similar to a textView which contains password text and can animate when the text is + * changed + */ +public class PasswordTextView extends View { + + private static final float DOT_OVERSHOOT_FACTOR = 1.5f; + private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320; + private static final long APPEAR_DURATION = 160; + private static final long DISAPPEAR_DURATION = 160; + private static final long RESET_DELAY_PER_ELEMENT = 40; + private static final long RESET_MAX_DELAY = 200; + + /** + * The overlap between the text disappearing and the dot appearing animation + */ + private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130; + + /** + * The duration the text needs to stay there at least before it can morph into a dot + */ + private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100; + + /** + * The duration the text should be visible, starting with the appear animation + */ + private static final long TEXT_VISIBILITY_DURATION = 1300; + + /** + * The position in time from [0,1] where the overshoot should be finished and the settle back + * animation of the dot should start + */ + private static final float OVERSHOOT_TIME_POSITION = 0.5f; + + /** + * The raw text size, will be multiplied by the scaled density when drawn + */ + private final int mTextHeightRaw; + private ArrayList mTextChars = new ArrayList<>(); + private String mText = ""; + private Stack mCharPool = new Stack<>(); + private int mDotSize; + private PowerManager mPM; + private int mCharPadding; + private final Paint mDrawPaint = new Paint(); + private Interpolator mAppearInterpolator; + private Interpolator mDisappearInterpolator; + private Interpolator mFastOutSlowInInterpolator; + private boolean mShowPassword; + + public PasswordTextView(Context context) { + this(context, null); + } + + public PasswordTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setFocusableInTouchMode(true); + setFocusable(true); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView); + try { + mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0); + } finally { + a.recycle(); + } + mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); + mDrawPaint.setTextAlign(Paint.Align.CENTER); + mDrawPaint.setColor(0xffffffff); + mDrawPaint.setTypeface(Typeface.create("sans-serif-light", 0)); + mDotSize = getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size); + mCharPadding = getContext().getResources().getDimensionPixelSize(R.dimen + .password_char_padding); + mShowPassword = Settings.System.getInt(mContext.getContentResolver(), + Settings.System.TEXT_SHOW_PASSWORD, 1) == 1; + mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); + mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_linear_in); + mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_slow_in); + mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + } + + @Override + protected void onDraw(Canvas canvas) { + float totalDrawingWidth = getDrawingWidth(); + float currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2; + int length = mTextChars.size(); + Rect bounds = getCharBounds(); + int charHeight = (bounds.bottom - bounds.top); + float yPosition = getHeight() / 2; + float charLength = bounds.right - bounds.left; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition, + charLength); + currentDrawPosition += charWidth; + } + } + + private Rect getCharBounds() { + float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity; + mDrawPaint.setTextSize(textHeight); + Rect bounds = new Rect(); + mDrawPaint.getTextBounds("0", 0, 1, bounds); + return bounds; + } + + private float getDrawingWidth() { + int width = 0; + int length = mTextChars.size(); + Rect bounds = getCharBounds(); + int charLength = bounds.right - bounds.left; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + if (i != 0) { + width += mCharPadding * charState.currentWidthFactor; + } + width += charLength * charState.currentWidthFactor; + } + return width; + } + + + public void append(char c) { + int visibleChars = mTextChars.size(); + mText = mText + c; + int newLength = mText.length(); + CharState charState; + if (newLength > visibleChars) { + charState = obtainCharState(c); + mTextChars.add(charState); + } else { + charState = mTextChars.get(newLength - 1); + charState.whichChar = c; + } + charState.startAppearAnimation(); + + // ensure that the previous element is being swapped + if (newLength > 1) { + CharState previousState = mTextChars.get(newLength - 2); + if (previousState.isDotSwapPending) { + previousState.swapToDotWhenAppearFinished(); + } + } + userActivity(); + } + + private void userActivity() { + mPM.userActivity(SystemClock.uptimeMillis(), false); + } + + public void deleteLastChar() { + int length = mText.length(); + if (length > 0) { + mText = mText.substring(0, length - 1); + CharState charState = mTextChars.get(length - 1); + charState.startRemoveAnimation(0, 0); + } + userActivity(); + } + + public String getText() { + return mText; + } + + private CharState obtainCharState(char c) { + CharState charState; + if(mCharPool.isEmpty()) { + charState = new CharState(); + } else { + charState = mCharPool.pop(); + charState.reset(); + } + charState.whichChar = c; + return charState; + } + + public void reset(boolean animated) { + mText = ""; + int length = mTextChars.size(); + int middleIndex = (length - 1) / 2; + long delayPerElement = RESET_DELAY_PER_ELEMENT; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + if (animated) { + int delayIndex; + if (i <= middleIndex) { + delayIndex = i * 2; + } else { + int distToMiddle = i - middleIndex; + delayIndex = (length - 1) - (distToMiddle - 1) * 2; + } + long startDelay = delayIndex * delayPerElement; + startDelay = Math.min(startDelay, RESET_MAX_DELAY); + long maxDelay = delayPerElement * (length - 1); + maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION; + charState.startRemoveAnimation(startDelay, maxDelay); + charState.removeDotSwapCallbacks(); + } else { + mCharPool.push(charState); + } + } + if (!animated) { + mTextChars.clear(); + } + } + + private class CharState { + char whichChar; + ValueAnimator textAnimator; + boolean textAnimationIsGrowing; + Animator dotAnimator; + boolean dotAnimationIsGrowing; + ValueAnimator widthAnimator; + boolean widthAnimationIsGrowing; + float currentTextSizeFactor; + float currentDotSizeFactor; + float currentWidthFactor; + boolean isDotSwapPending; + float currentTextTranslationY = 1.0f; + ValueAnimator textTranslateAnimator; + + Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() { + private boolean mCancelled; + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelled) { + mTextChars.remove(CharState.this); + mCharPool.push(CharState.this); + reset(); + cancelAnimator(textTranslateAnimator); + textTranslateAnimator = null; + } + } + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; + } + }; + + Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dotAnimator = null; + } + }; + + Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + textAnimator = null; + } + }; + + Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + textTranslateAnimator = null; + } + }; + + Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + widthAnimator = null; + } + }; + + private ValueAnimator.AnimatorUpdateListener dotSizeUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentDotSizeFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener textSizeUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentTextSizeFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener textTranslationUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentTextTranslationY = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener widthUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentWidthFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private Runnable dotSwapperRunnable = new Runnable() { + @Override + public void run() { + performSwap(); + isDotSwapPending = false; + } + }; + + void reset() { + whichChar = 0; + currentTextSizeFactor = 0.0f; + currentDotSizeFactor = 0.0f; + currentWidthFactor = 0.0f; + cancelAnimator(textAnimator); + textAnimator = null; + cancelAnimator(dotAnimator); + dotAnimator = null; + cancelAnimator(widthAnimator); + widthAnimator = null; + currentTextTranslationY = 1.0f; + removeDotSwapCallbacks(); + } + + void startRemoveAnimation(long startDelay, long widthDelay) { + boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null) + || (dotAnimator != null && dotAnimationIsGrowing); + boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null) + || (textAnimator != null && textAnimationIsGrowing); + boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null) + || (widthAnimator != null && widthAnimationIsGrowing); + if (dotNeedsAnimation) { + startDotDisappearAnimation(startDelay); + } + if (textNeedsAnimation) { + startTextDisappearAnimation(startDelay); + } + if (widthNeedsAnimation) { + startWidthDisappearAnimation(widthDelay); + } + } + + void startAppearAnimation() { + boolean dotNeedsAnimation = !mShowPassword + && (dotAnimator == null || !dotAnimationIsGrowing); + boolean textNeedsAnimation = mShowPassword + && (textAnimator == null || !textAnimationIsGrowing); + boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing); + if (dotNeedsAnimation) { + startDotAppearAnimation(0); + } + if (textNeedsAnimation) { + startTextAppearAnimation(); + } + if (widthNeedsAnimation) { + startWidthAppearAnimation(); + } + if (mShowPassword) { + postDotSwap(TEXT_VISIBILITY_DURATION); + } + } + + /** + * Posts a runnable which ensures that the text will be replaced by a dot after {@link + * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}. + */ + private void postDotSwap(long delay) { + removeDotSwapCallbacks(); + postDelayed(dotSwapperRunnable, delay); + isDotSwapPending = true; + } + + private void removeDotSwapCallbacks() { + removeCallbacks(dotSwapperRunnable); + isDotSwapPending = false; + } + + void swapToDotWhenAppearFinished() { + removeDotSwapCallbacks(); + if (textAnimator != null) { + long remainingDuration = textAnimator.getDuration() + - textAnimator.getCurrentPlayTime(); + postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR); + } else { + performSwap(); + } + } + + private void performSwap() { + startTextDisappearAnimation(0); + startDotAppearAnimation(DISAPPEAR_DURATION + - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION); + } + + private void startWidthDisappearAnimation(long widthDelay) { + cancelAnimator(widthAnimator); + widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f); + widthAnimator.addUpdateListener(widthUpdater); + widthAnimator.addListener(widthFinishListener); + widthAnimator.addListener(removeEndListener); + widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor)); + widthAnimator.setStartDelay(widthDelay); + widthAnimator.start(); + widthAnimationIsGrowing = false; + } + + private void startTextDisappearAnimation(long startDelay) { + cancelAnimator(textAnimator); + textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f); + textAnimator.addUpdateListener(textSizeUpdater); + textAnimator.addListener(textFinishListener); + textAnimator.setInterpolator(mDisappearInterpolator); + textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor)); + textAnimator.setStartDelay(startDelay); + textAnimator.start(); + textAnimationIsGrowing = false; + } + + private void startDotDisappearAnimation(long startDelay) { + cancelAnimator(dotAnimator); + ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f); + animator.addUpdateListener(dotSizeUpdater); + animator.addListener(dotFinishListener); + animator.setInterpolator(mDisappearInterpolator); + long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f)); + animator.setDuration(duration); + animator.setStartDelay(startDelay); + animator.start(); + dotAnimator = animator; + dotAnimationIsGrowing = false; + } + + private void startWidthAppearAnimation() { + cancelAnimator(widthAnimator); + widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f); + widthAnimator.addUpdateListener(widthUpdater); + widthAnimator.addListener(widthFinishListener); + widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor))); + widthAnimator.start(); + widthAnimationIsGrowing = true; + } + + private void startTextAppearAnimation() { + cancelAnimator(textAnimator); + textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f); + textAnimator.addUpdateListener(textSizeUpdater); + textAnimator.addListener(textFinishListener); + textAnimator.setInterpolator(mAppearInterpolator); + textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor))); + textAnimator.start(); + textAnimationIsGrowing = true; + + // handle translation + if (textTranslateAnimator == null) { + textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); + textTranslateAnimator.addUpdateListener(textTranslationUpdater); + textTranslateAnimator.addListener(textTranslateFinishListener); + textTranslateAnimator.setInterpolator(mAppearInterpolator); + textTranslateAnimator.setDuration(APPEAR_DURATION); + textTranslateAnimator.start(); + } + } + + private void startDotAppearAnimation(long delay) { + cancelAnimator(dotAnimator); + if (!mShowPassword) { + // We perform an overshoot animation + ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, + DOT_OVERSHOOT_FACTOR); + overShootAnimator.addUpdateListener(dotSizeUpdater); + overShootAnimator.setInterpolator(mAppearInterpolator); + long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT + * OVERSHOOT_TIME_POSITION); + overShootAnimator.setDuration(overShootDuration); + ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR, + 1.0f); + settleBackAnimator.addUpdateListener(dotSizeUpdater); + settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration); + settleBackAnimator.addListener(dotFinishListener); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially(overShootAnimator, settleBackAnimator); + animatorSet.setStartDelay(delay); + animatorSet.start(); + dotAnimator = animatorSet; + } else { + ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f); + growAnimator.addUpdateListener(dotSizeUpdater); + growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor))); + growAnimator.addListener(dotFinishListener); + growAnimator.setStartDelay(delay); + growAnimator.start(); + dotAnimator = growAnimator; + } + dotAnimationIsGrowing = true; + } + + private void cancelAnimator(Animator animator) { + if (animator != null) { + animator.cancel(); + } + } + + /** + * Draw this char to the canvas. + * + * @return The width this character contributes, including padding. + */ + public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, + float charLength) { + boolean textVisible = currentTextSizeFactor > 0; + boolean dotVisible = currentDotSizeFactor > 0; + float charWidth = charLength * currentWidthFactor; + if (textVisible) { + float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor + + charHeight * currentTextTranslationY * 0.8f; + canvas.save(); + float centerX = currentDrawPosition + charWidth / 2; + canvas.translate(centerX, currYPosition); + canvas.scale(currentTextSizeFactor, currentTextSizeFactor); + canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint); + canvas.restore(); + } + if (dotVisible) { + canvas.save(); + float centerX = currentDrawPosition + charWidth / 2; + canvas.translate(centerX, yPosition); + canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint); + canvas.restore(); + } + return charWidth + mCharPadding * currentWidthFactor; + } + } +}