diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index e27fa06a9df75..b7a04206a4c6b 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -77,11 +77,6 @@ public final class Dataset implements Parcelable { return customPresentation != null ? customPresentation : mPresentation; } - /** @hide */ - public @Nullable RemoteViews getPresentation() { - return mPresentation; - } - /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 3d1c2511db275..dbf1e83f8eacd 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -290,7 +290,8 @@ final class AutofillManagerServiceImpl { + " f=" + flags; mRequestsHistory.log(historyItem); - // TODO(b/33197203): Handle partitioning + // TODO(b/33197203): Handle scenario when user forced autofill after app was already + // autofilled. final Session session = mSessions.get(activityToken); if (session != null) { // Already started... diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index ac7d19eaba46d..801769cc42e12 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -48,6 +48,7 @@ import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; import android.util.ArrayMap; +import android.util.DebugUtils; import android.util.Slog; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -112,20 +113,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") RemoteFillService mRemoteFillService; - // TODO(b/33197203 , b/35707731): Use List once it supports partitioning @GuardedBy("mLock") - private FillResponse mCurrentResponse; + private ArrayList mResponses; /** - * Used to remember which {@link Dataset} filled the session. + * Response that requires a service authentitcation request. */ - // TODO(b/33197203 , b/35707731): will be removed once it supports partitioning @GuardedBy("mLock") - private Dataset mAutoFilledDataset; + private FillResponse mResponseWaitingAuth; /** * Dataset that when tapped launched a service authentication request. */ + @GuardedBy("mLock") private Dataset mDatasetWaitingAuth; /** @@ -163,8 +163,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mClient = IAutoFillManagerClient.Stub.asInterface(client); try { client.linkToDeath(() -> { - if (DEBUG) { - Slog.d(TAG, "app binder died"); + if (VERBOSE) { + Slog.v(TAG, "app binder died"); } removeSelf(); @@ -193,6 +193,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState notifyUnavailableToClient(); } synchronized (mLock) { + if (response.getAuthentication() != null) { + // TODO(b/33197203 , b/35707731): make sure it's ignored if there is one already + mResponseWaitingAuth = response; + } processResponseLocked(response); } @@ -318,23 +322,27 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } public void setAuthenticationResultLocked(Bundle data) { - if (mCurrentResponse == null || data == null) { + if ((mResponseWaitingAuth == null && mDatasetWaitingAuth == null) || data == null) { removeSelf(); } else { final Parcelable result = data.getParcelable( AutofillManager.EXTRA_AUTHENTICATION_RESULT); if (result instanceof FillResponse) { mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName); - - mCurrentResponse = (FillResponse) result; - processResponseLocked(mCurrentResponse); + mResponseWaitingAuth = null; + processResponseLocked((FillResponse) result); } else if (result instanceof Dataset) { final Dataset dataset = (Dataset) result; - final int index = mCurrentResponse.getDatasets().indexOf(mDatasetWaitingAuth); - if (index >= 0) { - mCurrentResponse.getDatasets().set(index, dataset); - autoFill(dataset); - mDatasetWaitingAuth = null; + for (int i = 0; i < mResponses.size(); i++) { + final FillResponse response = mResponses.get(i); + final int index = response.getDatasets().indexOf(mDatasetWaitingAuth); + if (index >= 0) { + response.getDatasets().set(index, dataset); + mDatasetWaitingAuth = null; + autoFill(dataset); + resetViewStatesLocked(dataset, ViewState.STATE_WAITING_DATASET_AUTH); + return; + } } } } @@ -354,15 +362,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.wtf(TAG, "showSaveLocked(): no mStructure"); return true; } - if (mCurrentResponse == null) { + if (mResponses == null) { // Happens when the activity / session was finished before the service replied, or // when the service cannot autofill it (and returned a null response). if (DEBUG) { - Slog.d(TAG, "showSaveLocked(): no mCurrentResponse"); + Slog.d(TAG, "showSaveLocked(): no responses on session"); } return true; } - final SaveInfo saveInfo = mCurrentResponse.getSaveInfo(); + + // TODO(b/33197203 , b/35707731): must iterate over all responses + final FillResponse response = mResponses.get(0); + + final SaveInfo saveInfo = response.getSaveInfo(); if (DEBUG) { Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo); } @@ -385,7 +397,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return true; } - // TODO(b/33197203 , b/35707731): refactor excessive calls to getCurrentValue() boolean allRequiredAreNotEmpty = true; boolean atLeastOneChanged = false; for (int i = 0; i < requiredIds.length; i++) { @@ -393,7 +404,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final ViewState viewState = mViewStates.get(id); if (viewState == null) { Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id); - continue; + allRequiredAreNotEmpty = false; + break; } final AutofillValue currentValue = viewState.getCurrentValue(); @@ -462,7 +474,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates); } - final Bundle extras = this.mCurrentResponse.getExtras(); + // TODO(b/33197203 , b/35707731): decide how to handle bundle in multiple partitions + final Bundle extras = mResponses != null ? mResponses.get(0).getExtras() : null; for (Entry entry : mViewStates.entrySet()) { final AutofillValue value = entry.getValue().getCurrentValue(); @@ -497,16 +510,21 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) { - if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) { - // TODO(b/33197203): ignoring because we don't support partitions yet - Slog.d(TAG, "updateLocked(): ignoring " + id + " after app was autofilled"); - return; - } - ViewState viewState = mViewStates.get(id); + if (viewState == null) { - viewState = new ViewState(this, id, this, ViewState.STATE_INITIAL); - mViewStates.put(id, viewState); + if ((flags & (FLAG_START_SESSION | FLAG_VALUE_CHANGED)) != 0) { + if (DEBUG) { + Slog.d(TAG, "Creating viewState for " + id + " on " + getFlagAsString(flags)); + } + viewState = new ViewState(this, id, this, ViewState.STATE_INITIAL); + mViewStates.put(id, viewState); + } else if ((flags & FLAG_VIEW_ENTERED) != 0) { + viewState = startPartitionLocked(id); + } else { + if (VERBOSE) Slog.v(TAG, "Ignored " + getFlagAsString(flags) + " for " + id); + return; + } } if ((flags & FLAG_START_SESSION) != 0) { @@ -530,7 +548,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // Update the internal state... viewState.setState(ViewState.STATE_CHANGED); - // ... and the chooser UI. + + //..and the UI if (value.isText()) { getUiForShowing().filterFillUi(value.getTextValue().toString()); } else { @@ -551,10 +570,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // If the ViewState is ready to be displayed, onReady() will be called. viewState.update(value, virtualBounds); - if (mCurrentResponse != null) { - viewState.setResponse(mCurrentResponse); - } - return; } @@ -566,7 +581,28 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - Slog.w(TAG, "updateLocked(): unknown flags " + flags); + Slog.w(TAG, "updateLocked(): unknown flags " + flags + ": " + getFlagAsString(flags)); + } + + private ViewState startPartitionLocked(AutofillId id) { + if (DEBUG) { + Slog.d(TAG, "Starting partition for view id " + id); + } + final ViewState viewState = + new ViewState(this, id, this,ViewState.STATE_STARTED_PARTITION); + mViewStates.put(id, viewState); + + /* + * TODO(b/33197203 , b/35707731): when start a new partition, it should + * + * - add autofilled fields as sanitized + * - set focus on ViewStructure that triggered it + * - pass the first onFillRequest() bundle + * - optional: perhaps add a new flag onFilLRequest() to indicate it's a new partition? + */ + mRemoteFillService.onFillRequest(mStructure, null, 0); + + return viewState; } @Override @@ -580,6 +616,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState getUiForShowing().showFillUi(filledId, response, filterText, mPackageName); } + String getFlagAsString(int flag) { + return DebugUtils.flagsToString(AutofillManager.class, "FLAG_", flag); + } + private void notifyUnavailableToClient() { if (mCurrentViewId == null) { // TODO(b/33197203): temporary sanity check; should never happen @@ -597,8 +637,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private void processResponseLocked(FillResponse response) { if (DEBUG) { - Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication() - + "):" + response); + Slog.d(TAG, "processResponseLocked(mCurrentViewId=" + mCurrentViewId + "):" + response); } if (mCurrentViewId == null) { @@ -607,7 +646,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - mCurrentResponse = response; + if (mResponses == null) { + mResponses = new ArrayList<>(4); + } + mResponses.add(response); setViewStatesLocked(response, ViewState.STATE_FILLABLE); @@ -669,10 +711,22 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * Resets the given state from all existing views in the given dataset. + */ + private void resetViewStatesLocked(@NonNull Dataset dataset, int state) { + final ArrayList ids = dataset.getFieldIds(); + for (int j = 0; j < ids.size(); j++) { + final AutofillId id = ids.get(j); + final ViewState viewState = mViewStates.get(id); + if (viewState != null) { + viewState.resetState(state); + } + } + } + void autoFill(Dataset dataset) { synchronized (mLock) { - mAutoFilledDataset = dataset; - // Autofill it directly... if (dataset.getAuthentication() == null) { autoFillApp(dataset); @@ -680,7 +734,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // ...or handle authentication. + // TODO(b/33197203 , b/35707731): make sure it's ignored if there is one already mDatasetWaitingAuth = dataset; + setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH); final Intent fillInIntent = createAuthFillInIntent(mStructure, null); startAuthentication(dataset.getAuthentication(), fillInIntent); } @@ -690,8 +746,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return mService.getServiceName(); } - FillResponse getCurrentResponse() { - return mCurrentResponse; + FillResponse getResponseWaitingAuth() { + return mResponseWaitingAuth; } private Intent createAuthFillInIntent(AssistStructure structure, Bundle extras) { @@ -714,8 +770,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState void dumpLocked(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken); pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags); - pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse); - pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset); + pw.print(prefix); pw.print("mResponses: "); pw.println(mResponses); + pw.print(prefix); pw.print("mResponseWaitingAuth: "); pw.println(mResponseWaitingAuth); pw.print(prefix); pw.print("mDatasetWaitingAuth: "); pw.println(mDatasetWaitingAuth); pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId); pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size()); @@ -811,4 +867,4 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState destroyLocked(); mService.removeSessionLocked(mActivityToken); } -} \ No newline at end of file +} diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java index 20def0c5a4f8e..549f231367c61 100644 --- a/services/autofill/java/com/android/server/autofill/ViewState.java +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -16,10 +16,13 @@ package com.android.server.autofill; +import static com.android.server.autofill.Helper.DEBUG; + import android.annotation.Nullable; import android.graphics.Rect; import android.service.autofill.FillResponse; import android.util.DebugUtils; +import android.util.Slog; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; @@ -40,6 +43,8 @@ final class ViewState { @Nullable AutofillValue value); } + private static final String TAG = "ViewState"; + // NOTE: state constants must be public because of flagstoString(). public static final int STATE_UNKNOWN = 0x00; /** Initial state. */ @@ -52,6 +57,10 @@ final class ViewState { public static final int STATE_CHANGED = 0x08; /** Set only in the View that started a session. */ public static final int STATE_STARTED_SESSION = 0x10; + /** View that started a new partition when focused on. */ + public static final int STATE_STARTED_PARTITION = 0x20; + /** User select a dataset in this view, but service must authenticate first. */ + public static final int STATE_WAITING_DATASET_AUTH = 0x40; public final AutofillId id; private final Listener mListener; @@ -122,9 +131,15 @@ final class ViewState { } void setState(int state) { - // TODO(b/33197203 , b/35707731): currently it's always setting one state, but once it - // supports partitioning it will need to 'or' some of them.. - mState = state; + if (mState == STATE_INITIAL) { + mState = state; + } else { + mState |= state; + } + } + + void resetState(int state) { + mState &= ~state; } // TODO(b/33197203): need to refactor / rename / document this method to make it clear that @@ -147,6 +162,12 @@ final class ViewState { * fill UI is ready to be displayed (i.e. when response and bounds are set). */ void maybeCallOnFillReady() { + if ((mState & (STATE_AUTOFILLED | STATE_WAITING_DATASET_AUTH)) != 0) { + if (DEBUG) { + Slog.d(TAG, "Ignoring UI for " + id + " on " + getStateAsString()); + } + return; + } // First try the current response associated with this View. if (mResponse != null) { if (mResponse.getDatasets() != null) { @@ -155,9 +176,9 @@ final class ViewState { return; } // Then checks if the session has a response waiting authentication; if so, uses it instead. - final FillResponse currentResponse = mSession.getCurrentResponse(); - if (currentResponse != null && currentResponse.getAuthentication() != null) { - mListener.onFillReady(currentResponse, this.id, mCurrentValue); + final FillResponse responseWaitingAuth = mSession.getResponseWaitingAuth(); + if (responseWaitingAuth != null) { + mListener.onFillReady(responseWaitingAuth, this.id, mCurrentValue); } }