diff options
| author | 2024-07-09 21:23:46 -0700 | |
|---|---|---|
| committer | 2024-07-26 06:29:00 -0700 | |
| commit | 9e09bca5bf8508dc89d6db2f49174462e7519304 (patch) | |
| tree | 9e948dc41e942853daef725e7f956135d4bb5db9 | |
| parent | 8b4f51dea2173c4c92c5171c64a1b1dd4109ccd7 (diff) | |
[Relayout] Part 4: Implement core logic
Re-attempt when autofill fails
Bug: 238252288
Flag: EXEMPT : DeviceConfig flags used: enable_relayout
exception granted in b/318391032
Test: atest CtsAutoFillServiceTestCases
atest FrameworksCoreTests:AutofillStateFingerprint
Change-Id: I302584adbd6da338019663203941b5bc2173b7e7
6 files changed, 581 insertions, 25 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..3d310c39cc7a 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,12 @@ public final class AutofillManager { mOnInvisibleCalled = true; if (isExpiredResponse) { + if (mRelayoutFix && isAuthenticationPending()) { + Log.i(TAG, "onInvisibleForAutofill(): Ignoring expiringResponse due to pending" + + " authentication"); + return; + } + Log.i(TAG, "onInvisibleForAutofill(): expiringResponse"); // Notify service the response has expired. updateSessionLocked(/* id= */ null, /* bounds= */ null, /* value= */ null, ACTION_RESPONSE_EXPIRED, /* flags= */ 0); @@ -1519,8 +1539,9 @@ public final class AutofillManager { * @hide */ public boolean shouldRetryFill() { - // TODO: Implement in follow-up cl - return false; + synchronized (mLock) { + return isAuthenticationPending() && mFillReAttemptNeeded; + } } /** @@ -1531,8 +1552,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 +2519,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 +2533,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 +2689,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 +2751,9 @@ public final class AutofillManager { if (resetEnteredIds) { mEnteredIds = null; } + mFillReAttemptNeeded = false; + mFingerprintToViewMap.clear(); + mAutofillStateFingerprint = AutofillStateFingerprint.createInstance(); } @GuardedBy("mLock") @@ -2984,8 +3016,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,9 +3227,28 @@ 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()); } catch (RemoteException e) { @@ -3202,6 +3257,25 @@ public final class AutofillManager { // 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(); + } + } + } } private void autofill(int sessionId, List<AutofillId> ids, List<AutofillValue> values, @@ -3216,13 +3290,30 @@ 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; + } + 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 +3328,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 +3361,7 @@ public final class AutofillManager { } } - handleFailedIdsLocked(failedIds); + handleFailedIdsLocked(failedIds, failedAutofillValues, hideHighlight, isRefill); if (virtualValues != null) { for (int i = 0; i < virtualValues.size(); i++) { @@ -3349,20 +3442,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 +3467,7 @@ public final class AutofillManager { + ", fillableIds=" + Arrays.toString(fillableIds) + ", saveTrigerId=" + saveTriggerId + ", mFillableIds=" + mFillableIds + + ", shouldGrabViewFingerprints=" + shouldGrabViewFingerprints + ", mEnabled=" + mEnabled + ", mSessionId=" + mSessionId); } @@ -3405,7 +3501,6 @@ public final class AutofillManager { trackedIds = null; } - final ArraySet<AutofillId> allFillableIds = new ArraySet<>(); if (mFillableIds != null) { allFillableIds.addAll(mFillableIds); } @@ -3424,6 +3519,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 +3946,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 +3959,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 +4801,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..eb857e034ec6 --- /dev/null +++ b/core/java/android/view/autofill/AutofillStateFingerprint.java @@ -0,0 +1,292 @@ +/* + * 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.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"; + + 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); + for (int i = 0; i < hashes.size(); i++) { + View view = autofillableViews.get(i); + if (view != null) { + mHashToAutofillIdMap.put(hashes.get(i), 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); + 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 + ArrayList<Integer> 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 (int i = 0; i < currentAutofillableViews.size(); i++) { + View view = currentAutofillableViews.get(i); + int currentHash = currentHashes.get(i); + 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 + */ + ArrayList<Integer> getFingerprintIds(@NonNull List<View> views) { + ArrayList<Integer> ids = new ArrayList(views.size()); + for (int i = 0; i < views.size(); i++) { + ids.add(getEphemeralFingerprintId(views.get(i))); + } + return ids; + } + + /** + * Returns fingerprint hash for the view. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public static int getEphemeralFingerprintId(View v) { + 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 (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 + ); + } + return hash; + } +} 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..fb50e988c417 --- /dev/null +++ b/core/tests/coretests/src/android/view/autofill/AutofillStateFingerprintTest.java @@ -0,0 +1,152 @@ +/* + * 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; + + @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(AutofillStateFingerprint.getEphemeralFingerprintId(v1), + AutofillStateFingerprint.getEphemeralFingerprintId(v2)); + } + + private void assertIdsNotEqual(View v1, View v2) { + assertNotEquals(AutofillStateFingerprint.getEphemeralFingerprintId(v1), + AutofillStateFingerprint.getEphemeralFingerprintId(v2)); + } + + 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/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 21df7a5487ef..6000a3655416 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); } |