1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.keyguard;
18 
19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static android.view.WindowInsets.Type.ime;
21 
22 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_ADAPTIVE_AUTH_REQUEST;
23 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN;
24 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE;
25 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT;
26 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
27 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
28 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE;
29 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
30 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
31 import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
32 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
33 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
34 
35 import android.animation.Animator;
36 import android.animation.AnimatorListenerAdapter;
37 import android.animation.ValueAnimator;
38 import android.app.ActivityManager;
39 import android.content.Context;
40 import android.content.res.ColorStateList;
41 import android.graphics.Insets;
42 import android.graphics.Rect;
43 import android.os.Trace;
44 import android.util.AttributeSet;
45 import android.view.WindowInsets;
46 import android.view.WindowInsetsAnimationControlListener;
47 import android.view.WindowInsetsAnimationController;
48 import android.widget.TextView;
49 
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.constraintlayout.motion.widget.MotionLayout;
53 
54 import com.android.app.animation.Interpolators;
55 import com.android.internal.widget.LockscreenCredential;
56 import com.android.internal.widget.TextViewInputDisabler;
57 import com.android.systemui.DejankUtils;
58 import com.android.systemui.res.R;
59 import com.android.systemui.statusbar.policy.DevicePostureController;
60 
61 /**
62  * Displays an alphanumeric (latin-1) key entry for the user to enter
63  * an unlock password
64  */
65 public class KeyguardPasswordView extends KeyguardAbsKeyInputView {
66 
67     private TextView mPasswordEntry;
68     private TextViewInputDisabler mPasswordEntryDisabler;
69     private DisappearAnimationListener mDisappearAnimationListener;
70     @Nullable private MotionLayout mContainerMotionLayout;
71     private boolean mAlreadyUsingSplitBouncer = false;
72     private boolean mIsLockScreenLandscapeEnabled = false;
73     @DevicePostureController.DevicePostureInt
74     private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
75     private static final int[] DISABLE_STATE_SET = {-android.R.attr.state_enabled};
76     private static final int[] ENABLE_STATE_SET = {android.R.attr.state_enabled};
77 
KeyguardPasswordView(Context context)78     public KeyguardPasswordView(Context context) {
79         this(context, null);
80     }
81 
KeyguardPasswordView(Context context, AttributeSet attrs)82     public KeyguardPasswordView(Context context, AttributeSet attrs) {
83         super(context, attrs);
84     }
85 
86     /**
87      * Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
88      * enabled
89      */
setIsLockScreenLandscapeEnabled()90     public void setIsLockScreenLandscapeEnabled() {
91         mIsLockScreenLandscapeEnabled = true;
92         findContainerLayout();
93     }
94 
findContainerLayout()95     private void findContainerLayout() {
96         if (mIsLockScreenLandscapeEnabled) {
97             mContainerMotionLayout = findViewById(R.id.password_container);
98         }
99     }
100 
101     @Override
resetState()102     protected void resetState() {
103     }
104 
105     @Override
getPasswordTextViewId()106     protected int getPasswordTextViewId() {
107         return R.id.passwordEntry;
108     }
109 
110     @Override
getPromptReasonStringRes(int reason)111     protected int getPromptReasonStringRes(int reason) {
112         switch (reason) {
113             case PROMPT_REASON_RESTART:
114                 return R.string.kg_prompt_reason_restart_password;
115             case PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE:
116                 return R.string.kg_prompt_after_update_password;
117             case PROMPT_REASON_TIMEOUT:
118                 return R.string.kg_prompt_reason_timeout_password;
119             case PROMPT_REASON_DEVICE_ADMIN:
120                 return R.string.kg_prompt_reason_device_admin;
121             case PROMPT_REASON_USER_REQUEST:
122                 return R.string.kg_prompt_after_user_lockdown_password;
123             case PROMPT_REASON_PREPARE_FOR_UPDATE:
124                 return R.string.kg_prompt_added_security_password;
125             case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
126                 return R.string.kg_prompt_reason_timeout_password;
127             case PROMPT_REASON_TRUSTAGENT_EXPIRED:
128                 return R.string.kg_prompt_reason_timeout_password;
129             case PROMPT_REASON_ADAPTIVE_AUTH_REQUEST:
130                 return R.string.kg_prompt_after_adaptive_auth_lock;
131             case PROMPT_REASON_NONE:
132                 return 0;
133             default:
134                 return R.string.kg_prompt_reason_timeout_password;
135         }
136     }
137 
onDevicePostureChanged(@evicePostureController.DevicePostureInt int posture)138     void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) {
139         if (mLastDevicePosture == posture) return;
140         mLastDevicePosture = posture;
141 
142         if (mIsLockScreenLandscapeEnabled) {
143             boolean useSplitBouncerAfterFold =
144                     mLastDevicePosture == DEVICE_POSTURE_CLOSED
145                     && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
146                     && getResources().getBoolean(R.bool.update_bouncer_constraints);
147 
148             if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) {
149                 updateConstraints(useSplitBouncerAfterFold);
150             }
151         }
152 
153     }
154 
155     @Override
updateConstraints(boolean useSplitBouncer)156     protected void updateConstraints(boolean useSplitBouncer) {
157         mAlreadyUsingSplitBouncer = useSplitBouncer;
158         if (useSplitBouncer) {
159             mContainerMotionLayout.jumpToState(R.id.split_constraints);
160             mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
161         } else {
162             mContainerMotionLayout.jumpToState(R.id.single_constraints);
163             mContainerMotionLayout.setMaxWidth(getResources()
164                     .getDimensionPixelSize(R.dimen.keyguard_security_width));
165         }
166     }
167 
168     @Override
onFinishInflate()169     protected void onFinishInflate() {
170         super.onFinishInflate();
171 
172         mPasswordEntry = findViewById(getPasswordTextViewId());
173         mPasswordEntryDisabler = new TextViewInputDisabler(mPasswordEntry);
174 
175         // EditText cursor can fail screenshot tests, so disable it when testing
176         if (ActivityManager.isRunningInTestHarness()) {
177             mPasswordEntry.setCursorVisible(false);
178         }
179     }
180 
181     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)182     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
183         // send focus to the password field
184         return mPasswordEntry.requestFocus(direction, previouslyFocusedRect);
185     }
186 
187     @Override
resetPasswordText(boolean animate, boolean announce)188     protected void resetPasswordText(boolean animate, boolean announce) {
189         mPasswordEntry.setText("");
190     }
191 
192     @Override
getEnteredCredential()193     protected LockscreenCredential getEnteredCredential() {
194         return LockscreenCredential.createPasswordOrNone(mPasswordEntry.getText());
195     }
196 
197     @Override
setPasswordEntryEnabled(boolean enabled)198     protected void setPasswordEntryEnabled(boolean enabled) {
199         int color = mPasswordEntry.getTextColors().getColorForState(
200                 enabled ? ENABLE_STATE_SET : DISABLE_STATE_SET, 0);
201         mPasswordEntry.setBackgroundTintList(ColorStateList.valueOf(color));
202         mPasswordEntry.setCursorVisible(enabled);
203     }
204 
205     @Override
setPasswordEntryInputEnabled(boolean enabled)206     protected void setPasswordEntryInputEnabled(boolean enabled) {
207         mPasswordEntryDisabler.setInputEnabled(enabled);
208     }
209 
210     @Override
getWrongPasswordStringId()211     public int getWrongPasswordStringId() {
212         return R.string.kg_wrong_password;
213     }
214 
215     @Override
startAppearAnimation()216     public void startAppearAnimation() {
217         // Reset state, and let IME animation reveal the view as it slides in, if one exists.
218         // It is possible for an IME to have no view, so provide a default animation since no
219         // calls to animateForIme would occur
220         setAlpha(0f);
221         animate()
222             .alpha(1f)
223             .setDuration(300)
224             .start();
225 
226         setTranslationY(0f);
227     }
228 
229     @Override
startDisappearAnimation(Runnable finishRunnable)230     public boolean startDisappearAnimation(Runnable finishRunnable) {
231         getWindowInsetsController().controlWindowInsetsAnimation(ime(),
232                 100,
233                 Interpolators.LINEAR, null, new WindowInsetsAnimationControlListener() {
234 
235                     @Override
236                     public void onReady(@NonNull WindowInsetsAnimationController controller,
237                             int types) {
238                         ValueAnimator anim = ValueAnimator.ofFloat(1f, 0f);
239                         anim.addUpdateListener(animation -> {
240                             if (controller.isCancelled()) {
241                                 return;
242                             }
243                             float value = (float) animation.getAnimatedValue();
244                             float fraction = anim.getAnimatedFraction();
245                             Insets shownInsets = controller.getShownStateInsets();
246                             int dist = (int) (-shownInsets.bottom / 4
247                                     * fraction);
248                             Insets insets = Insets.add(shownInsets, Insets.of(0, 0, 0, dist));
249                             if (mDisappearAnimationListener != null) {
250                                 mDisappearAnimationListener.setTranslationY(-dist);
251                             }
252 
253                             controller.setInsetsAndAlpha(insets, value, fraction);
254                             setAlpha(value);
255                         });
256                         anim.addListener(new AnimatorListenerAdapter() {
257                             @Override
258                             public void onAnimationStart(Animator animation) {
259                             }
260 
261                             @Override
262                             public void onAnimationEnd(Animator animation) {
263                                 // Run this in the next frame since it results in a slow binder call
264                                 // to InputMethodManager#hideSoftInput()
265                                 DejankUtils.postAfterTraversal(() -> {
266                                     Trace.beginSection("KeyguardPasswordView#onAnimationEnd");
267                                     // // TODO(b/230620476): Make hideSoftInput oneway
268                                     // controller.finish() eventually calls hideSoftInput
269                                     controller.finish(false);
270                                     runOnFinishImeAnimationRunnable();
271                                     finishRunnable.run();
272                                     mDisappearAnimationListener = null;
273                                     Trace.endSection();
274                                 });
275                             }
276                         });
277                         anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
278                         anim.start();
279                     }
280 
281                     @Override
282                     public void onFinished(
283                             @NonNull WindowInsetsAnimationController controller) {
284                     }
285 
286                     @Override
287                     public void onCancelled(
288                             @Nullable WindowInsetsAnimationController controller) {
289                         // It is possible to be denied control of ime insets, which means onReady
290                         // is never called. We still need to notify the runnables in order to
291                         // complete the bouncer disappearing
292                         runOnFinishImeAnimationRunnable();
293                         finishRunnable.run();
294                     }
295                 });
296         return true;
297     }
298 
299     @Override
getTitle()300     public CharSequence getTitle() {
301         return getResources().getString(
302                 com.android.internal.R.string.keyguard_accessibility_password_unlock);
303     }
304 
305     @Override
onApplyWindowInsets(WindowInsets insets)306     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
307         if (!mPasswordEntry.isFocused() && isVisibleToUser()) {
308             mPasswordEntry.requestFocus();
309         }
310         return super.onApplyWindowInsets(insets);
311     }
312 
313     @Override
onWindowFocusChanged(boolean hasWindowFocus)314     public void onWindowFocusChanged(boolean hasWindowFocus) {
315         super.onWindowFocusChanged(hasWindowFocus);
316         if (hasWindowFocus) {
317             if (isVisibleToUser()) {
318                 showKeyboard();
319             } else {
320                 hideKeyboard();
321             }
322         }
323     }
324 
325     /**
326      * Sends signal to the focused window to show the keyboard.
327      */
showKeyboard()328     public void showKeyboard() {
329         post(() -> {
330             if (mPasswordEntry.isAttachedToWindow()
331                     && !mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
332                 mPasswordEntry.requestFocus();
333                 mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
334             }
335         });
336     }
337 
338     /**
339      * Sends signal to the focused window to hide the keyboard.
340      */
hideKeyboard()341     public void hideKeyboard() {
342         post(() -> {
343             if (mPasswordEntry.isAttachedToWindow()
344                     && mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
345                 mPasswordEntry.clearFocus();
346                 mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
347             }
348         });
349     }
350 
351     /**
352      * Listens to the progress of the disappear animation and handles it.
353      */
354     interface DisappearAnimationListener {
setTranslationY(int transY)355         void setTranslationY(int transY);
356     }
357 
358     /**
359      * Set an instance of the disappear animation listener to this class. This will be
360      * removed when the animation completes.
361      */
setDisappearAnimationListener(DisappearAnimationListener listener)362     public void setDisappearAnimationListener(DisappearAnimationListener listener) {
363         mDisappearAnimationListener = listener;
364     }
365 }
366