diff options
| author | 2024-07-26 18:15:36 +0000 | |
|---|---|---|
| committer | 2024-07-26 18:15:36 +0000 | |
| commit | 52fef1b2cd86f3058a91fb8fd215695dd7878fca (patch) | |
| tree | ea9dca52580f706cc3467bcbd912e30207726239 | |
| parent | ecec0dcaec2f310f53b1d6b70410820de81f4286 (diff) | |
| parent | a34dbfa9172bff40c3dcc6704e959a96efc356b3 (diff) | |
Merge changes I5f492a5f,I79c850b5,I302584ad into main
* changes:
[Relayout] Implement Logging
[Relayout] Part 5: Location based fingerprinting.
[Relayout] Part 4: Implement core logic
10 files changed, 862 insertions, 39 deletions
diff --git a/core/java/android/view/autofill/AutofillClientController.java b/core/java/android/view/autofill/AutofillClientController.java index d505c733b3e9..95cae226ca85 100644 --- a/core/java/android/view/autofill/AutofillClientController.java +++ b/core/java/android/view/autofill/AutofillClientController.java @@ -582,4 +582,9 @@ public final class AutofillClientController implements AutofillManager.AutofillC Log.e(TAG, "authenticate() failed for intent:" + intent, e); } } + + @Override + public boolean isActivityResumed() { + return mActivity.isResumed(); + } } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 02a86c9eecb3..79ecfe1e9141 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -114,6 +114,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -774,6 +775,13 @@ public final class AutofillManager { // dataset in responses. Used to avoid request pre-fill request again and again. private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>(); + // Whether we need to re-attempt fill again. Needed for case of relayout. + private boolean mFillReAttemptNeeded = false; + + private Map<Integer, AutofillId> mFingerprintToViewMap = new ArrayMap<>(); + + private AutofillStateFingerprint mAutofillStateFingerprint; + /** @hide */ public interface AutofillClient { /** @@ -909,6 +917,11 @@ public final class AutofillManager { * @return An ID that is unique in the activity. */ @Nullable AutofillId autofillClientGetNextAutofillId(); + + /** + * @return Whether the activity is resumed or not. + */ + boolean isActivityResumed(); } /** @@ -919,6 +932,7 @@ public final class AutofillManager { mService = service; mOptions = context.getAutofillOptions(); mIsFillRequested = new AtomicBoolean(false); + mAutofillStateFingerprint = AutofillStateFingerprint.createInstance(); mIsFillDialogEnabled = AutofillFeatureFlags.isFillDialogEnabled(); mFillDialogEnabledHints = AutofillFeatureFlags.getFillDialogEnabledHints(); @@ -1357,6 +1371,20 @@ public final class AutofillManager { mOnInvisibleCalled = true; if (isExpiredResponse) { + if (mRelayoutFix && isAuthenticationPending()) { + Log.i(TAG, "onInvisibleForAutofill(): Ignoring expiringResponse due to pending" + + " authentication"); + try { + mService.notifyNotExpiringResponseDuringAuth( + mSessionId, mContext.getUserId()); + } catch (RemoteException e) { + // The failure could be a consequence of something going wrong on the + // server side. Do nothing here since it's just logging, but it's + // possible follow-up actions may fail. + } + return; + } + Log.i(TAG, "onInvisibleForAutofill(): expiringResponse"); // Notify service the response has expired. updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null, ACTION_RESPONSE_EXPIRED, /* flags= */ 0); @@ -1513,14 +1541,29 @@ public final class AutofillManager { } /** + * Called to log notify view entered was ignored due to pending auth + * @hide + */ + public void notifyViewEnteredIgnoredDuringAuthCount() { + try { + mService.notifyViewEnteredIgnoredDuringAuthCount(mSessionId, mContext.getUserId()); + } catch (RemoteException e) { + // The failure could be a consequence of something going wrong on the + // server side. Do nothing here since it's just logging, but it's + // possible follow-up actions may fail. + } + } + + /** * Called to check if we should retry fill. * Useful for knowing whether to attempt refill after relayout. * * @hide */ public boolean shouldRetryFill() { - // TODO: Implement in follow-up cl - return false; + synchronized (mLock) { + return isAuthenticationPending() && mFillReAttemptNeeded; + } } /** @@ -1531,8 +1574,13 @@ public final class AutofillManager { */ public boolean attemptRefill() { Log.i(TAG, "Attempting refill"); - // TODO: Implement in follow-up cl - return false; + // Find active autofillable views. Compute their fingerprints + List<View> autofillableViews = + getClient().autofillClientFindAutofillableViewsByTraversal(); + if (sDebug) { + Log.d(TAG, "Autofillable views count:" + autofillableViews.size()); + } + return mAutofillStateFingerprint.attemptRefill(autofillableViews, this); } /** @@ -2493,7 +2541,13 @@ public final class AutofillManager { /** @hide */ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) { + if (sVerbose) { + Log.v(TAG, "onAuthenticationResult(): authId= " + authenticationId + ", data=" + data); + } if (!hasAutofillFeature()) { + if (sVerbose) { + Log.v(TAG, "onAuthenticationResult(): autofill not enabled"); + } return; } // TODO: the result code is being ignored, so this method is not reliably @@ -2501,10 +2555,6 @@ public final class AutofillManager { // set the EXTRA_AUTHENTICATION_RESULT extra, but it could cause weird results if the // service set the extra and returned RESULT_CANCELED... - if (sDebug) { - Log.d(TAG, "onAuthenticationResult(): id= " + authenticationId + ", data=" + data); - } - synchronized (mLock) { if (!isActiveLocked()) { Log.w(TAG, "onAuthenticationResult(): sessionId=" + mSessionId + " not active"); @@ -2661,6 +2711,7 @@ public final class AutofillManager { mSessionId = receiver.getIntResult(); if (mSessionId != NO_SESSION) { mState = STATE_ACTIVE; + mAutofillStateFingerprint.setSessionId(mSessionId); } final int extraFlags = receiver.getOptionalExtraIntResult(0); if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) { @@ -2722,6 +2773,9 @@ public final class AutofillManager { if (resetEnteredIds) { mEnteredIds = null; } + mFillReAttemptNeeded = false; + mFingerprintToViewMap.clear(); + mAutofillStateFingerprint = AutofillStateFingerprint.createInstance(); } @GuardedBy("mLock") @@ -2984,8 +3038,12 @@ public final class AutofillManager { Intent fillInIntent, boolean authenticateInline) { synchronized (mLock) { if (sessionId == mSessionId) { - if (mRelayoutFixDeprecated) { + if (mRelayoutFixDeprecated || mRelayoutFix) { mState = STATE_PENDING_AUTHENTICATION; + if (sVerbose) { + Log.v(TAG, "entering STATE_PENDING_AUTHENTICATION : mRelayoutFix:" + + mRelayoutFix); + } } final AutofillClient client = getClient(); if (client != null) { @@ -3191,17 +3249,56 @@ public final class AutofillManager { @GuardedBy("mLock") private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds) { + handleFailedIdsLocked(failedIds, null, false, false); + } + + @GuardedBy("mLock") + private void handleFailedIdsLocked(@NonNull ArrayList<AutofillId> failedIds, + ArrayList<AutofillValue> failedAutofillValues, boolean hideHighlight, + boolean isRefill) { if (!failedIds.isEmpty() && sVerbose) { Log.v(TAG, "autofill(): total failed views: " + failedIds); } + + if (mRelayoutFix && !failedIds.isEmpty()) { + // Activity isn't in resumed state, so it's very possible that relayout could've + // occurred, so wait for it to declare proper failure. It's a temporary failure at the + // moment. We'll try again later when the activity is resumed. + + // The above doesn't seem to be the correct way. Look for pending auth cases. + // TODO(b/238252288): Check whether there was any auth done at all + mFillReAttemptNeeded = true; + mAutofillStateFingerprint.storeFailedIdsAndValues( + failedIds, failedAutofillValues, hideHighlight); + } try { - mService.setAutofillFailure(mSessionId, failedIds, mContext.getUserId()); + mService.setAutofillFailure(mSessionId, failedIds, isRefill, mContext.getUserId()); } catch (RemoteException e) { // In theory, we could ignore this error since it's not a big deal, but // in reality, we rather crash the app anyways, as the failure could be // a consequence of something going wrong on the server side... throw e.rethrowFromSystemServer(); } + if (mRelayoutFix && !failedIds.isEmpty()) { + if (!getClient().isActivityResumed()) { + if (sVerbose) { + Log.v(TAG, "handleFailedIdsLocked(): failed id's exist, but activity not" + + " resumed"); + } + } else { + if (isRefill) { + Log.i(TAG, "handleFailedIdsLocked(): Attempted refill, but failed"); + } else { + // activity has been resumed, try to re-fill + // getClient().isActivityResumed() && !failedIds.isEmpty() && !isRefill + // TODO(b/238252288): Do better state management, and only trigger the following + // if there was auth previously. + Log.i(TAG, "handleFailedIdsLocked(): Attempting refill"); + attemptRefill(); + mFillReAttemptNeeded = false; + } + } + } } private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values, @@ -3216,13 +3313,46 @@ public final class AutofillManager { return; } + final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( + Helper.toArray(ids)); + + autofill(views, ids, values, hideHighlight, false); + } + } + + void autofill(View[] views, List<AutofillId> ids, List<AutofillValue> values, + boolean hideHighlight, boolean isRefill) { + if (sVerbose) { + Log.v(TAG, "autofill() ids:" + ids + " isRefill:" + isRefill); + } + synchronized (mLock) { + final AutofillClient client = getClient(); + if (client == null) { + return; + } + + if (ids == null) { + Log.i(TAG, "autofill(): No id's to fill"); + return; + } + + if (mRelayoutFix && isRefill) { + try { + mService.setAutofillIdsAttemptedForRefill( + mSessionId, ids, mContext.getUserId()); + } catch (RemoteException e) { + // The failure could be a consequence of something going wrong on the + // server side. Do nothing here since it's just logging, but it's + // possible follow-up actions may fail. + } + } + final int itemCount = ids.size(); int numApplied = 0; ArrayMap<View, SparseArray<AutofillValue>> virtualValues = null; - final View[] views = client.autofillClientFindViewsByAutofillIdTraversal( - Helper.toArray(ids)); ArrayList<AutofillId> failedIds = new ArrayList<>(); + ArrayList<AutofillValue> failedAutofillValues = new ArrayList<>(); if (mLastAutofilledData == null) { mLastAutofilledData = new ParcelableMap(itemCount); @@ -3237,7 +3367,9 @@ public final class AutofillManager { // the service; this is fine, but we need to update the view status in the // server side so it can be triggered again. Log.d(TAG, "autofill(): no View with id " + id); + // Possible relayout scenario failedIds.add(id); + failedAutofillValues.add(value); continue; } // Mark the view as to be autofilled with 'value' @@ -3268,7 +3400,8 @@ public final class AutofillManager { } } - handleFailedIdsLocked(failedIds); + handleFailedIdsLocked( + failedIds, failedAutofillValues, hideHighlight, isRefill); if (virtualValues != null) { for (int i = 0; i < virtualValues.size(); i++) { @@ -3322,7 +3455,7 @@ public final class AutofillManager { private void reportAutofillContentFailure(AutofillId id) { try { mService.setAutofillFailure(mSessionId, Collections.singletonList(id), - mContext.getUserId()); + false /* isRefill */, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -3349,20 +3482,22 @@ public final class AutofillManager { } /** - * Set the tracked views. + * Set the tracked views. * - * @param trackedIds The views to be tracked. + * @param trackedIds The views to be tracked. * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible. - * @param saveOnFinish Finish the session once the activity is finished. - * @param fillableIds Views that might anchor FillUI. - * @param saveTriggerId View that when clicked triggers commit(). + * @param saveOnFinish Finish the session once the activity is finished. + * @param fillableIds Views that might anchor FillUI. + * @param saveTriggerId View that when clicked triggers commit(). */ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds, boolean saveOnAllViewsInvisible, boolean saveOnFinish, - @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) { + @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId, + boolean shouldGrabViewFingerprints) { if (saveTriggerId != null) { saveTriggerId.resetSessionId(); } + final ArraySet<AutofillId> allFillableIds = new ArraySet<>(); synchronized (mLock) { if (sVerbose) { Log.v(TAG, "setTrackedViews(): sessionId=" + sessionId @@ -3372,6 +3507,7 @@ public final class AutofillManager { + ", fillableIds=" + Arrays.toString(fillableIds) + ", saveTrigerId=" + saveTriggerId + ", mFillableIds=" + mFillableIds + + ", shouldGrabViewFingerprints=" + shouldGrabViewFingerprints + ", mEnabled=" + mEnabled + ", mSessionId=" + mSessionId); } @@ -3405,7 +3541,6 @@ public final class AutofillManager { trackedIds = null; } - final ArraySet<AutofillId> allFillableIds = new ArraySet<>(); if (mFillableIds != null) { allFillableIds.addAll(mFillableIds); } @@ -3424,6 +3559,12 @@ public final class AutofillManager { mTrackedViews = null; } } + if (mRelayoutFix && shouldGrabViewFingerprints) { + // For all the views: tracked and others, calculate fingerprints and store them. + mAutofillStateFingerprint.setUseRelativePosition(mRelativePositionForRelayout); + mAutofillStateFingerprint.storeStatePriorToAuthentication( + getClient(), allFillableIds); + } } } @@ -3845,7 +3986,7 @@ public final class AutofillManager { @GuardedBy("mLock") private boolean isPendingAuthenticationLocked() { - return mRelayoutFixDeprecated && mState == STATE_PENDING_AUTHENTICATION; + return (mRelayoutFixDeprecated || mRelayoutFix) && mState == STATE_PENDING_AUTHENTICATION; } @GuardedBy("mLock") @@ -3858,7 +3999,7 @@ public final class AutofillManager { return mState == STATE_FINISHED; } - private void post(Runnable runnable) { + void post(Runnable runnable) { final AutofillClient client = getClient(); if (client == null) { if (sVerbose) Log.v(TAG, "ignoring post() because client is null"); @@ -4700,11 +4841,11 @@ public final class AutofillManager { @Override public void setTrackedViews(int sessionId, AutofillId[] ids, boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds, - AutofillId saveTriggerId) { + AutofillId saveTriggerId, boolean shouldGrabViewFingerprints) { final AutofillManager afm = mAfm.get(); if (afm != null) { afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, - saveOnFinish, fillableIds, saveTriggerId)); + saveOnFinish, fillableIds, saveTriggerId, shouldGrabViewFingerprints)); } } diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java new file mode 100644 index 000000000000..2db4285f0820 --- /dev/null +++ b/core/java/android/view/autofill/AutofillStateFingerprint.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Slog; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class manages and stores the autofillable views fingerprints for use in relayout situations. + * @hide + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public final class AutofillStateFingerprint { + + ArrayList<AutofillId> mPriorAutofillIds; + ArrayList<Integer> mViewHashCodes; // each entry corresponding to mPriorAutofillIds . + + boolean mHideHighlight = false; + + private int mSessionId; + + Map<Integer, AutofillId> mHashToAutofillIdMap = new ArrayMap<>(); + Map<AutofillId, AutofillId> mOldIdsToCurrentAutofillIdMap = new ArrayMap<>(); + + // These failed id's are attempted to be refilled again after relayout. + private ArrayList<AutofillId> mFailedIds = new ArrayList<>(); + private ArrayList<AutofillValue> mFailedAutofillValues = new ArrayList<>(); + + // whether to use relative positions for computing hashes. + private boolean mUseRelativePosition; + + private static final String TAG = "AutofillStateFingerprint"; + + /** + * Returns an instance of this class + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public static AutofillStateFingerprint createInstance() { + return new AutofillStateFingerprint(); + } + + private AutofillStateFingerprint() { + } + + /** + * Set sessionId for the instance + */ + void setSessionId(int sessionId) { + mSessionId = sessionId; + } + + /** + * Sets whether relative position of the views should be used to calculate fingerprints. + */ + void setUseRelativePosition(boolean useRelativePosition) { + mUseRelativePosition = useRelativePosition; + } + + /** + * Store the state of the views prior to the authentication. + */ + void storeStatePriorToAuthentication( + AutofillManager.AutofillClient client, Set<AutofillId> autofillIds) { + if (mUseRelativePosition) { + List<View> autofillableViews = client.autofillClientFindAutofillableViewsByTraversal(); + if (sDebug) { + Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size()); + } +// ArrayList<Integer> hashes = getFingerprintIds(autofillableViews); + + ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews); + for (Map.Entry<Integer, View> entry : hashes.entrySet()) { + View view = entry.getValue(); + if (view != null) { + mHashToAutofillIdMap.put(entry.getKey(), view.getAutofillId()); + } else { + if (sDebug) { + Log.d(TAG, "Encountered null view"); + } + } + } + } else { + // Just use the provided autofillIds and get their hashes + if (sDebug) { + Log.d(TAG, "Size of autofillId's being stored: " + autofillIds.size() + + " list:" + autofillIds); + } + AutofillId[] autofillIdsArr = Helper.toArray(autofillIds); + View[] views = client.autofillClientFindViewsByAutofillIdTraversal(autofillIdsArr); + for (int i = 0; i < autofillIdsArr.length; i++) { + View view = views[i]; + if (view != null) { + int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */); + AutofillId autofillId = view.getAutofillId(); + autofillId.setSessionId(mSessionId); + mHashToAutofillIdMap.put(id, autofillId); + } else { + if (sDebug) { + Log.d(TAG, "Encountered null view"); + } + } + } + } + } + + /** + * Store failed ids, so that they can be refilled later + */ + void storeFailedIdsAndValues( + @NonNull ArrayList<AutofillId> failedIds, + ArrayList<AutofillValue> failedAutofillValues, + boolean hideHighlight) { + for (AutofillId failedId : failedIds) { + if (failedId != null) { + failedId.setSessionId(mSessionId); + } else { + if (sDebug) { + Log.d(TAG, "Got null failed ids"); + } + } + } + mFailedIds = failedIds; + mFailedAutofillValues = failedAutofillValues; + mHideHighlight = hideHighlight; + } + + private void dumpCurrentState() { + Log.d(TAG, "FailedId's: " + mFailedIds); + Log.d(TAG, "Hashes from map" + mHashToAutofillIdMap); + } + + boolean attemptRefill( + List<View> currentAutofillableViews, @NonNull AutofillManager autofillManager) { + if (sDebug) { + dumpCurrentState(); + } + // For the autofillable views, compute their hashes + ArrayMap<Integer, View> currentHashes = getFingerprintIds(currentAutofillableViews); + + // For the computed hashes, try to look for the old fingerprints. + // If match found, update the new autofill ids of those views + Map<AutofillId, View> oldFailedIdsToCurrentViewMap = new HashMap<>(); + for (Map.Entry<Integer, View> entry : currentHashes.entrySet()) { + View view = entry.getValue(); + int currentHash = entry.getKey(); + AutofillId currentAutofillId = view.getAutofillId(); + currentAutofillId.setSessionId(mSessionId); + if (mHashToAutofillIdMap.containsKey(currentHash)) { + AutofillId oldAutofillId = mHashToAutofillIdMap.get(currentHash); + oldAutofillId.setSessionId(mSessionId); + mOldIdsToCurrentAutofillIdMap.put(oldAutofillId, currentAutofillId); + Log.i(TAG, "Mapping current autofill id: " + view.getAutofillId() + + " to existing autofill id " + oldAutofillId); + + oldFailedIdsToCurrentViewMap.put(oldAutofillId, view); + } else { + Log.i(TAG, "Couldn't map current autofill id: " + view.getAutofillId() + + " with currentHash:" + currentHash + " for view:" + view); + } + } + + int viewsCount = 0; + View[] views = new View[mFailedIds.size()]; + for (int i = 0; i < mFailedIds.size(); i++) { + AutofillId oldAutofillId = mFailedIds.get(i); + AutofillId currentAutofillId = mOldIdsToCurrentAutofillIdMap.get(oldAutofillId); + if (currentAutofillId == null) { + if (sDebug) { + Log.d(TAG, "currentAutofillId = null"); + } + } + mFailedIds.set(i, currentAutofillId); + views[i] = oldFailedIdsToCurrentViewMap.get(oldAutofillId); + if (views[i] != null) { + viewsCount++; + } + } + + if (sDebug) { + dumpCurrentState(); + } + + // Attempt autofill now + Slog.i(TAG, "Attempting refill of views. Found " + viewsCount + + " views to refill from previously " + mFailedIds.size() + + " failed ids:" + mFailedIds); + autofillManager.post( + () -> autofillManager.autofill( + views, mFailedIds, mFailedAutofillValues, mHideHighlight, + true /* isRefill */)); + + return false; + } + + /** + * Retrieves fingerprint hashes for the views + */ + ArrayMap<Integer, View> getFingerprintIds(@NonNull List<View> views) { + ArrayMap<Integer, View> map = new ArrayMap<>(); + if (mUseRelativePosition) { + Collections.sort(views, (View v1, View v2) -> { + int[] posV1 = v1.getLocationOnScreen(); + int[] posV2 = v2.getLocationOnScreen(); + + int compare = posV1[0] - posV2[0]; // x coordinate + if (compare != 0) { + return compare; + } + compare = posV1[1] - posV2[1]; // y coordinate + if (compare != 0) { + return compare; + } + // Sort on vertical + compare = compareTop(v1, v2); + if (compare != 0) { + return compare; + } + compare = compareBottom(v1, v2); + if (compare != 0) { + return compare; + } + compare = compareLeft(v1, v2); + if (compare != 0) { + return compare; + } + return compareRight(v1, v2); + // Note that if compareRight also returned 0, that means both the views have exact + // same location, so just treat them as equal + }); + } + for (int i = 0; i < views.size(); i++) { + View view = views.get(i); + map.put(getEphemeralFingerprintId(view, i), view); + } + return map; + } + + /** + * Returns fingerprint hash for the view. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public int getEphemeralFingerprintId(View v, int position) { + if (v == null) return -1; + int inputType = Integer.MIN_VALUE; + int imeOptions = Integer.MIN_VALUE; + boolean isSingleLine = false; + CharSequence hints = ""; + if (v instanceof TextView) { + TextView tv = (TextView) v; + inputType = tv.getInputType(); + hints = tv.getHint(); + isSingleLine = tv.isSingleLine(); + imeOptions = tv.getImeOptions(); + // TODO(b/238252288): Consider adding more IME related fields. + } + CharSequence contentDesc = v.getContentDescription(); + CharSequence tooltip = v.getTooltipText(); + + int autofillType = v.getAutofillType(); + String[] autofillHints = v.getAutofillHints(); + int visibility = v.getVisibility(); + + int paddingLeft = v.getPaddingLeft(); + int paddingRight = v.getPaddingRight(); + int paddingTop = v.getPaddingTop(); + int paddingBottom = v.getPaddingBottom(); + + // TODO(b/238252288): Following are making relayout flaky. Do more analysis to figure out + // why. + int height = v.getHeight(); + int width = v.getWidth(); + + // Order doesn't matter much here. We can change the order, as long as we use the same + // order for storing and fetching fingerprints. The order can be changed in platform + // versions. + int hash = Objects.hash(visibility, inputType, imeOptions, isSingleLine, hints, + contentDesc, tooltip, autofillType, Arrays.deepHashCode(autofillHints), + paddingBottom, paddingTop, paddingRight, paddingLeft); + if (mUseRelativePosition) { + hash = Objects.hash(hash, position); + } + if (sDebug) { + Log.d(TAG, "Hash: " + hash + " for AutofillId:" + v.getAutofillId() + + " visibility:" + visibility + + " inputType:" + inputType + + " imeOptions:" + imeOptions + + " isSingleLine:" + isSingleLine + + " hints:" + hints + + " contentDesc:" + contentDesc + + " tooltipText:" + tooltip + + " autofillType:" + autofillType + + " autofillHints:" + Arrays.toString(autofillHints) + + " height:" + height + + " width:" + width + + " paddingLeft:" + paddingLeft + + " paddingRight:" + paddingRight + + " paddingTop:" + paddingTop + + " paddingBottom:" + paddingBottom + + " mUseRelativePosition" + mUseRelativePosition + + " position:" + position + ); + } + return hash; + } + + private int compareTop(View v1, View v2) { + return v1.getTop() - v2.getTop(); + } + + private int compareBottom(View v1, View v2) { + return v1.getBottom() - v2.getBottom(); + } + + private int compareLeft(View v1, View v2) { + return v1.getLeft() - v2.getLeft(); + } + + private int compareRight(View v1, View v2) { + return v1.getRight() - v2.getRight(); + } +} diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl index 2039b4d5c1bd..f67405f7c1e4 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -48,7 +48,7 @@ oneway interface IAutoFillManager { in IResultReceiver result); void updateSession(int sessionId, in AutofillId id, in Rect bounds, in AutofillValue value, int action, int flags, int userId); - void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId); + void setAutofillFailure(int sessionId, in List<AutofillId> ids, boolean isRefill, int userId); void setViewAutofilled(int sessionId, in AutofillId id, int userId); void finishSession(int sessionId, int userId, int commitReason); void cancelSession(int sessionId, int userId); @@ -67,4 +67,7 @@ oneway interface IAutoFillManager { void getDefaultFieldClassificationAlgorithm(in IResultReceiver result); void setAugmentedAutofillWhitelist(in List<String> packages, in List<ComponentName> activities, in IResultReceiver result); + void notifyNotExpiringResponseDuringAuth(int sessionId, int userId); + void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId); + void setAutofillIdsAttemptedForRefill(int sessionId, in List<AutofillId> ids, int userId); } diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 904a7e0d6173..39d71da1318c 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -72,7 +72,8 @@ oneway interface IAutoFillManagerClient { */ void setTrackedViews(int sessionId, in @nullable AutofillId[] savableIds, boolean saveOnAllViewsInvisible, boolean saveOnFinish, - in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId); + in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId, + in boolean shouldGrabViewFingerprints); /** * Requests showing the fill UI. diff --git a/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java new file mode 100644 index 000000000000..7cbfc40a62f1 --- /dev/null +++ b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.autofill; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import android.content.Context; +import android.text.InputType; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AutofillStateFingerprintTest { + + private static final Context sContext = ApplicationProvider.getApplicationContext(); + + private static final int MAGIC_AUTOFILL_NUMBER = 1000; + + private AutofillStateFingerprint mAutofillStateFingerprint = + AutofillStateFingerprint.createInstance(); + + @Test + public void testSameFingerprintsForTextView() throws Exception { + TextView tv = new TextView(sContext); + tv.setHint("Password"); + tv.setSingleLine(true); + tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT); + fillViewProperties(tv); + + // Create a copy Text View, and compare both id's + View tvCopy = copySelectiveViewAttributes(tv); + assertIdsEqual(tv, tvCopy); + } + + @Test + public void testDifferentFingerprintsForTextViewWithDifferentHint() throws Exception { + TextView tv = new TextView(sContext); + tv.setHint("Password"); + tv.setSingleLine(true); + tv.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); + tv.setImeOptions(EditorInfo.IME_FLAG_NAVIGATE_NEXT); + fillViewProperties(tv); + + TextView tvCopy = (TextView) copySelectiveViewAttributes(tv); + tvCopy.setHint("what a useless different hint"); + assertIdsNotEqual(tv, tvCopy); + } + + @Test + public void testSameFingerprintsForNonTextView() throws Exception { + View v = new View(sContext); + fillViewProperties(v); + + // Create a copy Text View, and compare both id's + View copy = copySelectiveViewAttributes(v); + assertIdsEqual(v, copy); + } + + @Test + public void testDifferentFingerprintsForNonTextViewWithDifferentVisibility() throws Exception { + View v = new View(sContext); + fillViewProperties(v); + + View copy = copySelectiveViewAttributes(v); + copy.setVisibility(View.GONE); + assertIdsNotEqual(v, copy); + } + + private void assertIdsEqual(View v1, View v2) { + assertEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0), + mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0)); + } + + private void assertIdsNotEqual(View v1, View v2) { + assertNotEquals(mAutofillStateFingerprint.getEphemeralFingerprintId(v1, 0), + mAutofillStateFingerprint.getEphemeralFingerprintId(v2, 0)); + } + + private void fillViewProperties(View view) { + // Fill in relevant view properties + view.setContentDescription("ContentDesc"); + view.setTooltipText("TooltipText"); + view.setAutofillHints(new String[] {"password"}); + view.setVisibility(View.VISIBLE); + view.setLeft(20); + view.setRight(200); + view.setTop(20); + view.setBottom(200); + view.setPadding(0, 1, 2, 3); + } + + // Only copy interesting view attributes, particularly the view attributes that are critical + // for calculating fingerprint. Keep Autofill Id different. + private View copySelectiveViewAttributes(View view) { + View copy; + if (view instanceof TextView) { + copy = new TextView(sContext); + copySelectiveTextViewAttributes((TextView) view, (TextView) copy); + } else { + copy = new View(sContext) { + public @AutofillType int getAutofillType() { + return view.getAutofillType(); + } + }; + } + // Copy over interested view properties. + // Keep the order same as with the tested code for easier clarity. + copy.setVisibility(view.getVisibility()); + copy.setAutofillHints(view.getAutofillHints()); + copy.setContentDescription(view.getContentDescription()); + copy.setTooltip(view.getTooltipText()); + + copy.setRight(view.getRight()); + copy.setLeft(view.getLeft()); + copy.setTop(view.getTop()); + copy.setBottom(view.getBottom()); + copy.setPadding(view.getPaddingLeft(), view.getPaddingTop(), + view.getPaddingRight(), view.getPaddingBottom()); + + // DO not copy over autofill id + AutofillId newId = new AutofillId(view.getAutofillId().getViewId() + MAGIC_AUTOFILL_NUMBER); + copy.setAutofillId(newId); + return copy; + } + + private void copySelectiveTextViewAttributes(TextView fromView, TextView toView) { + toView.setInputType(fromView.getInputType()); + toView.setHint(fromView.getHint()); + toView.setSingleLine(fromView.isSingleLine()); + toView.setImeOptions(fromView.getImeOptions()); + } +} diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index eae516e10d6e..9f7fb5710bcc 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -1985,12 +1985,13 @@ public final class AutofillManagerService } @Override - public void setAutofillFailure(int sessionId, @NonNull List<AutofillId> ids, int userId) { + public void setAutofillFailure( + int sessionId, @NonNull List<AutofillId> ids, boolean isRefill, int userId) { synchronized (mLock) { final AutofillManagerServiceImpl service = peekServiceForUserWithLocalBinderIdentityLocked(userId); if (service != null) { - service.setAutofillFailureLocked(sessionId, getCallingUid(), ids); + service.setAutofillFailureLocked(sessionId, getCallingUid(), ids, isRefill); } else if (sVerbose) { Slog.v(TAG, "setAutofillFailure(): no service for " + userId); } @@ -2011,6 +2012,46 @@ public final class AutofillManagerService } @Override + public void notifyNotExpiringResponseDuringAuth(int sessionId, int userId) { + synchronized (mLock) { + final AutofillManagerServiceImpl service = + peekServiceForUserWithLocalBinderIdentityLocked(userId); + if (service != null) { + service.notifyNotExpiringResponseDuringAuth(sessionId, getCallingUid()); + } else if (sVerbose) { + Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId); + } + } + } + + @Override + public void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int userId) { + synchronized (mLock) { + final AutofillManagerServiceImpl service = + peekServiceForUserWithLocalBinderIdentityLocked(userId); + if (service != null) { + service.notifyViewEnteredIgnoredDuringAuthCount(sessionId, getCallingUid()); + } else if (sVerbose) { + Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no service for " + userId); + } + } + } + + @Override + public void setAutofillIdsAttemptedForRefill( + int sessionId, @NonNull List<AutofillId> ids, int userId) { + synchronized (mLock) { + final AutofillManagerServiceImpl service = + peekServiceForUserWithLocalBinderIdentityLocked(userId); + if (service != null) { + service.setAutofillIdsAttemptedForRefill(sessionId, ids, getCallingUid()); + } else if (sVerbose) { + Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no service for " + userId); + } + } + } + + @Override public void finishSession(int sessionId, int userId, @AutofillCommitReason int commitReason) { synchronized (mLock) { diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 2bf319e06efa..c9f892907b59 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -464,7 +464,8 @@ final class AutofillManagerServiceImpl } @GuardedBy("mLock") - void setAutofillFailureLocked(int sessionId, int uid, @NonNull List<AutofillId> ids) { + void setAutofillFailureLocked( + int sessionId, int uid, @NonNull List<AutofillId> ids, boolean isRefill) { if (!isEnabledLocked()) { Slog.wtf(TAG, "Service not enabled"); return; @@ -474,7 +475,7 @@ final class AutofillManagerServiceImpl Slog.v(TAG, "setAutofillFailure(): no session for " + sessionId + "(" + uid + ")"); return; } - session.setAutofillFailureLocked(ids); + session.setAutofillFailureLocked(ids, isRefill); } @GuardedBy("mLock") @@ -492,6 +493,52 @@ final class AutofillManagerServiceImpl } @GuardedBy("mLock") + void notifyNotExpiringResponseDuringAuth(int sessionId, int uid) { + if (!isEnabledLocked()) { + Slog.wtf(TAG, "Service not enabled"); + return; + } + final Session session = mSessions.get(sessionId); + if (session == null || uid != session.uid) { + Slog.v(TAG, "notifyNotExpiringResponseDuringAuth(): no session for " + + sessionId + "(" + uid + ")"); + return; + } + session.setNotifyNotExpiringResponseDuringAuth(); + } + + @GuardedBy("mLock") + void notifyViewEnteredIgnoredDuringAuthCount(int sessionId, int uid) { + if (!isEnabledLocked()) { + Slog.wtf(TAG, "Service not enabled"); + return; + } + final Session session = mSessions.get(sessionId); + if (session == null || uid != session.uid) { + Slog.v(TAG, "notifyViewEnteredIgnoredDuringAuthCount(): no session for " + + sessionId + "(" + uid + ")"); + return; + } + session.setLogViewEnteredIgnoredDuringAuth(); + } + + @GuardedBy("mLock") + public void setAutofillIdsAttemptedForRefill( + int sessionId, @NonNull List<AutofillId> ids, int uid) { + if (!isEnabledLocked()) { + Slog.wtf(TAG, "Service not enabled"); + return; + } + final Session session = mSessions.get(sessionId); + if (session == null || uid != session.uid) { + Slog.v(TAG, "setAutofillIdsAttemptedForRefill(): no session for " + + sessionId + "(" + uid + ")"); + return; + } + session.setAutofillIdsAttemptedForRefillLocked(ids); + } + + @GuardedBy("mLock") void finishSessionLocked(int sessionId, int uid, @AutofillCommitReason int commitReason) { if (!isEnabledLocked()) { Slog.wtf(TAG, "Service not enabled"); diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java index 49ca29745b11..930af5e7f056 100644 --- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java @@ -58,6 +58,7 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_ import static com.android.server.autofill.Helper.sVerbose; import android.annotation.IntDef; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; @@ -685,6 +686,19 @@ public final class PresentationStatsEventLogger { } /** + * Set views_fillable_total_count as long as mEventInternal presents. + */ + public void maybeUpdateViewFillablesForRefillAttempt(List<AutofillId> autofillIds) { + mEventInternal.ifPresent(event -> { + // These autofill ids would be the ones being re-attempted. + event.mAutofillIdsAttemptedAutofill = new ArraySet<>(autofillIds); + // These autofill id's are being refilled, so they had failed previously. + // Note that these autofillIds correspond to the new autofill ids after relayout. + event.mFailedAutofillIds = new ArraySet<>(autofillIds); + }); + } + + /** * Set how many views are filtered from fill because they are not in current session */ public void maybeSetFilteredFillableViewsCount(int filteredViewsCount) { @@ -697,9 +711,16 @@ public final class PresentationStatsEventLogger { * Set views_filled_failure_count using failure count as long as mEventInternal * presents. */ - public void maybeSetViewFillFailureCounts(int failureCount) { + public void maybeSetViewFillFailureCounts(@NonNull List<AutofillId> ids, boolean isRefill) { mEventInternal.ifPresent(event -> { - event.mViewFillFailureCount = failureCount; + int failureCount = ids.size(); + if (isRefill) { + event.mViewFailedOnRefillCount = failureCount; + } else { + event.mViewFillFailureCount = failureCount; + event.mViewFailedPriorToRefillCount = failureCount; + event.mFailedAutofillIds = new ArraySet<>(ids); + } }); } @@ -719,7 +740,7 @@ public final class PresentationStatsEventLogger { * Set views_filled_failure_count using failure count as long as mEventInternal * presents. */ - public void maybeAddSuccessId(AutofillId autofillId) { + public synchronized void maybeAddSuccessId(AutofillId autofillId) { mEventInternal.ifPresent(event -> { ArraySet<AutofillId> autofillIds = event.mAutofillIdsAttemptedAutofill; if (autofillIds == null) { @@ -727,9 +748,21 @@ public final class PresentationStatsEventLogger { + " successfully filled"); event.mViewFilledButUnexpectedCount++; } else if (autofillIds.contains(autofillId)) { - if (sVerbose) { - Slog.v(TAG, "Logging autofill for id:" + autofillId); + ArraySet<AutofillId> failedIds = event.mFailedAutofillIds; + if (failedIds.contains(autofillId)) { + if (sVerbose) { + Slog.v(TAG, "Logging autofill refill of id:" + autofillId); + } + // This indicates the success after refill attempt + event.mViewFilledSuccessfullyOnRefillCount++; + // Remove so if we don't reprocess duplicate requests + failedIds.remove(autofillId); + } else { + if (sVerbose) { + Slog.v(TAG, "Logging autofill for id:" + autofillId); + } } + // Common actions to take irrespective of being filled by refill attempt or not. event.mViewFillSuccessCount++; autofillIds.remove(autofillId); event.mAlreadyFilledAutofillIds.add(autofillId); @@ -746,6 +779,23 @@ public final class PresentationStatsEventLogger { }); } + /** + * Set how many views are filtered from fill because they are not in current session + */ + public void maybeSetNotifyNotExpiringResponseDuringAuth() { + mEventInternal.ifPresent(event -> { + event.mFixExpireResponseDuringAuthCount++; + }); + } + /** + * Set how many views are filtered from fill because they are not in current session + */ + public void notifyViewEnteredIgnoredDuringAuthCount() { + mEventInternal.ifPresent(event -> { + event.mNotifyViewEnteredIgnoredDuringAuthCount++; + }); + } + public void logAndEndEvent() { if (!mEventInternal.isPresent()) { Slog.w(TAG, "Shouldn't be logging AutofillPresentationEventReported again for same " @@ -933,6 +983,7 @@ public final class PresentationStatsEventLogger { int mNotifyViewEnteredIgnoredDuringAuthCount = 0; ArraySet<AutofillId> mAutofillIdsAttemptedAutofill; + ArraySet<AutofillId> mFailedAutofillIds = new ArraySet<>(); ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>(); // Not logged - used for internal logic diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 21df7a5487ef..b7508b4f07b7 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -5360,6 +5360,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState saveTriggerId = null; } + boolean hasAuthentication = (response.getAuthentication() != null); + // Must also track that are part of datasets, otherwise the FillUI won't be hidden when // they go away (if they're not savable). @@ -5379,6 +5381,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } } + if (dataset.getAuthentication() != null) { + hasAuthentication = true; + } } } @@ -5390,7 +5395,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + " hasSaveInfo: " + (saveInfo != null)); } mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible, - saveOnFinish, toArray(fillableIds), saveTriggerId); + saveOnFinish, toArray(fillableIds), saveTriggerId, hasAuthentication); } catch (RemoteException e) { Slog.w(TAG, "Cannot set tracked ids", e); } @@ -5400,7 +5405,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * Sets the state of views that failed to autofill. */ @GuardedBy("mLock") - void setAutofillFailureLocked(@NonNull List<AutofillId> ids) { + void setAutofillFailureLocked(@NonNull List<AutofillId> ids, boolean isRefill) { if (sVerbose && !ids.isEmpty()) { Slog.v(TAG, "Total views that failed to populate: " + ids.size()); } @@ -5418,7 +5423,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString()); } } - mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids.size()); + mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids, isRefill); } /** @@ -5435,6 +5440,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mPresentationStatsEventLogger.maybeAddSuccessId(id); } + /** + * Sets the state of views that failed to autofill. + */ + void setNotifyNotExpiringResponseDuringAuth() { + synchronized (mLock) { + mPresentationStatsEventLogger.maybeSetNotifyNotExpiringResponseDuringAuth(); + } + } + /** + * Sets the state of views that failed to autofill. + */ + void setLogViewEnteredIgnoredDuringAuth() { + synchronized (mLock) { + mPresentationStatsEventLogger.notifyViewEnteredIgnoredDuringAuthCount(); + } + } + @GuardedBy("mLock") private void replaceResponseLocked(@NonNull FillResponse oldResponse, @NonNull FillResponse newResponse, @Nullable Bundle newClientState) { @@ -6665,6 +6687,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + @GuardedBy("mLock") + public void setAutofillIdsAttemptedForRefillLocked(@NonNull List<AutofillId> ids) { + mPresentationStatsEventLogger.maybeUpdateViewFillablesForRefillAttempt(ids); + } + private AutoFillUI getUiForShowing() { synchronized (mLock) { mUi.setCallback(this); |