Merge "Initial implementation of autofill partitioning." into oc-dev
am: ba2e284a62
Change-Id: I245fc0402f526f2e1ad512c3df5f885856193c5c
diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java
index e27fa06..b7a0420 100644
--- a/core/java/android/service/autofill/Dataset.java
+++ b/core/java/android/service/autofill/Dataset.java
@@ -78,11 +78,6 @@
}
/** @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 3d1c251..dbf1e83 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 @@
+ " 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 ac7d19e..801769c 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.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 @@
@GuardedBy("mLock")
RemoteFillService mRemoteFillService;
- // TODO(b/33197203 , b/35707731): Use List once it supports partitioning
@GuardedBy("mLock")
- private FillResponse mCurrentResponse;
+ private ArrayList<FillResponse> 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 @@
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 @@
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 @@
}
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 @@
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 @@
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 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 @@
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<AutofillId, ViewState> entry : mViewStates.entrySet()) {
final AutofillValue value = entry.getValue().getCurrentValue();
@@ -497,16 +510,21 @@
}
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 @@
}
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
return;
}
- mCurrentResponse = response;
+ if (mResponses == null) {
+ mResponses = new ArrayList<>(4);
+ }
+ mResponses.add(response);
setViewStatesLocked(response, ViewState.STATE_FILLABLE);
@@ -669,10 +711,22 @@
}
}
+ /**
+ * Resets the given state from all existing views in the given dataset.
+ */
+ private void resetViewStatesLocked(@NonNull Dataset dataset, int state) {
+ final ArrayList<AutofillId> 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 @@
}
// ...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 @@
return mService.getServiceName();
}
- FillResponse getCurrentResponse() {
- return mCurrentResponse;
+ FillResponse getResponseWaitingAuth() {
+ return mResponseWaitingAuth;
}
private Intent createAuthFillInIntent(AssistStructure structure, Bundle extras) {
@@ -714,8 +770,8 @@
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 @@
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 20def0c..549f231 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 @@
@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 @@
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 @@
}
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 @@
* 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 @@
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);
}
}