Move security pin pad to left half of screen
Change-Id: Ife323dfb782c369da814ab6829fdf072fd98d02e
Fixes: 74082771
Fixes: 73824490
Test: m -j CarSettingsRoboTests
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 93d7f8a..30feb98 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -123,6 +123,14 @@
</intent-filter>
</activity>
+ <activity android:name=".security.ChooseLockPinActivity"
+ android:configChanges="orientation|keyboardHidden|screenSize">
+ <intent-filter>
+ <action android:name="android.car.settings.CHOOSE_LOCK_PIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
<service android:name=".bluetooth.BluetoothPairingService" />
<receiver android:name=".bluetooth.BluetoothPairingRequest">
diff --git a/res/drawable/ic_backspace.xml b/res/drawable/ic_backspace.xml
new file mode 100644
index 0000000..fb0bb24
--- /dev/null
+++ b/res/drawable/ic_backspace.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2018 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
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M22,3H7C6.31,3 5.77,3.35 5.41,3.88l-5.04,7.57c-0.22,0.34 -0.22,0.77 0,1.11l5.04,7.56C5.77,20.64 6.31,21 7,21h15c1.1,0 2,-0.9 2,-2V5C24,3.9 23.1,3 22,3zM18.3,16.3L18.3,16.3c-0.39,0.39 -1.02,0.39 -1.41,0L14,13.41l-2.89,2.89c-0.39,0.39 -1.02,0.39 -1.41,0h0c-0.39,-0.39 -0.39,-1.02 0,-1.41L12.59,12L9.7,9.11c-0.39,-0.39 -0.39,-1.02 0,-1.41l0,0c0.39,-0.39 1.02,-0.39 1.41,0L14,10.59l2.89,-2.89c0.39,-0.39 1.02,-0.39 1.41,0v0c0.39,0.39 0.39,1.02 0,1.41L15.41,12l2.89,2.89C18.68,15.27 18.68,15.91 18.3,16.3z"/>
+</vector>
diff --git a/res/drawable/ic_done.xml b/res/drawable/ic_done.xml
new file mode 100644
index 0000000..3637853
--- /dev/null
+++ b/res/drawable/ic_done.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2018 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
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,16.2l-3.5,-3.5a0.984,0.984 0,0 0,-1.4 0,0.984 0.984,0 0,0 0,1.4l4.19,4.19c0.39,0.39 1.02,0.39 1.41,0L20.3,7.7a0.984,0.984 0,0 0,0 -1.4,0.984 0.984,0 0,0 -1.4,0L9,16.2z"/>
+</vector>
diff --git a/res/layout/choose_lock_password.xml b/res/layout/choose_lock_password.xml
index 67424ec..0b082be 100644
--- a/res/layout/choose_lock_password.xml
+++ b/res/layout/choose_lock_password.xml
@@ -32,20 +32,16 @@
android:orientation="vertical"
android:paddingTop="@dimen/car_primary_icon_size">
- <ImageView
- android:id="@+id/base_icon"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_primary_icon_size"
- android:layout_marginBottom="@dimen/car_padding_2"
- android:gravity="center"/>
-
<TextView
android:id="@+id/title_text"
- android:gravity="center"
- android:layout_width="match_parent"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/car_padding_2"
- android:textAppearance="@style/TextAppearance.Car.Title" />
+ android:textAppearance="@style/TextAppearance.Car.Title2"
+ android:text="@string/set_screen_lock"
+ android:drawableStart="@drawable/ic_lock"
+ android:drawablePadding="@dimen/lockscreen_title_drawable_padding"/>
<TextView
android:id="@+id/description_text"
@@ -53,23 +49,17 @@
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/car_padding_5"
android:gravity="center"
- android:textAppearance="@style/TextAppearance.Car.Body2" />
+ android:textAppearance="@style/TextAppearance.Car.Body2"
+ android:text="@string/choose_lock_password_message"/>
</LinearLayout>
- <!-- End side: pin/password entry field -->
+ <!-- End side: password entry field -->
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="@integer/content_weight"
android:orientation="vertical"
android:paddingTop="@dimen/car_primary_icon_size">
- <!-- header text -->
- <TextView
- android:id="@+id/header_text"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:textAppearance="@style/TextAppearance.Car.Body1" />
<EditText
android:id="@+id/password_entry"
@@ -103,7 +93,7 @@
<!-- left / top button: skip, or re-try -->
<Button android:id="@+id/footerSecondaryButton"
- style="@style/SetupWizardButton.Negative"
+ style="@style/Widget.Car.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/car_action1_size"
@@ -112,7 +102,7 @@
<!-- right / bottom button: confirm or ok -->
<Button android:id="@+id/footerPrimaryButton"
- style="@style/SetupWizardButton.Positive"
+ style="@style/Widget.Car.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/car_action1_size"
diff --git a/res/layout/choose_lock_pattern.xml b/res/layout/choose_lock_pattern.xml
index 09a07c4..adfb8db 100644
--- a/res/layout/choose_lock_pattern.xml
+++ b/res/layout/choose_lock_pattern.xml
@@ -86,7 +86,7 @@
<!-- left / top button: skip, or re-try -->
<Button android:id="@+id/footerSecondaryButton"
- style="@style/SetupWizardButton.Negative"
+ style="@style/Widget.Car.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/car_action1_size"
@@ -95,7 +95,7 @@
<!-- right / bottom button: confirm or ok -->
<Button android:id="@+id/footerPrimaryButton"
- style="@style/SetupWizardButton.Positive"
+ style="@style/Widget.Car.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/car_action1_size"
diff --git a/res/layout/choose_lock_pin.xml b/res/layout/choose_lock_pin.xml
new file mode 100644
index 0000000..8c57112
--- /dev/null
+++ b/res/layout/choose_lock_pin.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/car_margin"
+ android:layout_marginEnd="@dimen/car_margin"
+ android:orientation="horizontal"
+ android:paddingTop="@dimen/car_app_bar_height"
+ android:paddingBottom="@dimen/car_app_bar_height">
+
+ <!-- Start side: lock pattern -->
+ <FrameLayout
+ android:layout_weight="@integer/content_weight"
+ android:layout_width="0dp"
+ android:layout_height="match_parent">
+
+ <include layout="@layout/pin_pad"/>
+ </FrameLayout>
+
+ <!-- End side: pin entry field and messages -->
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="@integer/content_weight"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/pin_pad_end_padding_top">
+
+ <TextView
+ android:id="@+id/title_text"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_padding_2"
+ android:textAppearance="@style/TextAppearance.Car.Title2"
+ android:text="@string/set_screen_lock"
+ android:drawableStart="@drawable/ic_lock"
+ android:drawablePadding="@dimen/lockscreen_title_drawable_padding"/>
+
+ <TextView
+ android:id="@+id/description_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_padding_5"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.Car.Body2"
+ android:text="@string/choose_lock_pin_message"/>
+
+ <EditText
+ android:id="@+id/password_entry"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.Car.Body1"
+ android:maxLines="1"
+ android:inputType="none"
+ android:password="true"
+ android:cursorVisible="false"
+ android:focusable="false"/>
+
+ <!-- hint text -->
+ <TextView
+ android:id="@+id/hint_text"
+ android:text="@string/choose_lock_pin_hints"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.Car.Body2" />
+
+ <!-- confirm / restart buttons -->
+ <LinearLayout
+ android:id="@+id/buttonContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <!-- skip, or re-try -->
+ <Button android:id="@+id/footerSecondaryButton"
+ style="@style/Widget.Car.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/car_action1_size"
+ android:layout_marginEnd="@dimen/car_padding_4"
+ android:text="@string/lockpattern_cancel_button_text" />
+
+ <!-- confirm or ok -->
+ <Button android:id="@+id/footerPrimaryButton"
+ style="@style/Widget.Car.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/car_action1_size"
+ android:text="@string/lockpattern_confirm_button_text" />
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/pin_pad.xml b/res/layout/pin_pad.xml
new file mode 100644
index 0000000..1dbc77b
--- /dev/null
+++ b/res/layout/pin_pad.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/pin_pad_padding_top"
+ android:gravity="center"
+ android:columnCount="3">
+
+ <!-- Row 1 -->
+ <Button
+ android:id="@+id/key1"
+ style="@style/PinPadKey"
+ android:text="@string/one"
+ android:tag="1"/>
+ <Button
+ android:id="@+id/key2"
+ style="@style/PinPadKey"
+ android:text="@string/two"
+ android:tag="2"/>
+ <Button
+ android:id="@+id/key3"
+ style="@style/PinPadKey"
+ android:text="@string/three"
+ android:tag="3"/>
+
+ <!-- Row 2 -->
+ <Button
+ android:id="@+id/key4"
+ style="@style/PinPadKey"
+ android:text="@string/four"
+ android:tag="4"/>
+ <Button
+ android:id="@+id/key5"
+ style="@style/PinPadKey"
+ android:text="@string/five"
+ android:tag="5"/>
+ <Button
+ android:id="@+id/key6"
+ style="@style/PinPadKey"
+ android:text="@string/six"
+ android:tag="6"/>
+
+ <!-- Row 3 -->
+ <Button
+ android:id="@+id/key7"
+ style="@style/PinPadKey"
+ android:text="@string/seven"
+ android:tag="7"/>
+ <Button
+ android:id="@+id/key8"
+ style="@style/PinPadKey"
+ android:text="@string/eight"
+ android:tag="8"/>
+ <Button
+ android:id="@+id/key9"
+ style="@style/PinPadKey"
+ android:text="@string/nine"
+ android:tag="9"/>
+
+ <!-- Row 4 -->
+ <ImageButton
+ android:id="@+id/key_backspace"
+ style="@style/PinPadKey"
+ android:src="@drawable/ic_backspace"
+ android:contentDescription="@string/backspace_key"/>
+ <Button
+ android:id="@+id/key0"
+ style="@style/PinPadKey"
+ android:text="@string/zero"
+ android:tag="0"/>
+ <ImageButton
+ android:id="@+id/key_enter"
+ style="@style/PinPadKey"
+ android:src="@drawable/ic_done"
+ android:contentDescription="@string/enter_key"/>
+</GridLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3537274..cfc2d1d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -30,4 +30,13 @@
<integer name="bluetooth_name_length">32</integer>
+ <dimen name="pin_pad_key_width">120dp</dimen>
+ <dimen name="pin_pad_key_height">80dp</dimen>
+ <dimen name="pin_pad_key_margin">12dp</dimen>
+ <dimen name="pin_pad_padding_top">20dp</dimen>
+ <dimen name="pin_pad_end_padding_top">48dp</dimen>
+ <dimen name="pin_pad_key_text_size">48sp</dimen>
+
+ <dimen name="lockscreen_title_drawable_padding">5dp</dimen>
+
</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f873183..096e697 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -558,4 +558,21 @@
<string name="remove_button">Remove</string>
<!-- Cancel button text [CHAR LIMIT=20] -->
<string name="cancel">Cancel</string>
+
+ <!-- Accessibility description for the backspace key in the PIN pad [CHAR LIMIT=NONE] -->
+ <string name="backspace_key">Backspace key</string>
+ <!-- Accessibility description for the enter key in the PIN pad [CHAR LIMIT=NONE] -->
+ <string name="enter_key">Enter key</string>
+
+ <!-- These are the digits on the PIN pad -->
+ <string name="one" translatable="false">1</string>
+ <string name="two" translatable="false">2</string>
+ <string name="three" translatable="false">3</string>
+ <string name="four" translatable="false">4</string>
+ <string name="five" translatable="false">5</string>
+ <string name="six" translatable="false">6</string>
+ <string name="seven" translatable="false">7</string>
+ <string name="eight" translatable="false">8</string>
+ <string name="nine" translatable="false">9</string>
+ <string name="zero" translatable="false">0</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1fd5380..e15b73c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -81,4 +81,15 @@
<item name="*android:successColor">@color/lock_pattern_success</item>
<item name="*android:errorColor">?android:attr/colorError</item>
</style>
+
+ <style name="PinPadKey" parent="@style/TextAppearance.Car.Key2">
+ <item name="android:layout_width">@dimen/pin_pad_key_width</item>
+ <item name="android:layout_height">@dimen/pin_pad_key_height</item>
+ <item name="android:layout_margin">@dimen/pin_pad_key_margin</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textSize">@dimen/pin_pad_key_text_size</item>
+ <item name="android:textColor">@android:color/black</item>
+ <item name="android:clickable">true</item>
+ <item name="android:background">@drawable/button_ripple_bg</item>
+ </style>
</resources>
diff --git a/src/com/android/car/settings/security/ChooseLockPasswordActivity.java b/src/com/android/car/settings/security/ChooseLockPasswordActivity.java
index 063ed25..4c2eae8 100644
--- a/src/com/android/car/settings/security/ChooseLockPasswordActivity.java
+++ b/src/com/android/car/settings/security/ChooseLockPasswordActivity.java
@@ -16,470 +16,75 @@
package com.android.car.settings.security;
+import android.annotation.IdRes;
import android.app.admin.DevicePolicyManager;
-import android.app.admin.PasswordMetrics;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.UserHandle;
-import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
-import android.support.v4.app.FragmentActivity;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.Selection;
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.TextView.OnEditorActionListener;
import com.android.car.settings.R;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.TextViewInputDisabler;
-import java.util.ArrayList;
+import java.util.LinkedList;
import java.util.List;
/**
- * Activity for choosing a lock password/pin.
+ * Activity for choosing a lock password.
*/
-public class ChooseLockPasswordActivity extends FragmentActivity implements
- OnEditorActionListener,
- TextWatcher,
- View.OnClickListener,
- SaveChosenLockWorkerBase.Listener {
+public class ChooseLockPasswordActivity extends ChooseLockPasswordBaseActivity {
+
+ // Error code returned from validatePassword(String).
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int CONTAINS_INVALID_CHARACTERS = 1;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int DOES_NOT_MATCH_PATTERN = 1 << 1;
+
/**
* Password must contain at least one number, one letter,
* can not have white space, should be between 4 to 8 characters.
*/
- public static final String PASSWORD_PATTERN =
- "^(?=.*[0-9])(?=.*[a-zA-Z])(?=\\S+$).{4,8}$";
+ private static final String PASSWORD_PATTERN = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=\\S+$).{4,8}$";
- protected int mUserId;
- protected Stage mUiStage = Stage.Introduction;
+ // Allow non-control Latin-1 characters only.
+ private static final String VALID_CHAR_PATTERN = "^[\\x20-\\x7F]*$";
- // Error code returned from validatePassword(String).
- @VisibleForTesting
- static final int NO_ERROR = 0;
- @VisibleForTesting
- static final int CONTAIN_INVALID_CHARACTERS = 1 << 0;
- @VisibleForTesting
- static final int CONTAIN_NON_DIGITS = 1 << 1;
- @VisibleForTesting
- static final int CONTAIN_SEQUENTIAL_DIGITS = 1 << 2;
- @VisibleForTesting
- static final int DO_NOT_MATCH_PATTERN = 1 << 3;
- @VisibleForTesting
- static final int RECENTLY_USED = 1 << 4;
- @VisibleForTesting
- static final int BLACKLISTED = 1 << 5;
- private static final String TAG = "ChooseLockPassword";
- private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker";
-
- private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
- private boolean mIsAlphaMode;
- private String mEnteredPassword;
- private String mCurrentPassword;
- private EditText mPasswordEntry;
- private TextViewInputDisabler mPasswordEntryInputDisabler;
-
- private SaveLockPasswordWorker mSaveLockPasswordWorker;
-
- private String mFirstPassword;
-
- private TextView mDescriptionMessage;
- private TextView mHintMessage;
- private Button mSecondaryButton;
- private Button mPrimaryButton;
-
- private TextChangedHandler mTextChangedHandler;
-
- // Keep track internally of where the user is in choosing a password.
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- protected enum Stage {
- Introduction(
- R.string.choose_lock_password_hints,
- R.string.choose_lock_pin_hints,
- R.string.continue_button_text,
- R.string.lockpassword_cancel_label),
-
- PasswordInvalid(
- R.string.lockpassword_invalid_password,
- R.string.lockpin_invalid_pin,
- R.string.continue_button_text,
- R.string.lockpassword_clear_label),
-
- NeedToConfirm(
- R.string.confirm_your_password_header,
- R.string.confirm_your_pin_header,
- R.string.lockpassword_confirm_label,
- R.string.lockpassword_cancel_label),
-
- ConfirmWrong(
- R.string.confirm_passwords_dont_match,
- R.string.confirm_pins_dont_match,
- R.string.continue_button_text,
- R.string.lockpassword_cancel_label),
-
- SaveFailure(
- R.string.error_saving_password,
- R.string.error_saving_lockpin,
- R.string.lockscreen_retry_button_text,
- R.string.lockpassword_cancel_label);
-
- public final int alphaHint;
- public final int numericHint;
- public final int primaryButtonText;
- public final int secondaryButtonText;
-
- Stage(int hintInAlpha,
- int hintInNumeric,
- int primaryButtonText,
- int secondaryButtonText) {
- this.alphaHint = hintInAlpha;
- this.numericHint = hintInNumeric;
- this.primaryButtonText = primaryButtonText;
- this.secondaryButtonText = secondaryButtonText;
- }
-
- @StringRes
- public int getHint(boolean isAlpha) {
- if (isAlpha) {
- return alphaHint;
- } else {
- return numericHint;
- }
- }
+ @Override
+ @IdRes
+ protected int getLayoutResId() {
+ return R.layout.choose_lock_password;
}
@Override
- public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-
+ protected int getPasswordQuality() {
+ return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
}
@Override
- public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- // Changing the text while error displayed resets to NeedToConfirm state
- if (mUiStage == Stage.ConfirmWrong) {
- mUiStage = Stage.NeedToConfirm;
- }
- // Schedule the UI update.
- mTextChangedHandler.notifyAfterTextChanged();
- }
-
- @Override
- public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
- // Check if this was the result of hitting the enter or "done" key
- if (actionId == EditorInfo.IME_NULL
- || actionId == EditorInfo.IME_ACTION_DONE
- || actionId == EditorInfo.IME_ACTION_NEXT) {
- handlePrimaryButtonClick();
- return true;
- }
- return false;
- }
-
- @Override
- public void onClick(View view) {
- if (view == mPrimaryButton) {
- handlePrimaryButtonClick();
- } else if (view == mSecondaryButton) {
- handleSecondaryButtonClick();
- }
- }
-
- @Override
- public void onChosenLockSaveFinished(Intent data) {
- boolean isSaveSuccessful =
- data.getBooleanExtra(SaveChosenLockWorkerBase.EXTRA_KEY_SUCCESS, false);
- if (isSaveSuccessful) {
- setResult(RESULT_OK, data);
- finish();
- } else {
- mSaveLockPasswordWorker = null; // Allow new worker to be created
- updateStage(Stage.SaveFailure);
- }
- }
-
- /**
- * Validates PIN/Password and returns the validation result.
- *
- * @param password the raw password the user typed in
- * @return the validation result.
- */
- public int validatePassword(String password) {
+ protected int validatePassword(String password) {
int errorCode = NO_ERROR;
- final PasswordMetrics metrics = PasswordMetrics.computeForPassword(password);
+ if (!password.matches(VALID_CHAR_PATTERN)) {
+ errorCode |= CONTAINS_INVALID_CHARACTERS;
+ }
- // Ensure no non-digits if we are requesting numbers. This shouldn't be possible unless
- // user finds some way to bring up soft keyboard.
- if (mRequestedQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
- || mRequestedQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX) {
- if (metrics.letters > 0 || metrics.symbols > 0) {
- errorCode |= CONTAIN_NON_DIGITS;
- }
-
- if (mRequestedQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX) {
- // Check for repeated characters or sequences (e.g. '1234', '0000', '2468')
- final int sequence = PasswordMetrics.maxLengthSequence(password);
- if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) {
- errorCode |= CONTAIN_SEQUENTIAL_DIGITS;
- }
- }
- } else {
- // Allow non-control Latin-1 characters only.
- for (int i = 0; i < password.length(); i++) {
- char c = password.charAt(i);
- if (c < 32 || c > 127) {
- errorCode |= CONTAIN_INVALID_CHARACTERS;
- break;
- }
- }
- if (!password.matches(PASSWORD_PATTERN)) {
- errorCode |= DO_NOT_MATCH_PATTERN;
- }
+ if (!password.matches(PASSWORD_PATTERN)) {
+ errorCode |= DOES_NOT_MATCH_PATTERN;
}
return errorCode;
}
- @VisibleForTesting
- void setPasswordQuality(int passwordQuality) {
- mRequestedQuality = passwordQuality;
- }
-
@Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Intent intent = getIntent();
- int quality = intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
- DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC);
- setPasswordQuality(quality);
- setContentView(R.layout.choose_lock_password);
- ImageView iconImg = (ImageView) findViewById(R.id.base_icon);
- iconImg.setImageResource(R.drawable.ic_lock);
+ protected String[] convertErrorCodeToMessages(int errorCode) {
+ List<String> messages = new LinkedList<>();
- mTextChangedHandler = new TextChangedHandler();
- mUserId = UserHandle.myUserId();
-
- mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality
- || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == mRequestedQuality
- || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == mRequestedQuality;
-
- mPasswordEntry = (EditText) findViewById(R.id.password_entry);
- mPasswordEntry.setOnEditorActionListener(this);
- mPasswordEntry.addTextChangedListener(this);
- mPasswordEntry.requestFocus();
- mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
-
- int currentType = mPasswordEntry.getInputType();
- mPasswordEntry.setInputType(mIsAlphaMode ? currentType
- : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
-
- TextView titleText = (TextView) findViewById(R.id.title_text);
- titleText.setText(getString(R.string.set_screen_lock));
-
- mDescriptionMessage = (TextView) findViewById(R.id.description_text);
- mDescriptionMessage.setText(getString(
- mIsAlphaMode ? R.string.choose_lock_password_message
- : R.string.choose_lock_pin_message));
-
- mHintMessage = (TextView) findViewById(R.id.hint_text);
- mHintMessage.setText(getString(
- mIsAlphaMode ? R.string.choose_lock_password_hints
- : R.string.choose_lock_pin_hints));
-
- mPrimaryButton = (Button) findViewById(R.id.footerPrimaryButton);
- mPrimaryButton.setOnClickListener(this);
- mSecondaryButton = (Button) findViewById(R.id.footerSecondaryButton);
- mSecondaryButton.setOnClickListener(this);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (mSaveLockPasswordWorker != null) {
- mSaveLockPasswordWorker.setListener(null);
- }
- }
-
- private void setPrimaryButtonEnabled(boolean enabled) {
- mPrimaryButton.setEnabled(enabled);
- }
-
- private void setPrimaryButtonTextId(int textId) {
- mPrimaryButton.setText(textId);
- }
-
- private void setSecondaryButtonEnabled(boolean enabled) {
- mSecondaryButton.setEnabled(enabled);
- }
-
- private void setSecondaryButtonTextId(int textId) {
- mSecondaryButton.setText(textId);
- }
-
- // Updates display message and proceed to next step according to the different text on
- // the secondary button.
- private void handleSecondaryButtonClick() {
- if (mSaveLockPasswordWorker != null || TextUtils.isEmpty(mEnteredPassword)) {
- finish();
- return;
- }
-
- if (mUiStage.secondaryButtonText == R.string.lockpassword_clear_label) {
- mPasswordEntry.setText("");
- mUiStage = Stage.Introduction;
- setSecondaryButtonTextId(mUiStage.secondaryButtonText);
- } else {
- finish();
- }
- }
-
- // Updates display message and proceed to next step according to the different text on
- // the primary button.
- private void handlePrimaryButtonClick() {
- mEnteredPassword = mPasswordEntry.getText().toString();
- if (mSaveLockPasswordWorker != null || TextUtils.isEmpty(mEnteredPassword)) {
- return;
- }
-
- switch(mUiStage) {
- case Introduction:
- if (validatePassword(mEnteredPassword) == NO_ERROR) {
- mFirstPassword = mEnteredPassword;
- mPasswordEntry.setText("");
- updateStage(Stage.NeedToConfirm);
- } else {
- updateStage(Stage.PasswordInvalid);
- }
- break;
- case NeedToConfirm:
- case SaveFailure:
- if (mFirstPassword.equals(mEnteredPassword)) {
- startSaveAndFinish();
- } else {
- CharSequence tmp = mPasswordEntry.getText();
- if (tmp != null) {
- Selection.setSelection((Spannable) tmp, 0, tmp.length());
- }
- updateStage(Stage.ConfirmWrong);
- }
- break;
- default:
- // Do nothing.
- }
- }
-
- // Starts an async task to save the chosen password.
- private void startSaveAndFinish() {
- if (mSaveLockPasswordWorker != null) {
- Log.v(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker.");
- return;
- }
-
- mPasswordEntryInputDisabler.setInputEnabled(false);
- setPrimaryButtonEnabled(false);
-
- mSaveLockPasswordWorker = new SaveLockPasswordWorker();
- mSaveLockPasswordWorker.setListener(this);
-
- getFragmentManager().beginTransaction().add(mSaveLockPasswordWorker,
- FRAGMENT_TAG_SAVE_AND_FINISH).commit();
- getFragmentManager().executePendingTransactions();
-
- mSaveLockPasswordWorker.start(new LockPatternUtils(this),
- mEnteredPassword, mCurrentPassword, mRequestedQuality, mUserId);
- }
-
- // Updates the hint based on current Stage and length of password entry
- private void updateUi() {
- boolean inputAllowed = mSaveLockPasswordWorker == null;
-
- if (mUiStage == Stage.Introduction) {
- final int errorCode = validatePassword(mEnteredPassword);
- String[] messages = convertErrorCodeToMessages(errorCode);
- // Update the fulfillment of requirements.
- mHintMessage.setText(messages.toString());
- // Enable/Disable the next button accordingly.
- setPrimaryButtonEnabled(errorCode == NO_ERROR);
- } else {
- mHintMessage.setText(getString(mUiStage.getHint(mIsAlphaMode)));
- boolean hasPassword = mEnteredPassword == "" ? false : mEnteredPassword.length() > 0;
- setPrimaryButtonEnabled(inputAllowed && hasPassword);
- setSecondaryButtonEnabled(inputAllowed && hasPassword);
- }
-
- setPrimaryButtonTextId(mUiStage.primaryButtonText);
- setSecondaryButtonTextId(mUiStage.secondaryButtonText);
- mPasswordEntryInputDisabler.setInputEnabled(inputAllowed);
- }
-
- @VisibleForTesting
- void updateStage(Stage stage) {
- mUiStage = stage;
- updateUi();
- }
-
- class TextChangedHandler extends Handler {
- private static final int ON_TEXT_CHANGED = 1;
- private static final int DELAY_IN_MILLISECOND = 100;
-
- /**
- * With the introduction of delay, we batch processing the text changed event to reduce
- * unnecessary UI updates.
- */
- private void notifyAfterTextChanged() {
- removeMessages(ON_TEXT_CHANGED);
- sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND);
- }
-
- @Override
- public void handleMessage(Message msg) {
- }
- }
-
- // Converts error code from validatePassword to an array of message,
- // describing the error, important message comes first.
- private String[] convertErrorCodeToMessages(int errorCode) {
- List<String> messages = new ArrayList<>();
- if ((errorCode & CONTAIN_INVALID_CHARACTERS) > 0) {
+ if ((errorCode & CONTAINS_INVALID_CHARACTERS) > 0) {
messages.add(getString(R.string.lockpassword_illegal_character));
}
- if ((errorCode & CONTAIN_NON_DIGITS) > 0) {
- messages.add(getString(R.string.lockpassword_pin_contains_non_digits));
- }
- if ((errorCode & CONTAIN_SEQUENTIAL_DIGITS) > 0) {
- messages.add(getString(R.string.lockpassword_pin_no_sequential_digits));
- }
- if ((errorCode & DO_NOT_MATCH_PATTERN) > 0) {
+
+ if ((errorCode & DOES_NOT_MATCH_PATTERN) > 0) {
messages.add(getString(R.string.lockpassword_invalid_password));
}
- if ((errorCode & RECENTLY_USED) > 0) {
- messages.add(getString((mIsAlphaMode) ? R.string.lockpassword_password_recently_used
- : R.string.lockpassword_pin_recently_used));
- }
- if ((errorCode & BLACKLISTED) > 0) {
- messages.add(getString((mIsAlphaMode)
- ? R.string.lockpassword_password_blacklisted_by_admin
- : R.string.lockpassword_pin_blacklisted_by_admin));
- }
- return messages.toArray(new String[0]);
+
+ return messages.toArray(new String[messages.size()]);
}
}
diff --git a/src/com/android/car/settings/security/ChooseLockPasswordBaseActivity.java b/src/com/android/car/settings/security/ChooseLockPasswordBaseActivity.java
new file mode 100644
index 0000000..5e259c0
--- /dev/null
+++ b/src/com/android/car/settings/security/ChooseLockPasswordBaseActivity.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2018 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.car.settings.security;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.app.admin.DevicePolicyManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.FragmentActivity;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.TextViewInputDisabler;
+
+/**
+ * Abstract base Activity for choosing a lock password/pin.
+ */
+public abstract class ChooseLockPasswordBaseActivity extends FragmentActivity implements
+ SaveChosenLockWorkerBase.Listener {
+
+ protected int mUserId;
+ protected Stage mUiStage = Stage.Introduction;
+
+ // Error code returned from validatePassword(String).
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int NO_ERROR = 0;
+
+ private static final Logger LOG = new Logger(ChooseLockPasswordBaseActivity.class);
+
+ private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker";
+
+ private boolean mIsAlphaMode;
+ private String mEnteredPassword;
+ private String mCurrentPassword;
+ private TextViewInputDisabler mPasswordEntryInputDisabler;
+
+ private SaveLockPasswordWorker mSaveLockPasswordWorker;
+
+ private String mFirstPassword;
+
+ private TextView mDescriptionMessage;
+ private TextView mHintMessage;
+ private Button mSecondaryButton;
+ private Button mPrimaryButton;
+
+ private TextChangedHandler mTextChangedHandler;
+
+ private EditText mPasswordEntry;
+
+ // Keep track internally of where the user is in choosing a password.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ protected enum Stage {
+ Introduction(
+ R.string.choose_lock_password_hints,
+ R.string.choose_lock_pin_hints,
+ R.string.continue_button_text,
+ R.string.lockpassword_cancel_label),
+
+ PasswordInvalid(
+ R.string.lockpassword_invalid_password,
+ R.string.lockpin_invalid_pin,
+ R.string.continue_button_text,
+ R.string.lockpassword_clear_label),
+
+ NeedToConfirm(
+ R.string.confirm_your_password_header,
+ R.string.confirm_your_pin_header,
+ R.string.lockpassword_confirm_label,
+ R.string.lockpassword_cancel_label),
+
+ ConfirmWrong(
+ R.string.confirm_passwords_dont_match,
+ R.string.confirm_pins_dont_match,
+ R.string.continue_button_text,
+ R.string.lockpassword_cancel_label),
+
+ SaveFailure(
+ R.string.error_saving_password,
+ R.string.error_saving_lockpin,
+ R.string.lockscreen_retry_button_text,
+ R.string.lockpassword_cancel_label);
+
+ public final int alphaHint;
+ public final int numericHint;
+ public final int primaryButtonText;
+ public final int secondaryButtonText;
+
+ Stage(int hintInAlpha,
+ int hintInNumeric,
+ int primaryButtonText,
+ int secondaryButtonText) {
+ this.alphaHint = hintInAlpha;
+ this.numericHint = hintInNumeric;
+ this.primaryButtonText = primaryButtonText;
+ this.secondaryButtonText = secondaryButtonText;
+ }
+
+ @StringRes
+ public int getHint(boolean isAlpha) {
+ if (isAlpha) {
+ return alphaHint;
+ } else {
+ return numericHint;
+ }
+ }
+ }
+
+ /**
+ * Returns the resource Id of the layout
+ */
+ @IdRes
+ protected abstract int getLayoutResId();
+
+ /**
+ * Returns one of the values defined in {@link DevicePolicyManager}
+ */
+ protected abstract int getPasswordQuality();
+
+ @Override
+ public void onChosenLockSaveFinished(Intent data) {
+ boolean isSaveSuccessful =
+ data.getBooleanExtra(SaveChosenLockWorkerBase.EXTRA_KEY_SUCCESS, false);
+ if (isSaveSuccessful) {
+ setResult(RESULT_OK, data);
+ finish();
+ } else {
+ mSaveLockPasswordWorker = null; // Allow new worker to be created
+ updateStage(Stage.SaveFailure);
+ }
+ }
+
+ /**
+ * Validates PIN/Password and returns the validation result.
+ *
+ * @param password the raw password the user typed in
+ * @return the validation result.
+ */
+ protected int validatePassword(String password) {
+ return NO_ERROR;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(getLayoutResId());
+
+ mTextChangedHandler = new TextChangedHandler();
+ mUserId = UserHandle.myUserId();
+
+ int passwordQuality = getPasswordQuality();
+ mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == passwordQuality
+ || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == passwordQuality
+ || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == passwordQuality;
+
+ mPasswordEntry = (EditText) findViewById(R.id.password_entry);
+ mPasswordEntry.setOnEditorActionListener((textView, actionId, keyEvent) -> {
+ // Check if this was the result of hitting the enter or "done" key
+ if (actionId == EditorInfo.IME_NULL
+ || actionId == EditorInfo.IME_ACTION_DONE
+ || actionId == EditorInfo.IME_ACTION_NEXT) {
+ handlePrimaryButtonClick(textView);
+ return true;
+ }
+ return false;
+ });
+
+ mPasswordEntry.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // Changing the text while error displayed resets to NeedToConfirm state
+ if (mUiStage == Stage.ConfirmWrong) {
+ mUiStage = Stage.NeedToConfirm;
+ }
+ // Schedule the UI update.
+ mTextChangedHandler.notifyAfterTextChanged();
+ }
+ });
+
+ mPasswordEntry.requestFocus();
+ mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
+
+ mDescriptionMessage = (TextView) findViewById(R.id.description_text);
+ mHintMessage = (TextView) findViewById(R.id.hint_text);
+
+ mPrimaryButton = (Button) findViewById(R.id.footerPrimaryButton);
+ mPrimaryButton.setOnClickListener(this::handlePrimaryButtonClick);
+ mSecondaryButton = (Button) findViewById(R.id.footerSecondaryButton);
+ mSecondaryButton.setOnClickListener(this::handleSecondaryButtonClick);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mSaveLockPasswordWorker != null) {
+ mSaveLockPasswordWorker.setListener(null);
+ }
+ }
+
+ protected final void appendToPasswordEntry(String text) {
+ mPasswordEntry.append(text);
+ }
+
+ protected final void setPasswordEntry(String text) {
+ mPasswordEntry.setText(text);
+ }
+
+ protected final String getPasswordEntry() {
+ return mPasswordEntry.getText().toString();
+ }
+
+ private void setPrimaryButtonEnabled(boolean enabled) {
+ mPrimaryButton.setEnabled(enabled);
+ }
+
+ private void setPrimaryButtonTextId(int textId) {
+ mPrimaryButton.setText(textId);
+ }
+
+ private void setSecondaryButtonEnabled(boolean enabled) {
+ mSecondaryButton.setEnabled(enabled);
+ }
+
+ private void setSecondaryButtonTextId(int textId) {
+ mSecondaryButton.setText(textId);
+ }
+
+ // Updates display message and proceed to next step according to the different text on
+ // the secondary button.
+ protected void handleSecondaryButtonClick(View unused) {
+ if (mSaveLockPasswordWorker != null || TextUtils.isEmpty(mEnteredPassword)) {
+ finish();
+ return;
+ }
+
+ if (mUiStage.secondaryButtonText == R.string.lockpassword_clear_label) {
+ mPasswordEntry.setText("");
+ mUiStage = Stage.Introduction;
+ setSecondaryButtonTextId(mUiStage.secondaryButtonText);
+ } else {
+ finish();
+ }
+ }
+
+ // Updates display message and proceed to next step according to the different text on
+ // the primary button.
+ protected void handlePrimaryButtonClick(View unused) {
+ mEnteredPassword = mPasswordEntry.getText().toString();
+ if (mSaveLockPasswordWorker != null || TextUtils.isEmpty(mEnteredPassword)) {
+ return;
+ }
+
+ switch(mUiStage) {
+ case Introduction:
+ if (validatePassword(mEnteredPassword) == NO_ERROR) {
+ mFirstPassword = mEnteredPassword;
+ mPasswordEntry.setText("");
+ updateStage(Stage.NeedToConfirm);
+ } else {
+ updateStage(Stage.PasswordInvalid);
+ }
+ break;
+ case NeedToConfirm:
+ case SaveFailure:
+ if (mFirstPassword.equals(mEnteredPassword)) {
+ startSaveAndFinish();
+ } else {
+ CharSequence tmp = mPasswordEntry.getText();
+ if (tmp != null) {
+ Selection.setSelection((Spannable) tmp, 0, tmp.length());
+ }
+ updateStage(Stage.ConfirmWrong);
+ }
+ break;
+ default:
+ // Do nothing.
+ }
+ }
+
+ // Starts an async task to save the chosen password.
+ private void startSaveAndFinish() {
+ if (mSaveLockPasswordWorker != null) {
+ LOG.v("startSaveAndFinish with an existing SaveAndFinishWorker.");
+ return;
+ }
+
+ mPasswordEntryInputDisabler.setInputEnabled(false);
+ setPrimaryButtonEnabled(false);
+
+ mSaveLockPasswordWorker = new SaveLockPasswordWorker();
+ mSaveLockPasswordWorker.setListener(this);
+
+ getFragmentManager().beginTransaction().add(mSaveLockPasswordWorker,
+ FRAGMENT_TAG_SAVE_AND_FINISH).commit();
+ getFragmentManager().executePendingTransactions();
+
+ mSaveLockPasswordWorker.start(new LockPatternUtils(this),
+ mEnteredPassword, mCurrentPassword, getPasswordQuality(), mUserId);
+ }
+
+ // Updates the hint based on current Stage and length of password entry
+ private void updateUi() {
+ boolean inputAllowed = mSaveLockPasswordWorker == null;
+
+ if (mUiStage == Stage.Introduction) {
+ final int errorCode = validatePassword(mEnteredPassword);
+ String[] messages = convertErrorCodeToMessages(errorCode);
+ // Update the fulfillment of requirements.
+ mHintMessage.setText(messages.toString());
+ // Enable/Disable the next button accordingly.
+ setPrimaryButtonEnabled(errorCode == NO_ERROR);
+ } else {
+ mHintMessage.setText(getString(mUiStage.getHint(mIsAlphaMode)));
+ boolean hasPassword = !mEnteredPassword.isEmpty();
+ setPrimaryButtonEnabled(inputAllowed && hasPassword);
+ setSecondaryButtonEnabled(inputAllowed && hasPassword);
+ }
+
+ setPrimaryButtonTextId(mUiStage.primaryButtonText);
+ setSecondaryButtonTextId(mUiStage.secondaryButtonText);
+ mPasswordEntryInputDisabler.setInputEnabled(inputAllowed);
+ }
+
+ @VisibleForTesting
+ void updateStage(Stage stage) {
+ mUiStage = stage;
+ updateUi();
+ }
+
+ /**
+ * Handler that batches text changed events
+ */
+ class TextChangedHandler extends Handler {
+ private static final int ON_TEXT_CHANGED = 1;
+ private static final int DELAY_IN_MILLISECOND = 100;
+
+ /**
+ * With the introduction of delay, we batch processing the text changed event to reduce
+ * unnecessary UI updates.
+ */
+ private void notifyAfterTextChanged() {
+ removeMessages(ON_TEXT_CHANGED);
+ sendEmptyMessageDelayed(ON_TEXT_CHANGED, DELAY_IN_MILLISECOND);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ }
+
+ // Converts error code from validatePassword to an array of message,
+ // describing the error, important message comes first.
+ @NonNull
+ protected String[] convertErrorCodeToMessages(int errorCode) {
+ return new String[0];
+ }
+}
diff --git a/src/com/android/car/settings/security/ChooseLockPinActivity.java b/src/com/android/car/settings/security/ChooseLockPinActivity.java
new file mode 100644
index 0000000..8fd899e
--- /dev/null
+++ b/src/com/android/car/settings/security/ChooseLockPinActivity.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2018 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.car.settings.security;
+
+import android.annotation.IdRes;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.PasswordMetrics;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.car.settings.R;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Activity for choosing a lock pin.
+ */
+public class ChooseLockPinActivity extends ChooseLockPasswordBaseActivity {
+ private static final int MIN_PIN_LENGTH = 4;
+
+ // View Ids used to set onClick listener
+ private static final int[] PIN_PAD_KEYS = { R.id.key0, R.id.key1, R.id.key2, R.id.key3,
+ R.id.key4, R.id.key5, R.id.key6, R.id.key7, R.id.key8, R.id.key9 };
+
+ // Error code returned from validatePassword
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int CONTAINS_NON_DIGITS = 1;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int CONTAINS_SEQUENTIAL_DIGITS = 1 << 1;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int TOO_FEW_DIGITS = 1 << 2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ View backspace = findViewById(R.id.key_backspace);
+ backspace.setOnClickListener(v -> {
+ onBackspaceClick();
+ });
+
+ View enter = findViewById(R.id.key_enter);
+ enter.setOnClickListener(view -> {
+ handlePrimaryButtonClick(view);
+ });
+
+ for (int keyId : PIN_PAD_KEYS) {
+ TextView key = findViewById(keyId);
+ String digit = key.getTag().toString();
+ key.setOnClickListener(v -> appendToPasswordEntry(digit));
+ }
+ }
+
+ @Override
+ @IdRes
+ protected int getLayoutResId() {
+ return R.layout.choose_lock_pin;
+ }
+
+ @Override
+ protected int getPasswordQuality() {
+ return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+ }
+
+ @Override
+ protected int validatePassword(String password) {
+ int errorCode = NO_ERROR;
+
+ PasswordMetrics metrics = PasswordMetrics.computeForPassword(password);
+ int passwordQuality = getPasswordQuality();
+
+ if (metrics.length < MIN_PIN_LENGTH) {
+ errorCode |= TOO_FEW_DIGITS;
+ }
+
+ if (metrics.letters > 0 || metrics.symbols > 0) {
+ errorCode |= CONTAINS_NON_DIGITS;
+ }
+
+ if (passwordQuality == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX) {
+ // Check for repeated characters or sequences (e.g. '1234', '0000', '2468')
+ int sequence = PasswordMetrics.maxLengthSequence(password);
+ if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) {
+ errorCode |= CONTAINS_SEQUENTIAL_DIGITS;
+ }
+ }
+
+ return errorCode;
+ }
+
+ @Override
+ protected String[] convertErrorCodeToMessages(int errorCode) {
+ List<String> messages = new LinkedList<>();
+
+ if ((errorCode & CONTAINS_NON_DIGITS) > 0) {
+ messages.add(getString(R.string.lockpassword_pin_contains_non_digits));
+ }
+
+ if ((errorCode & CONTAINS_SEQUENTIAL_DIGITS) > 0) {
+ messages.add(getString(R.string.lockpassword_pin_no_sequential_digits));
+ }
+
+ if ((errorCode & TOO_FEW_DIGITS) > 0) {
+ messages.add(getString(R.string.lockpin_invalid_pin));
+ }
+
+ return messages.toArray(new String[messages.size()]);
+ }
+
+ private void onBackspaceClick() {
+ String pin = getPasswordEntry();
+ if (pin.length() > 0) {
+ setPasswordEntry(pin.substring(0, pin.length() - 1));
+ }
+ }
+}
diff --git a/src/com/android/car/settings/security/ChooseLockTypeFragment.java b/src/com/android/car/settings/security/ChooseLockTypeFragment.java
index 686f562..8ead26e 100644
--- a/src/com/android/car/settings/security/ChooseLockTypeFragment.java
+++ b/src/com/android/car/settings/security/ChooseLockTypeFragment.java
@@ -16,20 +16,17 @@
package com.android.car.settings.security;
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.android.car.settings.R;
import com.android.car.settings.common.ListItemSettingsFragment;
-import com.android.internal.widget.LockPatternUtils;
import java.util.ArrayList;
import java.util.List;
-import androidx.car.widget.ListItemProvider;
import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemProvider;
import androidx.car.widget.TextListItem;
/**
@@ -103,15 +100,11 @@
private void startChooseLockPasswordActivity() {
Intent intent = new Intent(getContext(), ChooseLockPasswordActivity.class);
- intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
- DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC);
startActivityForResult(intent, REQUEST_CHOOSE_LOCK);
}
private void startChooseLockPinActivity() {
- Intent intent = new Intent(getContext(), ChooseLockPasswordActivity.class);
- intent.putExtra(LockPatternUtils.PASSWORD_TYPE_KEY,
- DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+ Intent intent = new Intent(getContext(), ChooseLockPinActivity.class);
startActivityForResult(intent, REQUEST_CHOOSE_LOCK);
}
}
diff --git a/tests/robotests/src/com/android/car/settings/security/ChooseLockPasswordActivityTest.java b/tests/robotests/src/com/android/car/settings/security/ChooseLockPasswordActivityTest.java
index e8dea99..464b0fc 100644
--- a/tests/robotests/src/com/android/car/settings/security/ChooseLockPasswordActivityTest.java
+++ b/tests/robotests/src/com/android/car/settings/security/ChooseLockPasswordActivityTest.java
@@ -16,14 +16,9 @@
package com.android.car.settings.security;
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -57,10 +52,9 @@
*/
@Test
public void testValidatePasswordTooShort() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "lov";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.DO_NOT_MATCH_PATTERN);
+ .isEqualTo(ChooseLockPasswordActivity.DOES_NOT_MATCH_PATTERN);
}
/**
@@ -69,10 +63,9 @@
*/
@Test
public void testValidatePasswordTooLong() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "passwordtoolong";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.DO_NOT_MATCH_PATTERN);
+ .isEqualTo(ChooseLockPasswordActivity.DOES_NOT_MATCH_PATTERN);
}
/**
@@ -81,10 +74,9 @@
*/
@Test
public void testValidatePasswordWhiteSpace() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "pass wd";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.DO_NOT_MATCH_PATTERN);
+ .isEqualTo(ChooseLockPasswordActivity.DOES_NOT_MATCH_PATTERN);
}
/**
@@ -93,10 +85,9 @@
*/
@Test
public void testValidatePasswordNoDigit() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "password";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.DO_NOT_MATCH_PATTERN);
+ .isEqualTo(ChooseLockPasswordActivity.DOES_NOT_MATCH_PATTERN);
}
/**
@@ -105,10 +96,9 @@
*/
@Test
public void testValidatePasswordNonAscii() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "1passwýd";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.CONTAIN_INVALID_CHARACTERS);
+ .isEqualTo(ChooseLockPasswordActivity.CONTAINS_INVALID_CHARACTERS);
}
/**
@@ -117,34 +107,9 @@
*/
@Test
public void testValidatePasswordNoLetter() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_ALPHANUMERIC);
String password = "123456";
assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.DO_NOT_MATCH_PATTERN);
- }
-
- /**
- * A test to check validatePassword works as expected for numeric complex password
- * that has sequential digits.
- */
- @Test
- public void testValidatePasswordSequentialDigits() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_NUMERIC_COMPLEX);
- String password = "1234";
- assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.CONTAIN_SEQUENTIAL_DIGITS);
- }
-
- /**
- * A test to check validatePassword works as expected for numeric password
- * that contains non digits.
- */
- @Test
- public void testValidatePasswordNoneDigits() {
- mActivity.setPasswordQuality(PASSWORD_QUALITY_NUMERIC);
- String password = "1a34";
- assertThat(mActivity.validatePassword(password))
- .isEqualTo(ChooseLockPasswordActivity.CONTAIN_NON_DIGITS);
+ .isEqualTo(ChooseLockPasswordActivity.DOES_NOT_MATCH_PATTERN);
}
/**
diff --git a/tests/robotests/src/com/android/car/settings/security/ChooseLockPatternActivityTest.java b/tests/robotests/src/com/android/car/settings/security/ChooseLockPatternActivityTest.java
index cce0e64..bff460b 100644
--- a/tests/robotests/src/com/android/car/settings/security/ChooseLockPatternActivityTest.java
+++ b/tests/robotests/src/com/android/car/settings/security/ChooseLockPatternActivityTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 Google Inc.
+ * Copyright (C) 2018 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.
@@ -16,10 +16,7 @@
package com.android.car.settings.security;
-import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
diff --git a/tests/robotests/src/com/android/car/settings/security/ChooseLockPinActivityTest.java b/tests/robotests/src/com/android/car/settings/security/ChooseLockPinActivityTest.java
new file mode 100644
index 0000000..c535525
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/security/ChooseLockPinActivityTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 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.car.settings.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Intent;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+
+/**
+ * Tests for ChooseLockPinActivity class.
+ */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+public class ChooseLockPinActivityTest {
+ private ChooseLockPinActivity mActivity;
+
+ @Before
+ public void setUpPinActivity() {
+ mActivity = Robolectric.buildActivity(ChooseLockPinActivity.class)
+ .create()
+ .get();
+ }
+
+ /**
+ * A test to check validatePassword works as expected for pin that contains non digits.
+ */
+ @Test
+ public void testValidatePinContainingNonDigits() {
+ String password = "1a34";
+ assertThat(mActivity.validatePassword(password))
+ .isEqualTo(ChooseLockPinActivity.CONTAINS_NON_DIGITS);
+ }
+
+ /**
+ * A test to check validatePassword works as expected for pin with too few digits
+ */
+ @Test
+ public void testValidatePinWithTooFewDigits() {
+ String password = "12";
+ assertThat(mActivity.validatePassword(password))
+ .isEqualTo(ChooseLockPinActivity.TOO_FEW_DIGITS);
+ }
+
+ /**
+ * A test to check validatePassword works as expected for numeric complex password
+ * that has sequential digits.
+ */
+ @Test
+ public void testValidatePinWithSequentialDigits() {
+ ChooseLockPinActivity spyActivity = spy(mActivity);
+ doReturn(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX)
+ .when(spyActivity)
+ .getPasswordQuality();
+
+ String password = "1234";
+ assertThat(spyActivity.validatePassword(password))
+ .isEqualTo(ChooseLockPinActivity.CONTAINS_SEQUENTIAL_DIGITS);
+ }
+
+ /**
+ * A test to verify that the activity is finished when save worker succeeds
+ */
+ @Test
+ public void testActivityIsFinishedWhenSaveWorkerSucceeds() {
+ ChooseLockPinActivity spyActivity = spy(mActivity);
+
+ Intent intent = new Intent();
+ intent.putExtra(SaveChosenLockWorkerBase.EXTRA_KEY_SUCCESS, true);
+ spyActivity.onChosenLockSaveFinished(intent);
+
+ verify(spyActivity).finish();
+ }
+
+ /**
+ * A test to verify that the UI stage is updated when save worker fails
+ */
+ @Test
+ public void testStageIsUpdatedWhenSaveWorkerFails() {
+ ChooseLockPinActivity spyActivity = spy(mActivity);
+ doNothing().when(spyActivity).updateStage(ChooseLockPinActivity.Stage.SaveFailure);
+
+ Intent intent = new Intent();
+ intent.putExtra(SaveChosenLockWorkerBase.EXTRA_KEY_SUCCESS, false);
+ spyActivity.onChosenLockSaveFinished(intent);
+
+ verify(spyActivity, never()).finish();
+ verify(spyActivity).updateStage(ChooseLockPinActivity.Stage.SaveFailure);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/security/SaveLockPasswordWorkerTest.java b/tests/robotests/src/com/android/car/settings/security/SaveLockPasswordWorkerTest.java
index 94c5329..33d6765 100644
--- a/tests/robotests/src/com/android/car/settings/security/SaveLockPasswordWorkerTest.java
+++ b/tests/robotests/src/com/android/car/settings/security/SaveLockPasswordWorkerTest.java
@@ -1,18 +1,18 @@
/*
-* Copyright (C) 2018 Google Inc.
-*
-* 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.
-*/
+ * Copyright (C) 2018 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.car.settings.security;
diff --git a/tests/robotests/src/com/android/car/settings/security/SaveLockPatternWorkerTest.java b/tests/robotests/src/com/android/car/settings/security/SaveLockPatternWorkerTest.java
index 11de1dd..90d90d0 100644
--- a/tests/robotests/src/com/android/car/settings/security/SaveLockPatternWorkerTest.java
+++ b/tests/robotests/src/com/android/car/settings/security/SaveLockPatternWorkerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 Google Inc.
+ * Copyright (C) 2018 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.