• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 android.view.autofill;
18 
19 import static android.view.autofill.Helper.sDebug;
20 
21 import android.annotation.NonNull;
22 import android.util.ArrayMap;
23 import android.util.Log;
24 import android.util.Slog;
25 import android.view.View;
26 import android.widget.TextView;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Objects;
37 import java.util.Set;
38 
39 /**
40  * This class manages and stores the autofillable views fingerprints for use in relayout situations.
41  * @hide
42  */
43 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
44 public final class AutofillStateFingerprint {
45 
46     ArrayList<AutofillId> mPriorAutofillIds;
47     ArrayList<Integer> mViewHashCodes; // each entry corresponding to mPriorAutofillIds .
48 
49     boolean mHideHighlight = false;
50 
51     private int mSessionId;
52 
53     Map<Integer, AutofillId> mHashToAutofillIdMap = new ArrayMap<>();
54     Map<AutofillId, AutofillId> mOldIdsToCurrentAutofillIdMap = new ArrayMap<>();
55 
56     // These failed id's are attempted to be refilled again after relayout.
57     private ArrayList<AutofillId> mFailedIds = new ArrayList<>();
58     private ArrayList<AutofillValue> mFailedAutofillValues = new ArrayList<>();
59 
60     // whether to use relative positions for computing hashes.
61     private boolean mUseRelativePosition;
62 
63     private static final String TAG = "AutofillStateFingerprint";
64 
65     /**
66      * Returns an instance of this class
67      */
68     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
createInstance()69     public static AutofillStateFingerprint createInstance() {
70         return new AutofillStateFingerprint();
71     }
72 
AutofillStateFingerprint()73     private AutofillStateFingerprint() {
74     }
75 
76     /**
77      * Set sessionId for the instance
78      */
setSessionId(int sessionId)79     void setSessionId(int sessionId) {
80         mSessionId = sessionId;
81     }
82 
83     /**
84      * Sets whether relative position of the views should be used to calculate fingerprints.
85      */
setUseRelativePosition(boolean useRelativePosition)86     void setUseRelativePosition(boolean useRelativePosition) {
87         mUseRelativePosition = useRelativePosition;
88     }
89 
90     /**
91      * Store the state of the views prior to the authentication.
92      */
storeStatePriorToAuthentication( AutofillManager.AutofillClient client, Set<AutofillId> autofillIds)93     void storeStatePriorToAuthentication(
94             AutofillManager.AutofillClient client, Set<AutofillId> autofillIds) {
95         if (mUseRelativePosition) {
96             List<View> autofillableViews = client.autofillClientFindAutofillableViewsByTraversal();
97             if (sDebug) {
98                 Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size());
99             }
100 
101             ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews);
102             for (Map.Entry<Integer, View> entry : hashes.entrySet()) {
103                 View view = entry.getValue();
104                 if (view != null) {
105                     mHashToAutofillIdMap.put(entry.getKey(), view.getAutofillId());
106                 } else {
107                     if (sDebug) {
108                         Log.d(TAG, "Encountered null view");
109                     }
110                 }
111             }
112         } else {
113             // Just use the provided autofillIds and get their hashes
114             if (sDebug) {
115                 Log.d(TAG, "Size of autofillId's being stored: " + autofillIds.size()
116                         + " list:" + autofillIds);
117             }
118             AutofillId[] autofillIdsArr = Helper.toArray(autofillIds);
119             View[] views = client.autofillClientFindViewsByAutofillIdTraversal(autofillIdsArr);
120             for (int i = 0; i < autofillIdsArr.length; i++) {
121                 View view = views[i];
122                 if (view != null) {
123                     int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */);
124                     AutofillId autofillId = view.getAutofillId();
125                     mHashToAutofillIdMap.put(id, autofillId);
126                 } else {
127                     if (sDebug) {
128                         Log.d(TAG, "Encountered null view");
129                     }
130                 }
131             }
132         }
133     }
134 
135     /**
136      * Store failed ids, so that they can be refilled later
137      */
storeFailedIdsAndValues( @onNull ArrayList<AutofillId> failedIds, ArrayList<AutofillValue> failedAutofillValues, boolean hideHighlight)138     void storeFailedIdsAndValues(
139             @NonNull ArrayList<AutofillId> failedIds,
140             ArrayList<AutofillValue> failedAutofillValues,
141             boolean hideHighlight) {
142         for (AutofillId failedId : failedIds) {
143             if (failedId != null) {
144                 failedId.setSessionId(mSessionId);
145             } else {
146                 if (sDebug) {
147                     Log.d(TAG, "Got null failed ids");
148                 }
149             }
150         }
151         mFailedIds = failedIds;
152         mFailedAutofillValues = failedAutofillValues;
153         mHideHighlight = hideHighlight;
154     }
155 
dumpCurrentState()156     private void dumpCurrentState() {
157         Log.d(TAG, "FailedId's: " + mFailedIds);
158         Log.d(TAG, "Hashes from map" + mHashToAutofillIdMap);
159     }
160 
attemptRefill( List<View> currentAutofillableViews, @NonNull AutofillManager autofillManager)161     boolean attemptRefill(
162             List<View> currentAutofillableViews, @NonNull AutofillManager autofillManager) {
163         if (sDebug) {
164             dumpCurrentState();
165         }
166         // For the autofillable views, compute their hashes
167         ArrayMap<Integer, View> currentHashes = getFingerprintIds(currentAutofillableViews);
168 
169         // For the computed hashes, try to look for the old fingerprints.
170         // If match found, update the new autofill ids of those views
171         Map<AutofillId, View> oldFailedIdsToCurrentViewMap = new HashMap<>();
172         for (Map.Entry<Integer, View> entry : currentHashes.entrySet()) {
173             View view = entry.getValue();
174             int currentHash = entry.getKey();
175             AutofillId currentAutofillId = view.getAutofillId();
176             currentAutofillId.setSessionId(mSessionId);
177             if (mHashToAutofillIdMap.containsKey(currentHash)) {
178                 AutofillId oldAutofillId = mHashToAutofillIdMap.get(currentHash);
179                 oldAutofillId.setSessionId(mSessionId);
180                 mOldIdsToCurrentAutofillIdMap.put(oldAutofillId, currentAutofillId);
181                 Log.i(TAG, "Mapping current autofill id: " + view.getAutofillId()
182                         + " to existing autofill id " + oldAutofillId);
183 
184                 oldFailedIdsToCurrentViewMap.put(oldAutofillId, view);
185             } else {
186                 Log.i(TAG, "Couldn't map current autofill id: " + view.getAutofillId()
187                         + " with currentHash:" + currentHash + " for view:" + view);
188             }
189         }
190 
191         int viewsCount = 0;
192         View[] views = new View[mFailedIds.size()];
193         for (int i = 0; i < mFailedIds.size(); i++) {
194             AutofillId oldAutofillId = mFailedIds.get(i);
195             AutofillId currentAutofillId = mOldIdsToCurrentAutofillIdMap.get(oldAutofillId);
196             if (currentAutofillId == null) {
197                 if (sDebug) {
198                     Log.d(TAG, "currentAutofillId = null");
199                 }
200             }
201             mFailedIds.set(i, currentAutofillId);
202             views[i] = oldFailedIdsToCurrentViewMap.get(oldAutofillId);
203             if (views[i] != null) {
204                 viewsCount++;
205             }
206         }
207 
208         if (sDebug) {
209             dumpCurrentState();
210         }
211 
212         // Attempt autofill now
213         Slog.i(TAG, "Attempting refill of views. Found " + viewsCount
214                 + " views to refill from previously " + mFailedIds.size()
215                 + " failed ids:" + mFailedIds);
216         autofillManager.post(
217                 () -> autofillManager.autofill(
218                         views, mFailedIds, mFailedAutofillValues, mHideHighlight,
219                         true /* isRefill */));
220 
221         return false;
222     }
223 
224     /**
225      * Retrieves fingerprint hashes for the views
226      */
getFingerprintIds(@onNull List<View> views)227     ArrayMap<Integer, View> getFingerprintIds(@NonNull List<View> views) {
228         ArrayMap<Integer, View> map = new ArrayMap<>();
229         if (mUseRelativePosition) {
230             Collections.sort(views, (View v1, View v2) -> {
231                 int[] posV1 = v1.getLocationOnScreen();
232                 int[] posV2 = v2.getLocationOnScreen();
233 
234                 int compare = posV1[0] - posV2[0]; // x coordinate
235                 if (compare != 0) {
236                     return compare;
237                 }
238                 compare = posV1[1] - posV2[1]; // y coordinate
239                 if (compare != 0) {
240                     return compare;
241                 }
242                 // Sort on vertical
243                 compare = compareTop(v1, v2);
244                 if (compare != 0) {
245                     return compare;
246                 }
247                 compare = compareBottom(v1, v2);
248                 if (compare != 0) {
249                     return compare;
250                 }
251                 compare = compareLeft(v1, v2);
252                 if (compare != 0) {
253                     return compare;
254                 }
255                 return compareRight(v1, v2);
256                 // Note that if compareRight also returned 0, that means both the views have exact
257                 // same location, so just treat them as equal
258             });
259         }
260         for (int i = 0; i < views.size(); i++) {
261             View view = views.get(i);
262             map.put(getEphemeralFingerprintId(view, i), view);
263         }
264         return map;
265     }
266 
267     /**
268      * Returns fingerprint hash for the view.
269      */
270     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
getEphemeralFingerprintId(View v, int position)271     public int getEphemeralFingerprintId(View v, int position) {
272         if (v == null) return -1;
273         int inputType = Integer.MIN_VALUE;
274         int imeOptions = Integer.MIN_VALUE;
275         boolean isSingleLine = false;
276         CharSequence hints = "";
277         if (v instanceof TextView) {
278             TextView tv = (TextView) v;
279             inputType = tv.getInputType();
280             hints = tv.getHint();
281             isSingleLine = tv.isSingleLine();
282             imeOptions = tv.getImeOptions();
283             // TODO(b/238252288): Consider adding more IME related fields.
284         }
285         CharSequence contentDesc = v.getContentDescription();
286         CharSequence tooltip = v.getTooltipText();
287 
288         int autofillType = v.getAutofillType();
289         String[] autofillHints = v.getAutofillHints();
290         int visibility = v.getVisibility();
291 
292         int paddingLeft = v.getPaddingLeft();
293         int paddingRight = v.getPaddingRight();
294         int paddingTop = v.getPaddingTop();
295         int paddingBottom = v.getPaddingBottom();
296 
297         // TODO(b/238252288): Following are making relayout flaky. Do more analysis to figure out
298         //  why.
299         int height = v.getHeight();
300         int width = v.getWidth();
301 
302         // Order doesn't matter much here. We can change the order, as long as we use the same
303         // order for storing and fetching fingerprints. The order can be changed in platform
304         // versions.
305         int hash = Objects.hash(visibility, inputType, imeOptions, isSingleLine, hints,
306                 contentDesc, tooltip, autofillType, Arrays.deepHashCode(autofillHints),
307                 paddingBottom, paddingTop, paddingRight, paddingLeft);
308         if (mUseRelativePosition) {
309             hash = Objects.hash(hash, position);
310         }
311         if (sDebug) {
312             Log.d(TAG, "Hash: " + hash + " for AutofillId:" + v.getAutofillId()
313                     + " visibility:" + visibility
314                     + " inputType:" + inputType
315                     + " imeOptions:" + imeOptions
316                     + " isSingleLine:" + isSingleLine
317                     + " hints:" + hints
318                     + " contentDesc:" + contentDesc
319                     + " tooltipText:" + tooltip
320                     + " autofillType:" + autofillType
321                     + " autofillHints:" + Arrays.toString(autofillHints)
322                     + " height:" + height
323                     + " width:" + width
324                     + " paddingLeft:" + paddingLeft
325                     + " paddingRight:" + paddingRight
326                     + " paddingTop:" + paddingTop
327                     + " paddingBottom:" + paddingBottom
328                     + " mUseRelativePosition" + mUseRelativePosition
329                     + " position:" + position
330             );
331         }
332         return hash;
333     }
334 
compareTop(View v1, View v2)335     private int compareTop(View v1, View v2) {
336         return v1.getTop() - v2.getTop();
337     }
338 
compareBottom(View v1, View v2)339     private int compareBottom(View v1, View v2) {
340         return v1.getBottom() - v2.getBottom();
341     }
342 
compareLeft(View v1, View v2)343     private int compareLeft(View v1, View v2) {
344         return v1.getLeft() - v2.getLeft();
345     }
346 
compareRight(View v1, View v2)347     private int compareRight(View v1, View v2) {
348         return v1.getRight() - v2.getRight();
349     }
350 }
351