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