1 /* 2 * Copyright (C) 2023 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 android.annotation.CallSuper; 20 import android.content.Context; 21 import android.text.InputType; 22 import android.text.TextUtils; 23 import android.util.AttributeSet; 24 import android.view.accessibility.AccessibilityEvent; 25 import android.view.accessibility.AccessibilityManager; 26 import android.view.accessibility.AccessibilityNodeInfo; 27 import android.widget.EditText; 28 import android.widget.FrameLayout; 29 30 /** 31 * A View similar to a textView which contains password text and can animate when the text is 32 * changed 33 */ 34 public abstract class BasePasswordTextView extends FrameLayout { 35 private String mText = ""; 36 private UserActivityListener mUserActivityListener; 37 protected boolean mIsPinHinting; 38 protected PinShapeInput mPinShapeInput; 39 protected boolean mShowPassword = true; 40 protected boolean mUsePinShapes = false; 41 protected static final char DOT = '\u2022'; 42 43 /** Listens to user activities like appending, deleting and resetting PIN text */ 44 public interface UserActivityListener { 45 46 /** Listens to user activities. */ onUserActivity()47 void onUserActivity(); 48 } 49 BasePasswordTextView(Context context)50 public BasePasswordTextView(Context context) { 51 this(context, null); 52 } 53 BasePasswordTextView(Context context, AttributeSet attrs)54 public BasePasswordTextView(Context context, AttributeSet attrs) { 55 this(context, attrs, 0); 56 } 57 BasePasswordTextView(Context context, AttributeSet attrs, int defStyleAttr)58 public BasePasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) { 59 this(context, attrs, defStyleAttr, 0); 60 } 61 BasePasswordTextView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)62 public BasePasswordTextView( 63 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 64 super(context, attrs, defStyleAttr, defStyleRes); 65 } 66 inflatePinShapeInput(boolean isPinHinting)67 protected abstract PinShapeInput inflatePinShapeInput(boolean isPinHinting); 68 shouldSendAccessibilityEvent()69 protected abstract boolean shouldSendAccessibilityEvent(); 70 onAppend(char c, int newLength)71 protected void onAppend(char c, int newLength) {} 72 onDelete(int index)73 protected void onDelete(int index) {} 74 onReset(boolean animated)75 protected void onReset(boolean animated) {} 76 77 @CallSuper onUserActivity()78 protected void onUserActivity() { 79 if (mUserActivityListener != null) { 80 mUserActivityListener.onUserActivity(); 81 } 82 } 83 84 @Override hasOverlappingRendering()85 public boolean hasOverlappingRendering() { 86 return false; 87 } 88 89 /** Appends a PIN text */ append(char c)90 public void append(char c) { 91 CharSequence textbefore = getTransformedText(); 92 93 mText = mText + c; 94 int newLength = mText.length(); 95 onAppend(c, newLength); 96 97 if (mPinShapeInput != null) { 98 mPinShapeInput.append(); 99 } 100 101 onUserActivity(); 102 103 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1); 104 } 105 106 /** Sets a listener who is notified on user activity */ setUserActivityListener(UserActivityListener userActivityListener)107 public void setUserActivityListener(UserActivityListener userActivityListener) { 108 mUserActivityListener = userActivityListener; 109 } 110 111 /** Deletes the last PIN text */ deleteLastChar()112 public void deleteLastChar() { 113 int length = mText.length(); 114 if (length > 0) { 115 CharSequence textbefore = getTransformedText(); 116 117 mText = mText.substring(0, length - 1); 118 onDelete(length - 1); 119 120 if (mPinShapeInput != null) { 121 mPinShapeInput.delete(); 122 } 123 124 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0); 125 } 126 onUserActivity(); 127 } 128 129 /** Gets entered PIN text */ getText()130 public String getText() { 131 return mText; 132 } 133 134 /** Gets a transformed text for accessibility event. Called before text changed. */ getTransformedText()135 protected CharSequence getTransformedText() { 136 return String.valueOf(DOT).repeat(mText.length()); 137 } 138 139 /** Gets a transformed text for accessibility event. Called after text changed. */ getTransformedText(int fromIndex, int removedCount, int addedCount)140 protected CharSequence getTransformedText(int fromIndex, int removedCount, int addedCount) { 141 return getTransformedText(); 142 } 143 144 /** Reset PIN text without error */ reset(boolean animated, boolean announce)145 public void reset(boolean animated, boolean announce) { 146 reset(false /* error */, animated, announce); 147 } 148 149 /** Reset PIN text */ reset(boolean error, boolean animated, boolean announce)150 public void reset(boolean error, boolean animated, boolean announce) { 151 CharSequence textbefore = getTransformedText(); 152 153 mText = ""; 154 155 onReset(animated); 156 if (animated) { 157 onUserActivity(); 158 } 159 160 if (mPinShapeInput != null) { 161 if (error) { 162 mPinShapeInput.resetWithError(); 163 } else { 164 mPinShapeInput.reset(); 165 } 166 } 167 168 if (announce) { 169 sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0); 170 } 171 } 172 sendAccessibilityEventTypeViewTextChanged( CharSequence beforeText, int fromIndex, int removedCount, int addedCount)173 void sendAccessibilityEventTypeViewTextChanged( 174 CharSequence beforeText, int fromIndex, int removedCount, int addedCount) { 175 if (AccessibilityManager.getInstance(mContext).isEnabled() 176 && shouldSendAccessibilityEvent()) { 177 AccessibilityEvent event = 178 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 179 event.setFromIndex(fromIndex); 180 event.setRemovedCount(removedCount); 181 event.setAddedCount(addedCount); 182 event.setBeforeText(beforeText); 183 CharSequence transformedText = getTransformedText(fromIndex, removedCount, addedCount); 184 if (!TextUtils.isEmpty(transformedText)) { 185 event.getText().add(transformedText); 186 } 187 event.setPassword(true); 188 sendAccessibilityEventUnchecked(event); 189 } 190 } 191 192 /** Sets whether to use pin shapes. */ setUsePinShapes(boolean usePinShapes)193 public void setUsePinShapes(boolean usePinShapes) { 194 mUsePinShapes = usePinShapes; 195 } 196 197 /** Determines whether AutoConfirmation feature is on. */ setIsPinHinting(boolean isPinHinting)198 public void setIsPinHinting(boolean isPinHinting) { 199 // Do not reinflate the view if we are using the same one. 200 if (mPinShapeInput != null && mIsPinHinting == isPinHinting) { 201 return; 202 } 203 mIsPinHinting = isPinHinting; 204 205 if (mPinShapeInput != null) { 206 removeView(mPinShapeInput.getView()); 207 mPinShapeInput = null; 208 } 209 210 mPinShapeInput = inflatePinShapeInput(isPinHinting); 211 addView(mPinShapeInput.getView()); 212 } 213 214 /** Controls whether the last entered digit is briefly shown after being entered */ setShowPassword(boolean enabled)215 public void setShowPassword(boolean enabled) { 216 mShowPassword = enabled; 217 } 218 219 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)220 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 221 super.onInitializeAccessibilityEvent(event); 222 223 event.setClassName(EditText.class.getName()); 224 event.setPassword(true); 225 } 226 227 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)228 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 229 super.onInitializeAccessibilityNodeInfo(info); 230 231 info.setClassName(EditText.class.getName()); 232 info.setPassword(true); 233 info.setText(getTransformedText()); 234 info.setSelected(false); 235 info.setEditable(true); 236 237 info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD); 238 } 239 } 240