diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 9285027618334..3d35247280460 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -63,6 +63,7 @@ import com.android.internal.os.HandlerCaller; import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Map; import java.util.Map.Entry; @@ -101,23 +102,25 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private final Map mViewStates = new ArrayMap<>(); + /** + * Id of the View currently being displayed. + */ @GuardedBy("mLock") - @Nullable - private ViewState mCurrentViewState; + @Nullable private AutofillId mCurrentViewId; private final IAutoFillManagerClient mClient; @GuardedBy("mLock") RemoteFillService mRemoteFillService; - // TODO(b/33197203): Get a response per view instead of per activity. + // TODO(b/33197203 , b/35707731): Use List once it supports partitioning @GuardedBy("mLock") private FillResponse mCurrentResponse; /** * Used to remember which {@link Dataset} filled the session. */ - // TODO(b/33197203): might need more than one once we support partitions + // TODO(b/33197203 , b/35707731): might need more than one once it supports partitioning @GuardedBy("mLock") private Dataset mAutoFilledDataset; @@ -189,7 +192,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState processResponseLocked(response); } - LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) + final LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) .setType(MetricsEvent.TYPE_SUCCESS) .setPackageName(mPackageName) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, @@ -292,8 +295,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState public void requestShowFillUi(AutofillId id, int width, int height, IAutofillWindowPresenter presenter) { try { + final ViewState currentView = mViewStates.get(mCurrentViewId); mClient.requestShowFillUi(mWindowToken, id, width, height, - mCurrentViewState.mVirtualBounds, presenter); + currentView.getVirtualBounds(), presenter); } catch (RemoteException e) { Slog.e(TAG, "Error requesting to show fill UI", e); } @@ -376,13 +380,14 @@ 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++) { final AutofillId id = requiredIds[i]; final ViewState state = mViewStates.get(id); - if (state == null || state.mAutofillValue == null - || state.mAutofillValue.isEmpty()) { + if (state == null || state.getCurrentValue() == null + || state.getCurrentValue().isEmpty()) { final ViewNode node = findViewNodeByIdLocked(id); if (node == null) { Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id); @@ -398,20 +403,20 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState break; } } - if (state.mValueUpdated) { + if (state.isChanged()) { final AutofillValue filledValue = findValue(mAutoFilledDataset, id); - if (!state.mAutofillValue.equals(filledValue)) { + if (!state.getCurrentValue().equals(filledValue)) { if (DEBUG) { Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": " - + filledValue + " => " + state.mAutofillValue); + + filledValue + " => " + state.getCurrentValue()); } atLeastOneChanged = true; } } else { - if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) { + if (state.getCurrentValue() == null || state.getCurrentValue().isEmpty()) { if (DEBUG) { Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": " - + state.mAutofillValue); + + state.getCurrentValue()); } allRequiredAreNotEmpty = false; break; @@ -425,13 +430,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState for (int i = 0; i < saveInfo.getOptionalIds().length; i++) { final AutofillId id = saveInfo.getOptionalIds()[i]; final ViewState state = mViewStates.get(id); - if (state != null && state.mAutofillValue != null && state.mValueUpdated) { + if (state != null && state.getCurrentValue() != null && state.isChanged()) { final AutofillValue filledValue = findValue(mAutoFilledDataset, id); - if (!state.mAutofillValue.equals(filledValue)) { + if (!state.getCurrentValue().equals(filledValue)) { if (DEBUG) { Slog.d(TAG, "finishSessionLocked(): found a change on optional " + id + ": " + filledValue + " => " - + state.mAutofillValue); + + state.getCurrentValue()); } atLeastOneChanged = true; break; @@ -464,7 +469,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final Bundle extras = this.mCurrentResponse.getExtras(); for (Entry entry : mViewStates.entrySet()) { - final AutofillValue value = entry.getValue().mAutofillValue; + final AutofillValue value = entry.getValue().getCurrentValue(); if (value == null) { if (VERBOSE) { Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey()); @@ -498,39 +503,43 @@ 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 " + flags + " after app was autofilled"); + 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 = new ViewState(this, id, this, ViewState.STATE_INITIAL); mViewStates.put(id, viewState); } if ((flags & FLAG_START_SESSION) != 0) { // View is triggering autofill. - mCurrentViewState = viewState; + mCurrentViewId = viewState.id; viewState.update(value, virtualBounds); + viewState.setState(ViewState.STATE_STARTED_SESSION); return; } if ((flags & FLAG_VALUE_CHANGED) != 0) { - if (value != null && !value.equals(viewState.mAutofillValue)) { - viewState.mValueUpdated = true; + if (value != null && !value.equals(viewState.getCurrentValue())) { + + // TODO(b/33197203 , b/35707731): currently resets STATE_AUTOFILLED; should check + // first (doesn't make a difference now, but it will when it supports partitions) + viewState.setState(ViewState.STATE_CHANGED); // Must check if this update was caused by autofilling the view, in which // case we just update the value, but not the UI. if (mAutoFilledDataset != null) { final AutofillValue filledValue = findValue(mAutoFilledDataset, id); if (value.equals(filledValue)) { - viewState.mAutofillValue = value; + viewState.setCurrentValue(value); return; } } // Change value - viewState.mAutofillValue = value; + viewState.setCurrentValue(value); // Update the chooser UI if (value.isText()) { @@ -545,15 +554,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if ((flags & FLAG_VIEW_ENTERED) != 0) { // Remove the UI if the ViewState has changed. - if (mCurrentViewState != viewState) { - mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null); - mCurrentViewState = viewState; + if (mCurrentViewId != viewState.id) { + mUi.hideFillUi(mCurrentViewId != null ? mCurrentViewId : null); + mCurrentViewId = viewState.id; } // If the ViewState is ready to be displayed, onReady() will be called. viewState.update(value, virtualBounds); - // TODO(b/33197203): Remove when there is a response per activity. if (mCurrentResponse != null) { viewState.setResponse(mCurrentResponse); } @@ -562,9 +570,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if ((flags & FLAG_VIEW_EXITED) != 0) { - if (mCurrentViewState == viewState) { - mUi.hideFillUi(viewState.mId); - mCurrentViewState = null; + if (mCurrentViewId == viewState.id) { + mUi.hideFillUi(viewState.id); + mCurrentViewId = null; } return; } @@ -584,17 +592,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } private void notifyUnavailableToClient() { - if (mCurrentViewState == null) { + if (mCurrentViewId == null) { // TODO(b/33197203): temporary sanity check; should never happen - Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null"); + Slog.w(TAG, "notifyUnavailable(): mCurrentViewId is null"); return; } if (!mHasCallback) return; try { - mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId); + mClient.notifyNoFillUi(mWindowToken, mCurrentViewId); } catch (RemoteException e) { Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken - + " id=" + mCurrentViewState.mId, e); + + " id=" + mCurrentViewId, e); } } @@ -604,21 +612,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + "):" + response); } - if (mCurrentViewState == null) { + if (mCurrentViewId == null) { // TODO(b/33197203): temporary sanity check; should never happen - Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null"); + Slog.w(TAG, "processResponseLocked(): mCurrentViewId is null"); return; } mCurrentResponse = response; - if (mCurrentResponse.getAuthentication() != null) { - // Handle authentication. - final Intent fillInIntent = createAuthFillInIntent(mStructure, - mCurrentResponse.getExtras()); - mCurrentViewState.setResponse(mCurrentResponse, fillInIntent); - return; - } + setViewStatesLocked(response, ViewState.STATE_FILLABLE); if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null && response.getDatasets().size() == 1) { @@ -627,7 +629,50 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - mCurrentViewState.setResponse(mCurrentResponse); + // Updates the UI, if necessary. + final ViewState currentView = mViewStates.get(mCurrentViewId); + currentView.maybeCallOnFillReady(); + } + + /** + * Sets the state of all views in the given response. + */ + private void setViewStatesLocked(FillResponse response, int state) { + final ArrayList datasets = response.getDatasets(); + if (datasets != null) { + for (int i = 0; i < datasets.size(); i++) { + final Dataset dataset = datasets.get(i); + if (dataset == null) { + Slog.w(TAG, "Ignoring null dataset on " + datasets); + continue; + } + setViewStatesLocked(response, dataset, state); + } + } + } + + /** + * Sets the state of all views in the given dataset and response. + */ + private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset, + int state) { + final ArrayList ids = dataset.getFieldIds(); + for (int j = 0; j < ids.size(); j++) { + final AutofillId id = ids.get(j); + ViewState viewState = mViewStates.get(id); + if (viewState != null) { + viewState.setState(state); + } else { + viewState = new ViewState(this, id, this, state); + if (DEBUG) { // TODO(b/33197203): change to VERBOSE once stable + Slog.d(TAG, "Adding autofillable view with id " + id + " and state " + state); + } + mViewStates.put(id, viewState); + } + if (response != null) { + viewState.setResponse(response); + } + } } void autoFill(Dataset dataset) { @@ -650,6 +695,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return mService.getServiceName(); } + FillResponse getCurrentResponse() { + return mCurrentResponse; + } + private Intent createAuthFillInIntent(AssistStructure structure, Bundle extras) { final Intent fillInIntent = new Intent(); fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure); @@ -672,8 +721,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState 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("mCurrentViewStates: "); pw.println(mCurrentViewState); - pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size()); + pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId); + pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size()); final String prefix2 = prefix + " "; for (Map.Entry entry : mViewStates.entrySet()) { pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey()); @@ -700,6 +749,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); } mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues()); + setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED); } catch (RemoteException e) { Slog.w(TAG, "Error autofilling activity: " + e); } diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java index d31dcfd061c00..e3d9c1fcf9ddc 100644 --- a/services/autofill/java/com/android/server/autofill/ViewState.java +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -17,9 +17,9 @@ package com.android.server.autofill; import android.annotation.Nullable; -import android.content.Intent; import android.graphics.Rect; import android.service.autofill.FillResponse; +import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; @@ -40,55 +40,92 @@ final class ViewState { @Nullable AutofillValue value); } - final AutofillId mId; + // NOTE: state constants must be public because of flagstoString(). + public static final int STATE_UNKNOWN = 0x00; + /** Initial state. */ + public static final int STATE_INITIAL = 0x01; + /** View id is present in a dataset returned by the service. */ + public static final int STATE_FILLABLE = 0x02; + /** View was autofilled after user selected a dataset. */ + public static final int STATE_AUTOFILLED = 0x04; + /** View value was changed, but not by the service. */ + public static final int STATE_CHANGED = 0x08; + /** Set only in the View that started a session . */ + public static final int STATE_STARTED_SESSION = 0x10; + + public final AutofillId id; private final Listener mListener; - // TODO(b/33197203): would not need a reference to response and session if it was an inner - // class of Session... private final Session mSession; private FillResponse mResponse; - private Intent mAuthIntent; - // TODO(b/33197203): encapsulate access so it's not called by UI - AutofillValue mAutofillValue; + private AutofillValue mCurrentValue; + private Rect mVirtualBounds; - // TODO(b/33197203): encapsulate access so it's not called by UI - // Bounds if a virtual view, null otherwise - Rect mVirtualBounds; + private int mState; - boolean mValueUpdated; - - ViewState(Session session, AutofillId id, Listener listener) { + ViewState(Session session, AutofillId id, Listener listener, int state) { mSession = session; - mId = id; + this.id = id; mListener = listener; + mState = state; } /** - * Response should only be set once. + * Gets the boundaries of the virtual view, or {@code null} if the the view is not virtual. */ + @Nullable + Rect getVirtualBounds() { + return mVirtualBounds; + } + + /** + * Gets the current value of the view. + */ + @Nullable + AutofillValue getCurrentValue() { + return mCurrentValue; + } + void setResponse(FillResponse response) { mResponse = response; - maybeCallOnFillReady(); } - /** - * Used when a {@link FillResponse} requires authentication to be unlocked. - */ - void setResponse(FillResponse response, Intent authIntent) { - mAuthIntent = authIntent; - setResponse(response); + FillResponse getResponse() { + return mResponse; } CharSequence getServiceName() { return mSession.getServiceName(); } + boolean isChanged() { + return (mState & STATE_CHANGED) != 0; + } + + int getState() { + return mState; + } + + String getStateAsString() { + return DebugUtils.flagsToString(ViewState.class, "STATE_", mState); + } + + void setCurrentValue(AutofillValue value) { + mCurrentValue = value; + } + + 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; + } + // TODO(b/33197203): need to refactor / rename / document this method to make it clear that // it can change the value and update the UI; similarly, should replace code that // directly sets mAutoFilLValue to use encapsulation. void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) { if (autofillValue != null) { - mAutofillValue = autofillValue; + mCurrentValue = autofillValue; } if (virtualBounds != null) { mVirtualBounds = virtualBounds; @@ -103,23 +140,31 @@ final class ViewState { * fill UI is ready to be displayed (i.e. when response and bounds are set). */ void maybeCallOnFillReady() { - if (mResponse != null && (mResponse.getAuthentication() != null - || mResponse.getDatasets() != null)) { - mListener.onFillReady(mResponse, mId, mAutofillValue); + // First try the current response associated with this View. + if (mResponse != null) { + if (mResponse.getDatasets() != null) { + mListener.onFillReady(mResponse, this.id, mCurrentValue); + } + return; + } + // Then checks if the session has a response waiting authentication; if so, uses it instead. + final FillResponse currentResponse = mSession.getCurrentResponse(); + if (currentResponse.getAuthentication() != null) { + mListener.onFillReady(currentResponse, this.id, mCurrentValue); } } @Override public String toString() { - return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds - + ", updated = " + mValueUpdated + "]"; + return "ViewState: [id=" + id + ", currentValue=" + mCurrentValue + + ", bounds=" + mVirtualBounds + ", state=" + getStateAsString() +"]"; } void dump(String prefix, PrintWriter pw) { - pw.print(prefix); pw.print("id:" ); pw.println(mId); - pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue); - pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated); + pw.print(prefix); pw.print("id:" ); pw.println(this.id); + pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString()); + pw.print(prefix); pw.print("has response:" ); pw.println(mResponse != null); + pw.print(prefix); pw.print("currentValue:" ); pw.println(mCurrentValue); pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds); - pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent); } } \ No newline at end of file