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