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