/*
* Copyright (C) 2017 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 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.ACTION_START_SESSION;
import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED;
import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sPartitionMaxCount;
import static com.android.server.autofill.Helper.sVerbose;
import static com.android.server.autofill.Helper.toArray;
import static com.android.server.autofill.ViewState.STATE_AUTOFILLED;
import static com.android.server.autofill.ViewState.STATE_RESTARTED_SESSION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.assist.AssistStructure;
import android.app.assist.AssistStructure.AutofillOverlay;
import android.app.assist.AssistStructure.ViewNode;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.graphics.Rect;
import android.metrics.LogMaker;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.InternalValidator;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
import android.service.autofill.ValueFinder;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutoFillManagerClient;
import android.view.autofill.IAutofillWindowPresenter;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.ArrayUtils;
import com.android.server.autofill.ui.AutoFillUI;
import com.android.server.autofill.ui.PendingUi;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A session for a given activity.
*
*
This class manages the multiple {@link ViewState}s for each view it has, and keeps track
* of the current {@link ViewState} to display the appropriate UI.
*
*
Although the autofill requests and callbacks are stateless from the service's point of
* view, we need to keep state in the framework side for cases such as authentication. For
* example, when service return a {@link FillResponse} that contains all the fields needed
* to fill the activity but it requires authentication first, that response need to be held
* until the user authenticates or it times out.
*/
final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
AutoFillUI.AutoFillUiCallback {
private static final String TAG = "AutofillSession";
private static final String EXTRA_REQUEST_ID = "android.service.autofill.extra.REQUEST_ID";
private final AutofillManagerServiceImpl mService;
private final HandlerCaller mHandlerCaller;
private final Object mLock;
private final AutoFillUI mUi;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
private static AtomicInteger sIdCounter = new AtomicInteger();
/** Id of the session */
public final int id;
/** uid the session is for */
public final int uid;
@GuardedBy("mLock")
@NonNull private IBinder mActivityToken;
/** Package name of the app that is auto-filled */
@NonNull private final String mPackageName;
@GuardedBy("mLock")
private final ArrayMap mViewStates = new ArrayMap<>();
/**
* Id of the View currently being displayed.
*/
@GuardedBy("mLock")
@Nullable private AutofillId mCurrentViewId;
@GuardedBy("mLock")
private IAutoFillManagerClient mClient;
private final RemoteFillService mRemoteFillService;
@GuardedBy("mLock")
private SparseArray mResponses;
/**
* 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 ArrayList mContexts;
/**
* Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
*/
private boolean mHasCallback;
/**
* Extras sent by service on {@code onFillRequest()} calls; the first non-null extra is saved
* and used on subsequent {@code onFillRequest()} and {@code onSaveRequest()} calls.
*/
@GuardedBy("mLock")
private Bundle mClientState;
@GuardedBy("mLock")
private boolean mDestroyed;
/** Whether the session is currently saving. */
@GuardedBy("mLock")
private boolean mIsSaving;
/**
* Helper used to handle state of Save UI when it must be hiding to show a custom description
* link and later recovered.
*/
@GuardedBy("mLock")
private PendingUi mPendingSaveUi;
/**
* When the session started (using elapsed time since boot).
*/
private final long mStartTime;
/**
* When the UI was shown for the first time (using elapsed time since boot).
*/
@GuardedBy("mLock")
private long mUiShownTime;
@GuardedBy("mLock")
private final LocalLog mUiLatencyHistory;
/**
* Receiver of assist data from the app's {@link Activity}.
*/
private final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
@Override
public void send(int resultCode, Bundle resultData) throws RemoteException {
final AssistStructure structure = resultData.getParcelable(KEY_STRUCTURE);
if (structure == null) {
Slog.e(TAG, "No assist structure - app might have crashed providing it");
return;
}
final Bundle receiverExtras = resultData.getBundle(KEY_RECEIVER_EXTRAS);
if (receiverExtras == null) {
Slog.e(TAG, "No receiver extras - app might have crashed providing it");
return;
}
final int requestId = receiverExtras.getInt(EXTRA_REQUEST_ID);
if (sVerbose) {
Slog.v(TAG, "New structure for requestId " + requestId + ": " + structure);
}
final FillRequest request;
synchronized (mLock) {
// TODO(b/35708678): Must fetch the data so it's available later on handleSave(),
// even if if the activity is gone by then, but structure .ensureData() gives a
// ONE_WAY warning because system_service could block on app calls. We need to
// change AssistStructure so it provides a "one-way" writeToParcel() method that
// sends all the data
structure.ensureData();
// Sanitize structure before it's sent to service.
structure.sanitizeForParceling(true);
// Flags used to start the session.
final int flags = structure.getFlags();
if (mContexts == null) {
mContexts = new ArrayList<>(1);
}
mContexts.add(new FillContext(requestId, structure));
cancelCurrentRequestLocked();
final int numContexts = mContexts.size();
for (int i = 0; i < numContexts; i++) {
fillContextWithAllowedValuesLocked(mContexts.get(i), flags);
}
// Dispatch a snapshot of the current contexts list since it may change
// until the dispatch happens. The items in the list don't need to be cloned
// since we don't hold on them anywhere else. The client state is not touched
// by us, so no need to copy.
request = new FillRequest(requestId, new ArrayList<>(mContexts),
mClientState, flags);
}
mRemoteFillService.onFillRequest(request);
}
};
/**
* Returns the ids of all entries in {@link #mViewStates} in the same order.
*/
private AutofillId[] getIdsOfAllViewStatesLocked() {
final int numViewState = mViewStates.size();
final AutofillId[] ids = new AutofillId[numViewState];
for (int i = 0; i < numViewState; i++) {
ids[i] = mViewStates.valueAt(i).id;
}
return ids;
}
/**
* Gets the value of a field, using either the {@code viewStates} or the {@code mContexts}, or
* {@code null} when not found on either of them.
*/
@Nullable
private String getValueAsString(@NonNull AutofillId id) {
AutofillValue value = null;
synchronized (mLock) {
final ViewState state = mViewStates.get(id);
if (state == null) {
if (sDebug) Slog.d(TAG, "getValue(): no view state for " + id);
return null;
}
value = state.getCurrentValue();
if (value == null) {
if (sDebug) Slog.d(TAG, "getValue(): no current value for " + id);
value = getValueFromContextsLocked(id);
}
}
if (value != null) {
if (value.isText()) {
return value.getTextValue().toString();
}
if (value.isList()) {
final CharSequence[] options = getAutofillOptionsFromContextsLocked(id);
if (options != null) {
final int index = value.getListValue();
final CharSequence option = options[index];
return option != null ? option.toString() : null;
} else {
Slog.w(TAG, "getValueAsString(): no autofill options for id " + id);
}
}
}
return null;
}
/**
* Updates values of the nodes in the context's structure so that:
* - proper node is focused
* - autofillValue is sent back to service when it was previously autofilled
* - autofillValue is sent in the view used to force a request
*
* @param fillContext The context to be filled
* @param flags The flags that started the session
*/
private void fillContextWithAllowedValuesLocked(@NonNull FillContext fillContext, int flags) {
final ViewNode[] nodes = fillContext
.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked());
final int numViewState = mViewStates.size();
for (int i = 0; i < numViewState; i++) {
final ViewState viewState = mViewStates.valueAt(i);
final ViewNode node = nodes[i];
if (node == null) {
if (sVerbose) {
Slog.v(TAG, "fillStructureWithAllowedValues(): no node for " + viewState.id);
}
continue;
}
final AutofillValue currentValue = viewState.getCurrentValue();
final AutofillValue filledValue = viewState.getAutofilledValue();
final AutofillOverlay overlay = new AutofillOverlay();
// Sanitizes the value if the current value matches what the service sent.
if (filledValue != null && filledValue.equals(currentValue)) {
overlay.value = currentValue;
}
if (mCurrentViewId != null) {
// Updates the focus value.
overlay.focused = mCurrentViewId.equals(viewState.id);
// Sanitizes the value of the focused field in a manual request.
if (overlay.focused && (flags & FLAG_MANUAL_REQUEST) != 0) {
overlay.value = currentValue;
}
}
node.setAutofillOverlay(overlay);
}
}
/**
* Cancels the last request sent to the {@link #mRemoteFillService}.
*/
private void cancelCurrentRequestLocked() {
final int canceledRequest = mRemoteFillService.cancelCurrentRequest();
// Remove the FillContext as there will never be a response for the service
if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
final 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) {
if (sDebug) Slog.d(TAG, "cancelCurrentRequest(): id = " + canceledRequest);
mContexts.remove(i);
break;
}
}
}
}
/**
* Reads a new structure and then request a new fill response from the fill service.
*/
private void requestNewFillResponseLocked(int flags) {
int requestId;
do {
requestId = sIdCounter.getAndIncrement();
} while (requestId == INVALID_REQUEST_ID);
if (sVerbose) {
Slog.v(TAG, "Requesting structure for requestId=" + requestId + ", flags=" + flags);
}
// If the focus changes very quickly before the first request is returned each focus change
// triggers a new partition and we end up with many duplicate partitions. This is
// 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.
cancelCurrentRequestLocked();
try {
final Bundle receiverExtras = new Bundle();
receiverExtras.putInt(EXTRA_REQUEST_ID, requestId);
final long identity = Binder.clearCallingIdentity();
try {
if (!ActivityManager.getService().requestAutofillData(mAssistReceiver,
receiverExtras, mActivityToken, flags)) {
Slog.w(TAG, "failed to request autofill data for " + mActivityToken);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
} catch (RemoteException e) {
// Should not happen, it's a local call.
}
}
Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui,
@NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId,
@NonNull Object lock, int sessionId, int uid, @NonNull IBinder activityToken,
@NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory,
@NonNull ComponentName componentName, @NonNull String packageName) {
id = sessionId;
this.uid = uid;
mStartTime = SystemClock.elapsedRealtime();
mService = service;
mLock = lock;
mUi = ui;
mHandlerCaller = handlerCaller;
mRemoteFillService = new RemoteFillService(context, componentName, userId, this);
mActivityToken = activityToken;
mHasCallback = hasCallback;
mUiLatencyHistory = uiLatencyHistory;
mPackageName = packageName;
mClient = IAutoFillManagerClient.Stub.asInterface(client);
writeLog(MetricsEvent.AUTOFILL_SESSION_STARTED);
}
/**
* Gets the currently registered activity token
*
* @return The activity token
*/
@NonNull IBinder getActivityTokenLocked() {
return mActivityToken;
}
/**
* Sets new activity and client for this session.
*
* @param newActivity The token of the new activity
* @param newClient The client receiving autofill callbacks
*/
void switchActivity(@NonNull IBinder newActivity, @NonNull IBinder newClient) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#switchActivity() rejected - session: "
+ id + " destroyed");
return;
}
mActivityToken = newActivity;
mClient = IAutoFillManagerClient.Stub.asInterface(newClient);
// The tracked id are not persisted in the client, hence update them
updateTrackedIdsLocked();
}
}
// FillServiceCallbacks
@Override
public void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
int serviceUid, @NonNull String servicePackageName) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
+ id + " destroyed");
return;
}
}
if (response == null) {
processNullResponseLocked(requestFlags);
return;
}
mService.setLastResponse(serviceUid, id, response);
if ((response.getDatasets() == null || response.getDatasets().isEmpty())
&& response.getAuthentication() == null) {
// Response is "empty" from an UI point of view, need to notify client.
notifyUnavailableToClient(false);
}
synchronized (mLock) {
processResponseLocked(response, requestFlags);
}
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_REQUEST, servicePackageName)
.setType(MetricsEvent.TYPE_SUCCESS)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
response.getDatasets() == null ? 0 : response.getDatasets().size());
mMetricsLogger.write(log);
}
// FillServiceCallbacks
@Override
public void onFillRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestFailure() rejected - session: "
+ id + " destroyed");
return;
}
mService.resetLastResponse();
}
LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_REQUEST, servicePackageName)
.setType(MetricsEvent.TYPE_FAILURE);
mMetricsLogger.write(log);
getUiForShowing().showError(message, this);
removeSelf();
}
// FillServiceCallbacks
@Override
public void onSaveRequestSuccess(@NonNull String servicePackageName) {
synchronized (mLock) {
mIsSaving = false;
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onSaveRequestSuccess() rejected - session: "
+ id + " destroyed");
return;
}
}
LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName)
.setType(MetricsEvent.TYPE_SUCCESS);
mMetricsLogger.write(log);
// Nothing left to do...
removeSelf();
}
// FillServiceCallbacks
@Override
public void onSaveRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName) {
synchronized (mLock) {
mIsSaving = false;
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onSaveRequestFailure() rejected - session: "
+ id + " destroyed");
return;
}
}
LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName)
.setType(MetricsEvent.TYPE_FAILURE);
mMetricsLogger.write(log);
getUiForShowing().showError(message, this);
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(int requestId, int datasetIndex, IntentSender intent, Bundle extras) {
if (sDebug) {
Slog.d(TAG, "authenticate(): requestId=" + requestId + "; datasetIdx=" + datasetIndex
+ "; intentSender=" + intent);
}
final Intent fillInIntent;
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#authenticate() rejected - session: "
+ id + " destroyed");
return;
}
fillInIntent = createAuthFillInIntentLocked(requestId, extras);
if (fillInIntent == null) {
forceRemoveSelfLocked();
return;
}
}
mService.setAuthenticationSelected(id);
final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
mHandlerCaller.getHandler().post(() -> startAuthentication(authenticationId,
intent, fillInIntent));
}
// FillServiceCallbacks
@Override
public void onServiceDied(RemoteFillService service) {
// TODO(b/337565347): implement
}
// AutoFillUiCallback
@Override
public void fill(int requestId, int datasetIndex, Dataset dataset) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#fill() rejected - session: "
+ id + " destroyed");
return;
}
}
mHandlerCaller.getHandler().post(() -> autoFill(requestId, datasetIndex, dataset, true));
}
// AutoFillUiCallback
@Override
public void save() {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#save() rejected - session: "
+ id + " destroyed");
return;
}
}
mHandlerCaller.getHandler()
.obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, id, 0)
.sendToTarget();
}
// AutoFillUiCallback
@Override
public void cancelSave() {
synchronized (mLock) {
mIsSaving = false;
if (mDestroyed) {
Slog.w(TAG, "Call to Session#cancelSave() rejected - session: "
+ id + " destroyed");
return;
}
}
mHandlerCaller.getHandler().post(() -> removeSelf());
}
// AutoFillUiCallback
@Override
public void requestShowFillUi(AutofillId id, int width, int height,
IAutofillWindowPresenter presenter) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#requestShowFillUi() rejected - session: "
+ id + " destroyed");
return;
}
if (id.equals(mCurrentViewId)) {
try {
final ViewState view = mViewStates.get(id);
mClient.requestShowFillUi(this.id, id, width, height, view.getVirtualBounds(),
presenter);
} catch (RemoteException e) {
Slog.e(TAG, "Error requesting to show fill UI", e);
}
} else {
if (sDebug) {
Slog.d(TAG, "Do not show full UI on " + id + " as it is not the current view ("
+ mCurrentViewId + ") anymore");
}
}
}
}
// AutoFillUiCallback
@Override
public void requestHideFillUi(AutofillId id) {
synchronized (mLock) {
// NOTE: We allow this call in a destroyed state as the UI is
// asked to go away after we get destroyed, so let it do that.
try {
mClient.requestHideFillUi(this.id, id);
} catch (RemoteException e) {
Slog.e(TAG, "Error requesting to hide fill UI", e);
}
}
}
// AutoFillUiCallback
@Override
public void startIntentSender(IntentSender intentSender) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#startIntentSender() rejected - session: "
+ id + " destroyed");
return;
}
removeSelfLocked();
}
mHandlerCaller.getHandler().post(() -> {
try {
synchronized (mLock) {
mClient.startIntentSender(intentSender, null);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
}
});
}
void setAuthenticationResultLocked(Bundle data, int authenticationId) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#setAuthenticationResultLocked() rejected - session: "
+ id + " destroyed");
return;
}
if (mResponses == null) {
// Typically happens when app explicitly called cancel() while the service was showing
// the auth UI.
Slog.w(TAG, "setAuthenticationResultLocked(" + authenticationId + "): no responses");
removeSelf();
return;
}
final int requestId = AutofillManager.getRequestIdFromAuthenticationId(authenticationId);
final FillResponse authenticatedResponse = mResponses.get(requestId);
if (authenticatedResponse == null || data == null) {
removeSelf();
return;
}
final int datasetIdx = AutofillManager.getDatasetIdFromAuthenticationId(
authenticationId);
// Authenticated a dataset - reset view state regardless if we got a response or a dataset
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
final Dataset dataset = authenticatedResponse.getDatasets().get(datasetIdx);
if (dataset == null) {
removeSelf();
return;
}
}
final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
if (sDebug) Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result);
if (result instanceof FillResponse) {
writeLog(MetricsEvent.AUTOFILL_AUTHENTICATED);
replaceResponseLocked(authenticatedResponse, (FillResponse) result);
} else if (result instanceof Dataset) {
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
writeLog(MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED);
final Dataset dataset = (Dataset) result;
authenticatedResponse.getDatasets().set(datasetIdx, dataset);
autoFill(requestId, datasetIdx, dataset, false);
} else {
writeLog(MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION);
}
} else {
if (result != null) {
Slog.w(TAG, "service returned invalid auth type: " + result);
}
writeLog(MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION);
processNullResponseLocked(0);
}
}
void setHasCallbackLocked(boolean hasIt) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#setHasCallbackLocked() rejected - session: "
+ id + " destroyed");
return;
}
mHasCallback = hasIt;
}
@Nullable
private FillResponse getLastResponseLocked(@Nullable String logPrefix) {
if (mContexts == null) {
if (sDebug && logPrefix != null) Slog.d(TAG, logPrefix + ": no contexts");
return 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 (sVerbose && logPrefix != null) {
Slog.v(TAG, logPrefix + ": no responses on session");
}
return null;
}
final int lastResponseIdx = getLastResponseIndexLocked();
if (lastResponseIdx < 0) {
if (logPrefix != null) {
Slog.w(TAG, logPrefix + ": did not get last response. mResponses=" + mResponses
+ ", mViewStates=" + mViewStates);
}
return null;
}
final FillResponse response = mResponses.valueAt(lastResponseIdx);
if (sVerbose && logPrefix != null) {
Slog.v(TAG, logPrefix + ": mResponses=" + mResponses + ", mContexts=" + mContexts
+ ", mViewStates=" + mViewStates);
}
return response;
}
@Nullable
private SaveInfo getSaveInfoLocked() {
final FillResponse response = getLastResponseLocked(null);
return response == null ? null : response.getSaveInfo();
}
/**
* Shows the save UI, when session can be saved.
*
* @return {@code true} if session is done, or {@code false} if it's pending user action.
*/
public boolean showSaveLocked() {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#showSaveLocked() rejected - session: "
+ id + " destroyed");
return false;
}
final FillResponse response = getLastResponseLocked("showSaveLocked()");
final SaveInfo saveInfo = response == null ? null : response.getSaveInfo();
/*
* The Save dialog is only shown if all conditions below are met:
*
* - saveInfo is not null.
* - autofillValue of all required ids is not null.
* - autofillValue of at least one id (required or optional) has changed.
* - there is no Dataset in the last FillResponse whose values of all dataset fields matches
* the current values of all fields in the screen.
*/
if (saveInfo == null) {
return true;
}
// Cache used to make sure changed fields do not belong to a dataset.
final ArrayMap currentValues = new ArrayMap<>();
final ArraySet allIds = new ArraySet<>();
final AutofillId[] requiredIds = saveInfo.getRequiredIds();
boolean allRequiredAreNotEmpty = true;
boolean atLeastOneChanged = false;
if (requiredIds != null) {
for (int i = 0; i < requiredIds.length; i++) {
final AutofillId id = requiredIds[i];
if (id == null) {
Slog.w(TAG, "null autofill id on " + Arrays.toString(requiredIds));
continue;
}
allIds.add(id);
final ViewState viewState = mViewStates.get(id);
if (viewState == null) {
Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id);
allRequiredAreNotEmpty = false;
break;
}
AutofillValue value = viewState.getCurrentValue();
if (value == null || value.isEmpty()) {
final AutofillValue initialValue = getValueFromContextsLocked(id);
if (initialValue != null) {
if (sDebug) {
Slog.d(TAG, "Value of required field " + id + " didn't change; "
+ "using initial value (" + initialValue + ") instead");
}
value = initialValue;
} else {
if (sDebug) {
Slog.d(TAG, "empty value for required " + id );
}
allRequiredAreNotEmpty = false;
break;
}
}
currentValues.put(id, value);
final AutofillValue filledValue = viewState.getAutofilledValue();
if (!value.equals(filledValue)) {
if (sDebug) {
Slog.d(TAG, "found a change on required " + id + ": " + filledValue
+ " => " + value);
}
atLeastOneChanged = true;
}
}
}
final AutofillId[] optionalIds = saveInfo.getOptionalIds();
if (allRequiredAreNotEmpty) {
if (!atLeastOneChanged && optionalIds != null) {
// No change on required ids yet, look for changes on optional ids.
for (int i = 0; i < optionalIds.length; i++) {
final AutofillId id = optionalIds[i];
allIds.add(id);
final ViewState viewState = mViewStates.get(id);
if (viewState == null) {
Slog.w(TAG, "no ViewState for optional " + id);
continue;
}
if ((viewState.getState() & ViewState.STATE_CHANGED) != 0) {
final AutofillValue currentValue = viewState.getCurrentValue();
currentValues.put(id, currentValue);
final AutofillValue filledValue = viewState.getAutofilledValue();
if (currentValue != null && !currentValue.equals(filledValue)) {
if (sDebug) {
Slog.d(TAG, "found a change on optional " + id + ": " + filledValue
+ " => " + currentValue);
}
atLeastOneChanged = true;
break;
}
} else {
// Update current values cache based on initial value
final AutofillValue initialValue = getValueFromContextsLocked(id);
if (sDebug) {
Slog.d(TAG, "no current value for " + id + "; initial value is "
+ initialValue);
}
if (initialValue != null) {
currentValues.put(id, initialValue);
}
}
}
}
if (atLeastOneChanged) {
if (sDebug) {
Slog.d(TAG, "at least one field changed, validate fields for save UI");
}
final ValueFinder valueFinder = (id) -> {return getValueAsString(id);};
final InternalValidator validator = saveInfo.getValidator();
if (validator != null) {
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION);
boolean isValid;
try {
isValid = validator.isValid(valueFinder);
log.setType(isValid
? MetricsEvent.TYPE_SUCCESS
: MetricsEvent.TYPE_DISMISS);
} catch (Exception e) {
Slog.e(TAG, "Not showing save UI because validation failed:", e);
log.setType(MetricsEvent.TYPE_FAILURE);
mMetricsLogger.write(log);
return true;
}
mMetricsLogger.write(log);
if (!isValid) {
Slog.i(TAG, "not showing save UI because fields failed validation");
return true;
}
}
// Make sure the service doesn't have the fields already by checking the datasets
// content.
final List datasets = response.getDatasets();
if (datasets != null) {
datasets_loop: for (int i = 0; i < datasets.size(); i++) {
final Dataset dataset = datasets.get(i);
final ArrayMap datasetValues =
Helper.getFields(dataset);
if (sVerbose) {
Slog.v(TAG, "Checking if saved fields match contents of dataset #" + i
+ ": " + dataset + "; allIds=" + allIds);
}
for (int j = 0; j < allIds.size(); j++) {
final AutofillId id = allIds.valueAt(j);
final AutofillValue currentValue = currentValues.get(id);
if (currentValue == null) {
if (sDebug) {
Slog.d(TAG, "dataset has value for field that is null: " + id);
}
continue datasets_loop;
}
final AutofillValue datasetValue = datasetValues.get(id);
if (!currentValue.equals(datasetValue)) {
if (sDebug) Slog.d(TAG, "found a change on id " + id);
continue datasets_loop;
}
if (sVerbose) Slog.v(TAG, "no changes for id " + id);
}
if (sDebug) {
Slog.d(TAG, "ignoring Save UI because all fields match contents of "
+ "dataset #" + i + ": " + dataset);
}
return true;
}
}
if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!");
mService.logSaveShown(id);
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
getUiForShowing().showSaveUi(mService.getServiceLabel(), mService.getServiceIcon(),
mService.getServicePackageName(), saveInfo, valueFinder, mPackageName, this,
mPendingSaveUi);
if (client != null) {
try {
client.setSaveUiState(id, true);
} catch (RemoteException e) {
Slog.e(TAG, "Error notifying client to set save UI state to shown: " + e);
}
}
mIsSaving = true;
return false;
}
}
// Nothing changed...
if (sDebug) {
Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities."
+ "allRequiredAreNotNull=" + allRequiredAreNotEmpty
+ ", atLeastOneChanged=" + atLeastOneChanged);
}
return true;
}
/**
* Returns whether the session is currently showing the save UI
*/
boolean isSavingLocked() {
return mIsSaving;
}
/**
* Gets the latest non-empty value for the given id in the autofill contexts.
*/
@Nullable
private AutofillValue getValueFromContextsLocked(AutofillId id) {
final int numContexts = mContexts.size();
for (int i = numContexts - 1; i >= 0; i--) {
final FillContext context = mContexts.get(i);
final ViewNode node = context.findViewNodeByAutofillId(id);
if (node != null) {
final AutofillValue value = node.getAutofillValue();
if (sDebug) {
Slog.d(TAG, "getValueFromContexts(" + id + ") at " + i + ": " + value);
}
if (value != null && !value.isEmpty()) {
return value;
}
}
}
return null;
}
/**
* Gets the latest autofill options for the given id in the autofill contexts.
*/
@Nullable
private CharSequence[] getAutofillOptionsFromContextsLocked(AutofillId id) {
final int numContexts = mContexts.size();
for (int i = numContexts - 1; i >= 0; i--) {
final FillContext context = mContexts.get(i);
final ViewNode node = context.findViewNodeByAutofillId(id);
if (node != null && node.getAutofillOptions() != null) {
return node.getAutofillOptions();
}
}
return null;
}
/**
* Calls service when user requested save.
*/
void callSaveLocked() {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#callSaveLocked() rejected - session: "
+ id + " destroyed");
return;
}
if (sVerbose) Slog.v(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
if (mContexts == null) {
Slog.w(TAG, "callSaveLocked(): no contexts");
return;
}
final int numContexts = mContexts.size();
for (int contextNum = 0; contextNum < numContexts; contextNum++) {
final FillContext context = mContexts.get(contextNum);
final ViewNode[] nodes =
context.findViewNodesByAutofillIds(getIdsOfAllViewStatesLocked());
if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + context);
for (int viewStateNum = 0; viewStateNum < mViewStates.size(); viewStateNum++) {
final ViewState state = mViewStates.valueAt(viewStateNum);
final AutofillId id = state.id;
final AutofillValue value = state.getCurrentValue();
if (value == null) {
if (sVerbose) Slog.v(TAG, "callSaveLocked(): skipping " + id);
continue;
}
final ViewNode node = nodes[viewStateNum];
if (node == null) {
Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
continue;
}
if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
node.updateAutofillValue(value);
}
// Sanitize structure before it's sent to service.
context.getStructure().sanitizeForParceling(false);
if (sVerbose) {
Slog.v(TAG, "Dumping structure of " + context + " before calling service.save()");
context.getStructure().dump(false);
}
}
// Remove pending fill requests as the session is finished.
cancelCurrentRequestLocked();
// Dispatch a snapshot of the current contexts list since it may change
// until the dispatch happens. The items in the list don't need to be cloned
// since we don't hold on them anywhere else. The client state is not touched
// by us, so no need to copy.
final SaveRequest saveRequest = new SaveRequest(new ArrayList<>(mContexts), mClientState);
mRemoteFillService.onSaveRequest(saveRequest);
}
/**
* Starts (if necessary) a new fill request upon entering a view.
*
* A new request will be started in 2 scenarios:
*
* - If the user manually requested autofill after the view was already filled.
*
- If the view is part of a new partition.
*
*
* @param id The id of the view that is entered.
* @param viewState The view that is entered.
* @param flags The flag that was passed by the AutofillManager.
*/
private void requestNewFillResponseIfNecessaryLocked(@NonNull AutofillId id,
@NonNull ViewState viewState, int flags) {
// First check if this is a manual request after view was autofilled.
final int state = viewState.getState();
final boolean restart = (state & STATE_AUTOFILLED) != 0
&& (flags & FLAG_MANUAL_REQUEST) != 0;
if (restart) {
if (sDebug) Slog.d(TAG, "Re-starting session on view " + id);
viewState.setState(STATE_RESTARTED_SESSION);
requestNewFillResponseLocked(flags);
return;
}
// If it's not, then check if it it should start a partition.
if (shouldStartNewPartitionLocked(id)) {
if (sDebug) {
Slog.d(TAG, "Starting partition for view id " + id + ": "
+ viewState.getStateAsString());
}
viewState.setState(ViewState.STATE_STARTED_PARTITION);
requestNewFillResponseLocked(flags);
}
}
/**
* Determines if a new partition should be started for an id.
*
* @param id The id of the view that is entered
*
* @return {@code true} iff a new partition should be started
*/
private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id) {
if (mResponses == null) {
return true;
}
final int numResponses = mResponses.size();
if (numResponses >= sPartitionMaxCount) {
Slog.e(TAG, "Not starting a new partition on " + id + " because session " + this.id
+ " reached maximum of " + sPartitionMaxCount);
return false;
}
for (int responseNum = 0; responseNum < numResponses; responseNum++) {
final FillResponse response = mResponses.valueAt(responseNum);
if (ArrayUtils.contains(response.getIgnoredIds(), id)) {
return false;
}
final SaveInfo saveInfo = response.getSaveInfo();
if (saveInfo != null) {
if (ArrayUtils.contains(saveInfo.getOptionalIds(), id)
|| ArrayUtils.contains(saveInfo.getRequiredIds(), id)) {
return false;
}
}
final List datasets = response.getDatasets();
if (datasets != null) {
final int numDatasets = datasets.size();
for (int dataSetNum = 0; dataSetNum < numDatasets; dataSetNum++) {
final ArrayList fields = datasets.get(dataSetNum).getFieldIds();
if (fields != null && fields.contains(id)) {
return false;
}
}
}
if (ArrayUtils.contains(response.getAuthenticationIds(), id)) {
return false;
}
}
return true;
}
void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int action,
int flags) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#updateLocked() rejected - session: "
+ id + " destroyed");
return;
}
if (sVerbose) {
Slog.v(TAG, "updateLocked(): id=" + id + ", action=" + action + ", flags=" + flags);
}
ViewState viewState = mViewStates.get(id);
if (viewState == null) {
if (action == ACTION_START_SESSION || action == ACTION_VALUE_CHANGED
|| action == ACTION_VIEW_ENTERED) {
if (sVerbose) Slog.v(TAG, "Creating viewState for " + id + " on " + action);
boolean isIgnored = isIgnoredLocked(id);
viewState = new ViewState(this, id, this,
isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL);
mViewStates.put(id, viewState);
if (isIgnored) {
if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + id);
return;
}
} else {
if (sVerbose) Slog.v(TAG, "Ignored action " + action + " for " + id);
return;
}
}
switch(action) {
case ACTION_START_SESSION:
// View is triggering autofill.
mCurrentViewId = viewState.id;
viewState.update(value, virtualBounds, flags);
viewState.setState(ViewState.STATE_STARTED_SESSION);
requestNewFillResponseLocked(flags);
break;
case ACTION_VALUE_CHANGED:
if (value != null && !value.equals(viewState.getCurrentValue())) {
if (value.isEmpty()
&& viewState.getCurrentValue() != null
&& viewState.getCurrentValue().isText()
&& viewState.getCurrentValue().getTextValue() != null
&& getSaveInfoLocked() != null) {
final int length = viewState.getCurrentValue().getTextValue().length();
if (sDebug) {
Slog.d(TAG, "updateLocked(" + id + "): resetting value that was "
+ length + " chars long");
}
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_VALUE_RESET)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_PREVIOUS_LENGTH, length);
mMetricsLogger.write(log);
}
// Always update the internal state.
viewState.setCurrentValue(value);
// Must check if this update was caused by autofilling the view, in which
// case we just update the value, but not the UI.
final AutofillValue filledValue = viewState.getAutofilledValue();
if (value.equals(filledValue)) {
return;
}
// Update the internal state...
viewState.setState(ViewState.STATE_CHANGED);
//..and the UI
if (value.isText()) {
getUiForShowing().filterFillUi(value.getTextValue().toString(), this);
} else {
getUiForShowing().filterFillUi(null, this);
}
}
break;
case ACTION_VIEW_ENTERED:
if (sVerbose && virtualBounds != null) {
Slog.w(TAG, "entered on virtual child " + id + ": " + virtualBounds);
}
requestNewFillResponseIfNecessaryLocked(id, viewState, flags);
// Remove the UI if the ViewState has changed.
if (mCurrentViewId != viewState.id) {
mUi.hideFillUi(this);
mCurrentViewId = viewState.id;
}
// If the ViewState is ready to be displayed, onReady() will be called.
viewState.update(value, virtualBounds, flags);
break;
case ACTION_VIEW_EXITED:
if (mCurrentViewId == viewState.id) {
if (sVerbose) Slog.d(TAG, "Exiting view " + id);
mUi.hideFillUi(this);
mCurrentViewId = null;
}
break;
default:
Slog.w(TAG, "updateLocked(): unknown action: " + action);
}
}
/**
* Checks whether a view should be ignored.
*/
private boolean isIgnoredLocked(AutofillId id) {
if (mResponses == null || mResponses.size() == 0) {
return false;
}
// Always check the latest response only
final FillResponse response = mResponses.valueAt(mResponses.size() - 1);
return ArrayUtils.contains(response.getIgnoredIds(), id);
}
@Override
public void onFillReady(FillResponse response, AutofillId filledId,
@Nullable AutofillValue value) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillReady() rejected - session: "
+ id + " destroyed");
return;
}
}
String filterText = null;
if (value != null && value.isText()) {
filterText = value.getTextValue().toString();
}
getUiForShowing().showFillUi(filledId, response, filterText,
mService.getServicePackageName(), mPackageName, this);
synchronized (mLock) {
if (mUiShownTime == 0) {
// Log first time UI is shown.
mUiShownTime = SystemClock.elapsedRealtime();
final long duration = mUiShownTime - mStartTime;
if (sDebug) {
final StringBuilder msg = new StringBuilder("1st UI for ")
.append(mActivityToken)
.append(" shown in ");
TimeUtils.formatDuration(duration, msg);
Slog.d(TAG, msg.toString());
}
final StringBuilder historyLog = new StringBuilder("id=").append(id)
.append(" app=").append(mActivityToken)
.append(" svc=").append(mService.getServicePackageName())
.append(" latency=");
TimeUtils.formatDuration(duration, historyLog);
mUiLatencyHistory.log(historyLog.toString());
final LogMaker metricsLog = newLogMaker(MetricsEvent.AUTOFILL_UI_LATENCY)
.setCounterValue((int) duration);
mMetricsLogger.write(metricsLog);
}
}
}
boolean isDestroyed() {
synchronized (mLock) {
return mDestroyed;
}
}
IAutoFillManagerClient getClient() {
synchronized (mLock) {
return mClient;
}
}
private void notifyUnavailableToClient(boolean sessionFinished) {
synchronized (mLock) {
if (mCurrentViewId == null) return;
try {
if (mHasCallback) {
mClient.notifyNoFillUi(id, mCurrentViewId, sessionFinished);
} else if (sessionFinished) {
mClient.setSessionFinished(AutofillManager.STATE_FINISHED);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error notifying client no fill UI: id=" + mCurrentViewId, e);
}
}
}
private void updateTrackedIdsLocked() {
if (mResponses == null || mResponses.size() == 0) {
return;
}
// Only track the views of the last response as only those are reported back to the
// service, see #showSaveLocked
final FillResponse response = mResponses.valueAt(getLastResponseIndexLocked());
ArraySet trackedViews = null;
boolean saveOnAllViewsInvisible = false;
final SaveInfo saveInfo = response.getSaveInfo();
if (saveInfo != null) {
saveOnAllViewsInvisible =
(saveInfo.getFlags() & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0;
// We only need to track views if we want to save once they become invisible.
if (saveOnAllViewsInvisible) {
if (trackedViews == null) {
trackedViews = new ArraySet<>();
}
if (saveInfo.getRequiredIds() != null) {
Collections.addAll(trackedViews, saveInfo.getRequiredIds());
}
if (saveInfo.getOptionalIds() != null) {
Collections.addAll(trackedViews, saveInfo.getOptionalIds());
}
}
}
// Must also track that are part of datasets, otherwise the FillUI won't be hidden when
// they go away (if they're not savable).
final List datasets = response.getDatasets();
ArraySet fillableIds = null;
if (datasets != null) {
for (int i = 0; i < datasets.size(); i++) {
final Dataset dataset = datasets.get(i);
final ArrayList fieldIds = dataset.getFieldIds();
if (fieldIds == null) continue;
for (int j = 0; j < fieldIds.size(); j++) {
final AutofillId id = fieldIds.get(j);
if (trackedViews == null || !trackedViews.contains(id)) {
fillableIds = ArrayUtils.add(fillableIds, id);
}
}
}
}
try {
if (sVerbose) {
Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds);
}
mClient.setTrackedViews(id, toArray(trackedViews), saveOnAllViewsInvisible,
toArray(fillableIds));
} catch (RemoteException e) {
Slog.w(TAG, "Cannot set tracked ids", e);
}
}
private void replaceResponseLocked(@NonNull FillResponse oldResponse,
@NonNull FillResponse newResponse) {
// Disassociate view states with the old response
setViewStatesLocked(oldResponse, ViewState.STATE_INITIAL, true);
// Move over the id
newResponse.setRequestId(oldResponse.getRequestId());
// Replace the old response
mResponses.put(newResponse.getRequestId(), newResponse);
// Now process the new response
processResponseLocked(newResponse, 0);
}
private void processNullResponseLocked(int flags) {
if (sVerbose) Slog.v(TAG, "canceling session " + id + " when server returned null");
if ((flags & FLAG_MANUAL_REQUEST) != 0) {
getUiForShowing().showError(R.string.autofill_error_cannot_autofill, this);
}
mService.resetLastResponse();
// Nothing to be done, but need to notify client.
notifyUnavailableToClient(true);
removeSelf();
}
private void processResponseLocked(@NonNull FillResponse newResponse, int flags) {
// Make sure we are hiding the UI which will be shown
// only if handling the current response requires it.
mUi.hideAll(this);
final int requestId = newResponse.getRequestId();
if (sVerbose) {
Slog.v(TAG, "processResponseLocked(): mCurrentViewId=" + mCurrentViewId
+ ",flags=" + flags + ", reqId=" + requestId + ", resp=" + newResponse);
}
if (mResponses == null) {
mResponses = new SparseArray<>(4);
}
mResponses.put(requestId, newResponse);
mClientState = newResponse.getClientState();
setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false);
updateTrackedIdsLocked();
if (mCurrentViewId == null) {
return;
}
// Updates the UI, if necessary.
final ViewState currentView = mViewStates.get(mCurrentViewId);
currentView.maybeCallOnFillReady(flags);
}
/**
* Sets the state of all views in the given response.
*/
private void setViewStatesLocked(FillResponse response, int state, boolean clearResponse) {
final List 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, clearResponse);
}
} else if (response.getAuthentication() != null) {
for (AutofillId autofillId : response.getAuthenticationIds()) {
final ViewState viewState = createOrUpdateViewStateLocked(autofillId, state, null);
if (!clearResponse) {
viewState.setResponse(response);
} else {
viewState.setResponse(null);
}
}
}
final SaveInfo saveInfo = response.getSaveInfo();
if (saveInfo != null) {
final AutofillId[] requiredIds = saveInfo.getRequiredIds();
if (requiredIds != null) {
for (AutofillId id : requiredIds) {
createOrUpdateViewStateLocked(id, state, null);
}
}
final AutofillId[] optionalIds = saveInfo.getOptionalIds();
if (optionalIds != null) {
for (AutofillId id : optionalIds) {
createOrUpdateViewStateLocked(id, state, null);
}
}
}
final AutofillId[] authIds = response.getAuthenticationIds();
if (authIds != null) {
for (AutofillId id : authIds) {
createOrUpdateViewStateLocked(id, state, null);
}
}
}
/**
* Sets the state of all views in the given dataset and response.
*/
private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset,
int state, boolean clearResponse) {
final ArrayList ids = dataset.getFieldIds();
final ArrayList values = dataset.getFieldValues();
for (int j = 0; j < ids.size(); j++) {
final AutofillId id = ids.get(j);
final AutofillValue value = values.get(j);
final ViewState viewState = createOrUpdateViewStateLocked(id, state, value);
if (response != null) {
viewState.setResponse(response);
} else if (clearResponse) {
viewState.setResponse(null);
}
}
}
private ViewState createOrUpdateViewStateLocked(@NonNull AutofillId id, int state,
@Nullable AutofillValue value) {
ViewState viewState = mViewStates.get(id);
if (viewState != null) {
viewState.setState(state);
} else {
viewState = new ViewState(this, id, this, state);
if (sVerbose) {
Slog.v(TAG, "Adding autofillable view with id " + id + " and state " + state);
}
mViewStates.put(id, viewState);
}
if ((state & ViewState.STATE_AUTOFILLED) != 0) {
viewState.setAutofilledValue(value);
}
return viewState;
}
void autoFill(int requestId, int datasetIndex, Dataset dataset, boolean generateEvent) {
if (sDebug) {
Slog.d(TAG, "autoFill(): requestId=" + requestId + "; datasetIdx=" + datasetIndex
+ "; dataset=" + dataset);
}
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#autoFill() rejected - session: "
+ id + " destroyed");
return;
}
// Autofill it directly...
if (dataset.getAuthentication() == null) {
if (generateEvent) {
mService.logDatasetSelected(dataset.getId(), id);
}
autoFillApp(dataset);
return;
}
// ...or handle authentication.
mService.logDatasetAuthenticationSelected(dataset.getId(), id);
setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH, false);
final Intent fillInIntent = createAuthFillInIntentLocked(requestId, mClientState);
if (fillInIntent == null) {
forceRemoveSelfLocked();
return;
}
final int authenticationId = AutofillManager.makeAuthenticationId(requestId,
datasetIndex);
startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent);
}
}
CharSequence getServiceName() {
synchronized (mLock) {
return mService.getServiceName();
}
}
// TODO: this should never be null, but we got at least one occurrence, probably due to a race.
@Nullable
private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) {
final Intent fillInIntent = new Intent();
final FillContext context = getFillContextByRequestIdLocked(requestId);
if (context == null) {
Slog.wtf(TAG, "createAuthFillInIntentLocked(): no FillContext. requestId=" + requestId
+ "; mContexts= " + mContexts);
return null;
}
fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure());
fillInIntent.putExtra(AutofillManager.EXTRA_CLIENT_STATE, extras);
return fillInIntent;
}
private void startAuthentication(int authenticationId, IntentSender intent,
Intent fillInIntent) {
try {
synchronized (mLock) {
mClient.authenticate(id, authenticationId, intent, fillInIntent);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
}
}
@Override
public String toString() {
return "Session: [id=" + id + ", pkg=" + mPackageName + "]";
}
void dumpLocked(String prefix, PrintWriter pw) {
final String prefix2 = prefix + " ";
pw.print(prefix); pw.print("id: "); pw.println(id);
pw.print(prefix); pw.print("uid: "); pw.println(uid);
pw.print(prefix); pw.print("mPackagename: "); pw.println(mPackageName);
pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("mStartTime: "); pw.println(mStartTime);
pw.print(prefix); pw.print("Time to show UI: ");
if (mUiShownTime == 0) {
pw.println("N/A");
} else {
TimeUtils.formatDuration(mUiShownTime - mStartTime, pw);
pw.println();
}
pw.print(prefix); pw.print("mResponses: ");
if (mResponses == null) {
pw.println("null");
} else {
pw.println(mResponses.size());
for (int i = 0; i < mResponses.size(); i++) {
pw.print(prefix2); pw.print('#'); pw.print(i);
pw.print(' '); pw.println(mResponses.valueAt(i));
}
}
pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId);
pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size());
pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving);
pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi);
for (Map.Entry entry : mViewStates.entrySet()) {
pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
entry.getValue().dump(prefix2, pw);
}
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 (sVerbose) {
pw.println(context.getStructure() + " (look at logcat)");
// TODO: add method on AssistStructure to dump on pw
context.getStructure().dump(false);
}
}
} else {
pw.println("null");
}
pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
pw.print(prefix); pw.print("mClientState: "); pw.println(
Helper.bundleToString(mClientState));
mRemoteFillService.dump(prefix, pw);
}
void autoFillApp(Dataset dataset) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#autoFillApp() rejected - session: "
+ id + " destroyed");
return;
}
try {
// Skip null values as a null values means no change
final int entryCount = dataset.getFieldIds().size();
final List ids = new ArrayList<>(entryCount);
final List values = new ArrayList<>(entryCount);
boolean waitingDatasetAuth = false;
for (int i = 0; i < entryCount; i++) {
if (dataset.getFieldValues().get(i) == null) {
continue;
}
final AutofillId viewId = dataset.getFieldIds().get(i);
ids.add(viewId);
values.add(dataset.getFieldValues().get(i));
final ViewState viewState = mViewStates.get(viewId);
if (viewState != null
&& (viewState.getState() & ViewState.STATE_WAITING_DATASET_AUTH) != 0) {
if (sVerbose) {
Slog.v(TAG, "autofillApp(): view " + viewId + " waiting auth");
}
waitingDatasetAuth = true;
viewState.resetState(ViewState.STATE_WAITING_DATASET_AUTH);
}
}
if (!ids.isEmpty()) {
if (waitingDatasetAuth) {
mUi.hideFillUi(this);
}
if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
mClient.autofill(id, ids, values);
setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED, false);
}
} catch (RemoteException e) {
Slog.w(TAG, "Error autofilling activity: " + e);
}
}
}
private AutoFillUI getUiForShowing() {
synchronized (mLock) {
mUi.setCallback(this);
return mUi;
}
}
/**
* Cleans up this session.
*
* Typically called in 2 scenarios:
*
*
* - When the session naturally finishes (i.e., from {@link #removeSelfLocked()}.
*
- When the service hosting the session is finished (for example, because the user
* disabled it).
*
*/
RemoteFillService destroyLocked() {
if (mDestroyed) {
return null;
}
mUi.destroyAll(mPendingSaveUi, this, true);
mUi.clearCallback(this);
mDestroyed = true;
writeLog(MetricsEvent.AUTOFILL_SESSION_FINISHED);
return mRemoteFillService;
}
/**
* Cleans up this session and remove it from the service always, even if it does have a pending
* Save UI.
*/
void forceRemoveSelfLocked() {
if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi);
final boolean isPendingSaveUi = isSaveUiPendingLocked();
mPendingSaveUi = null;
removeSelfLocked();
mUi.destroyAll(mPendingSaveUi, this, false);
if (!isPendingSaveUi) {
try {
mClient.setSessionFinished(AutofillManager.STATE_UNKNOWN);
} catch (RemoteException e) {
Slog.e(TAG, "Error notifying client to finish session", e);
}
}
}
/**
* Thread-safe version of {@link #removeSelfLocked()}.
*/
private void removeSelf() {
synchronized (mLock) {
removeSelfLocked();
}
}
/**
* Cleans up this session and remove it from the service, but but only if it does not have a
* pending Save UI.
*/
void removeSelfLocked() {
if (sVerbose) Slog.v(TAG, "removeSelfLocked(): " + mPendingSaveUi);
if (mDestroyed) {
Slog.w(TAG, "Call to Session#removeSelfLocked() rejected - session: "
+ id + " destroyed");
return;
}
if (isSaveUiPendingLocked()) {
Slog.i(TAG, "removeSelfLocked() ignored, waiting for pending save ui");
return;
}
final RemoteFillService remoteFillService = destroyLocked();
mService.removeSessionLocked(id);
if (remoteFillService != null) {
remoteFillService.destroy();
}
}
void onPendingSaveUi(int operation, @NonNull IBinder token) {
getUiForShowing().onPendingSaveUi(operation, token);
}
/**
* Checks whether this session is hiding the Save UI to handle a custom description link for
* a specific {@code token} created by
* {@link PendingUi#PendingUi(IBinder, int, IAutoFillManagerClient)}.
*/
boolean isSaveUiPendingForTokenLocked(@NonNull IBinder token) {
return isSaveUiPendingLocked() && token.equals(mPendingSaveUi.getToken());
}
/**
* Checks whether this session is hiding the Save UI to handle a custom description link.
*/
private boolean isSaveUiPendingLocked() {
return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING;
}
private int getLastResponseIndexLocked() {
// The response ids are monotonically increasing so
// we just find the largest id which is the last. We
// do not rely on the internal ordering in sparse
// array to avoid - wow this stopped working!?
int lastResponseIdx = -1;
int lastResponseId = -1;
if (mResponses != null) {
final int responseCount = mResponses.size();
for (int i = 0; i < responseCount; i++) {
if (mResponses.keyAt(i) > lastResponseId) {
lastResponseIdx = i;
}
}
}
return lastResponseIdx;
}
private LogMaker newLogMaker(int category) {
return newLogMaker(category, mService.getServicePackageName());
}
private LogMaker newLogMaker(int category, String servicePackageName) {
return Helper.newLogMaker(category, mPackageName, servicePackageName);
}
private void writeLog(int category) {
mMetricsLogger.write(newLogMaker(category));
}
}