package android.service.autofill;
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static android.view.autofill.Helper.DEBUG;
import android.annotation.NonNull;
private final @Nullable IntentSender mAuthentication;
private final @Nullable AutofillId[] mAuthenticationIds;
private final @Nullable AutofillId[] mIgnoredIds;
+ private int mRequestId;
private FillResponse(@NonNull Builder builder) {
mDatasets = builder.mDatasets;
mAuthentication = builder.mAuthentication;
mAuthenticationIds = builder.mAuthenticationIds;
mIgnoredIds = builder.mIgnoredIds;
+ mRequestId = INVALID_REQUEST_ID;
}
/** @hide */
}
/**
+ * Associates a {@link FillResponse} to a request.
+ *
+ * <p>Set inside of the {@link FillCallback} code, not the {@link AutofillService}.
+ *
+ * @param requestId The id of the request to associate the response to.
+ *
+ * @hide
+ */
+ public void setRequestId(int requestId) {
+ mRequestId = requestId;
+ }
+
+ /** @hide */
+ public int getRequestId() {
+ return mRequestId;
+ }
+
+ /**
* Builder for {@link FillResponse} objects. You must to provide at least
* one dataset or set an authentication intent with a presentation view.
*/
if (!DEBUG) return super.toString();
return new StringBuilder(
- "FillResponse: [datasets=").append(mDatasets)
+ "FillResponse : [mRequestId=" + mRequestId)
+ .append(", datasets=").append(mDatasets)
.append(", saveInfo=").append(mSaveInfo)
.append(", clientState=").append(mClientState != null)
.append(", hasPresentation=").append(mPresentation != null)
? mAuthenticationIds.length : "N/A")
.append(", ignoredIdsSize=").append(mIgnoredIds != null
? mIgnoredIds.length : "N/A")
+ .append("]")
.toString();
}
parcel.writeParcelable(mAuthentication, flags);
parcel.writeParcelable(mPresentation, flags);
parcel.writeParcelableArray(mIgnoredIds, flags);
+ parcel.writeInt(mRequestId);
}
public static final Parcelable.Creator<FillResponse> CREATOR =
builder.setAuthentication(parcel.readParcelableArray(null, AutofillId.class),
parcel.readParcelable(null), parcel.readParcelable(null));
builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
- return builder.build();
+ final FillResponse response = builder.build();
+
+ response.setRequestId(parcel.readInt());
+
+ return response;
}
@Override
package com.android.server.autofill;
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
+
import static com.android.server.autofill.Helper.DEBUG;
import android.annotation.NonNull;
public interface FillServiceCallbacks {
void onFillRequestSuccess(@Nullable FillResponse response, int serviceUid,
- @NonNull String servicePackageName, int requestId);
+ @NonNull String servicePackageName);
void onFillRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName);
void onSaveRequestSuccess(@NonNull String servicePackageName);
*
* <p>This can be used when the request is unnecessary or will be superceeded by a request that
* will soon be queued.
+ *
+ * @return the id of the canceled request, or {@link FillRequest#INVALID_REQUEST_ID} if no
+ * {@link PendingFillRequest} was canceled.
*/
- public void cancelCurrentRequest() {
+ public int cancelCurrentRequest() {
if (mDestroyed) {
- return;
+ return INVALID_REQUEST_ID;
}
+ int requestId = INVALID_REQUEST_ID;
if (mPendingRequest != null) {
+ if (mPendingRequest instanceof PendingFillRequest) {
+ requestId = ((PendingFillRequest) mPendingRequest).mRequest.getId();
+ }
+
mPendingRequest.cancel();
mPendingRequest = null;
}
+
+ return requestId;
}
public void onFillRequest(@NonNull FillRequest request) {
}
private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest,
- int callingUid, FillResponse response, int requestId) {
+ int callingUid, FillResponse response) {
mHandler.getHandler().post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
mCallbacks.onFillRequestSuccess(response, callingUid,
- mComponentName.getPackageName(), requestId);
+ mComponentName.getPackageName());
}
});
}
}
@Override
- public void onSuccess(FillResponse response, int requestId) {
+ public void onSuccess(FillResponse response) {
RemoteFillService remoteService = mWeakService.get();
if (remoteService != null) {
remoteService.dispatchOnFillRequestSuccess(
- PendingFillRequest.this, getCallingUid(), response, requestId);
+ PendingFillRequest.this, getCallingUid(), response);
}
}
package com.android.server.autofill;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
+import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static android.service.voice.VoiceInteractionSession.KEY_RECEIVER_EXTRAS;
import static android.service.voice.VoiceInteractionSession.KEY_STRUCTURE;
import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
private Dataset mDatasetWaitingAuth;
/**
- * Assist structure sent by the app; it will be updated (sanitized, change values for save)
- * before sent to {@link AutofillService}.
+ * Contexts read from the app; they will be updated (sanitized, change values for save) before
+ * sent to {@link AutofillService}. Ordered by the time they we read.
*/
@GuardedBy("mLock")
- private AssistStructure mStructure;
+ private ArrayList<FillContext> mContexts;
/**
* Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
// Sanitize structure before it's sent to service.
structure.sanitizeForParceling(true);
- mStructure = structure;
- }
+ fillStructureWithAllowedValues(structure);
+
+ if (mContexts == null) {
+ mContexts = new ArrayList<>(1);
+ }
+ mContexts.add(new FillContext(requestId, structure));
- fillStructureWithAllowedValues(mStructure);
+ cancelCurrentRequestLocked();
+ }
- FillRequest request = new FillRequest(requestId, mStructure, mClientState, mFlags);
+ FillRequest request = new FillRequest(requestId, structure, mClientState, mFlags);
mRemoteFillService.onFillRequest(request);
}
};
}
/**
+ * Cancels the last request sent to the {@link #mRemoteFillService}.
+ */
+ private void cancelCurrentRequestLocked() {
+ int canceledRequest = mRemoteFillService.cancelCurrentRequest();
+
+ // Remove the FillContext as there will never be a response for the service
+ if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
+ int numContexts = mContexts.size();
+
+ // It is most likely the last context, hence search backwards
+ for (int i = numContexts - 1; i >= 0; i--) {
+ if (mContexts.get(i).getRequestId() == canceledRequest) {
+ mContexts.remove(i);
+ break;
+ }
+ }
+ }
+
+ }
+
+ /**
* Reads a new structure and then request a new fill response from the fill service.
*/
private void requestNewFillResponseLocked() {
- final int requestId = sIdCounter.getAndIncrement();
+ int requestId;
+
+ do {
+ requestId = sIdCounter.getAndIncrement();
+ } while (requestId == INVALID_REQUEST_ID);
if (DEBUG) {
Slog.d(TAG, "Requesting structure for requestId " + requestId);
// enhanced as the focus change can be much faster than the taking of the assist structure.
// Hence remove the currently queued request and replace it with the one queued after the
// structure is taken. This causes only one fill request per bust of focus changes.
- mRemoteFillService.cancelCurrentRequest();
+ cancelCurrentRequestLocked();
try {
final Bundle receiverExtras = new Bundle();
// FillServiceCallbacks
@Override
public void onFillRequestSuccess(@Nullable FillResponse response, int serviceUid,
- @NonNull String servicePackageName, int requestId) {
+ @NonNull String servicePackageName) {
if (response == null) {
if ((mFlags & FLAG_MANUAL_REQUEST) != 0) {
getUiForShowing().showError(R.string.autofill_error_cannot_autofill);
// TODO(b/37424539): proper implementation
mResponseWaitingAuth = response;
}
- processResponseLocked(response, requestId);
+ processResponseLocked(response);
}
final LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST))
removeSelf();
}
+ /**
+ * Gets the {@link FillContext} for a request.
+ *
+ * @param requestId The id of the request
+ *
+ * @return The context or {@code null} if there is no context
+ */
+ @Nullable private FillContext getFillContextByRequestIdLocked(int requestId) {
+ if (mContexts == null) {
+ return null;
+ }
+
+ int numContexts = mContexts.size();
+ for (int i = 0; i < numContexts; i++) {
+ FillContext context = mContexts.get(i);
+
+ if (context.getRequestId() == requestId) {
+ return context;
+ }
+ }
+
+ return null;
+ }
+
// FillServiceCallbacks
@Override
- public void authenticate(IntentSender intent, Bundle extras) {
+ public void authenticate(int requestId, IntentSender intent, Bundle extras) {
final Intent fillInIntent;
synchronized (mLock) {
- fillInIntent = createAuthFillInIntent(mStructure, extras);
+ fillInIntent = createAuthFillInIntent(
+ getFillContextByRequestIdLocked(requestId).getStructure(), extras);
}
mService.setAuthenticationSelected();
// AutoFillUiCallback
@Override
- public void fill(Dataset dataset) {
- mHandlerCaller.getHandler().post(() -> autoFill(dataset));
+ public void fill(int requestId, Dataset dataset) {
+ mHandlerCaller.getHandler().post(() -> autoFill(requestId, dataset));
}
// AutoFillUiCallback
final Parcelable result = data.getParcelable(
AutofillManager.EXTRA_AUTHENTICATION_RESULT);
if (result instanceof FillResponse) {
+ FillResponse response = (FillResponse) result;
+
mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName);
final int requestIndex = mResponses.indexOfValue(mResponseWaitingAuth);
mResponseWaitingAuth = null;
if (requestIndex >= 0) {
- final int requestId = mResponses.keyAt(requestIndex);
- processResponseLocked((FillResponse) result, requestId);
+ response.setRequestId(mResponses.keyAt(requestIndex));
+ processResponseLocked(response);
} else {
Slog.e(TAG, "Error cannot find id for auth response");
}
if (index >= 0) {
response.getDatasets().set(index, dataset);
mDatasetWaitingAuth = null;
- autoFill(dataset);
+ autoFill(mResponses.keyAt(i), dataset);
resetViewStatesLocked(dataset, ViewState.STATE_WAITING_DATASET_AUTH);
return;
}
* @return {@code true} if session is done, or {@code false} if it's pending user action.
*/
public boolean showSaveLocked() {
- if (mStructure == null) {
- Slog.d(TAG, "showSaveLocked(): no mStructure");
+ if (mContexts == null) {
+ Slog.d(TAG, "showSaveLocked(): no contexts");
return true;
}
if (mResponses == null) {
final FillResponse response = mResponses.valueAt(lastResponseIdx);
final SaveInfo saveInfo = response.getSaveInfo();
if (DEBUG) {
- Slog.d(TAG, "showSaveLocked(): mResponses=" + mResponses
+ Slog.d(TAG, "showSaveLocked(): mResponses=" + mResponses + ", mContexts=" + mContexts
+ ", mViewStates=" + mViewStates);
}
Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
}
- for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
- final AutofillValue value = entry.getValue().getCurrentValue();
- if (value == null) {
- if (VERBOSE) {
- Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
- }
- continue;
- }
- final AutofillId id = entry.getKey();
- final ViewNode node = findViewNodeById(mStructure, id);
- if (node == null) {
- Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
- continue;
- }
+ int numContexts = mContexts.size();
+
+ for (int i = 0; i < numContexts; i++) {
+ FillContext context = mContexts.get(i);
+
if (VERBOSE) {
- Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
+ Slog.v(TAG, "callSaveLocked(): updating " + context);
}
- node.updateAutofillValue(value);
- }
+ for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
+ final AutofillValue value = entry.getValue().getCurrentValue();
+ if (value == null) {
+ if (VERBOSE) {
+ Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
+ }
+ continue;
+ }
+ final AutofillId id = entry.getKey();
+ final ViewNode node = findViewNodeById(context.getStructure(), id);
+ if (node == null) {
+ Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
+ continue;
+ }
+ if (VERBOSE) {
+ Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
+ }
+
+ node.updateAutofillValue(value);
+ }
- // Sanitize structure before it's sent to service.
- mStructure.sanitizeForParceling(false);
+ // Sanitize structure before it's sent to service.
+ context.getStructure().sanitizeForParceling(false);
- if (VERBOSE) {
- Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()");
- mStructure.dump();
+ if (VERBOSE) {
+ Slog.v(TAG, "Dumping structure of " + context + " before calling service.save()");
+ context.getStructure().dump();
+ }
}
- // TODO(b/37426206): Implement partitioning properly
- final int lastResponseIdx = getLastResponseIndex();
- final int requestId = mResponses.keyAt(lastResponseIdx);
- final FillContext fillContext = new FillContext(requestId, mStructure);
- final ArrayList<FillContext> fillContexts = new ArrayList<>(1);
- fillContexts.add(fillContext);
+ // Remove pending fill requests as the session is finished.
+ cancelCurrentRequestLocked();
- final SaveRequest saveRequest = new SaveRequest(fillContexts, mClientState);
+ final SaveRequest saveRequest = new SaveRequest(mContexts, mClientState);
mRemoteFillService.onSaveRequest(saveRequest);
}
}
}
- private void processResponseLocked(FillResponse response, int requestId) {
+ private void processResponseLocked(@NonNull FillResponse response) {
if (DEBUG) {
Slog.d(TAG, "processResponseLocked(mCurrentViewId=" + mCurrentViewId + "):" + response);
}
if (mResponses == null) {
mResponses = new SparseArray<>(4);
}
- mResponses.put(requestId, response);
- if (response != null) {
- mClientState = response.getClientState();
- }
+ mResponses.put(response.getRequestId(), response);
+ mClientState = response.getClientState();
setViewStatesLocked(response, ViewState.STATE_FILLABLE);
updateTrackedIdsLocked();
if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null
&& response.getDatasets().size() == 1) {
Slog.d(TAG, "autofilling manual request directly");
- autoFill(response.getDatasets().get(0));
+ autoFill(response.getRequestId(), response.getDatasets().get(0));
return;
}
}
}
- void autoFill(Dataset dataset) {
+ void autoFill(int requestId, Dataset dataset) {
synchronized (mLock) {
// Autofill it directly...
if (dataset.getAuthentication() == null) {
mService.setDatasetAuthenticationSelected(dataset.getId());
mDatasetWaitingAuth = dataset;
setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH);
- final Intent fillInIntent = createAuthFillInIntent(mStructure, null);
+ final Intent fillInIntent = createAuthFillInIntent(
+ getFillContextByRequestIdLocked(requestId).getStructure(), null);
startAuthentication(dataset.getAuthentication(), fillInIntent);
}
}
pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
entry.getValue().dump(prefix2, pw);
}
- if (VERBOSE) {
- pw.print(prefix); pw.print("mStructure: " );
- // TODO: add method on AssistStructure to dump on pw
- if (mStructure != null) {
- pw.println("look at logcat" );
- mStructure.dump(); // dumps to logcat
- } else {
- pw.println("null");
+
+ pw.print(prefix); pw.print("mContexts: " );
+ if (mContexts != null) {
+ int numContexts = mContexts.size();
+ for (int i = 0; i < numContexts; i++) {
+ FillContext context = mContexts.get(i);
+
+ pw.print(prefix2); pw.print(context);
+ if (VERBOSE) {
+ pw.println(context.getStructure() + " (look at logcat)");
+
+ // TODO: add method on AssistStructure to dump on pw
+ context.getStructure().dump();
+ }
}
+ } else {
+ pw.println("null");
}
+
pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
pw.print(prefix); pw.print("mClientState: "); pw.println(
Helper.bundleToString(mClientState));