blob: 988db6a871004d27bf1e5abd82ed6eefcef7fa97 [file] [log] [blame]
/*
* 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.AutofillFieldClassificationService.EXTRA_SCORES;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
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 android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM;
import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.autofill.Helper.getNumericValue;
import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sVerbose;
import static com.android.server.autofill.Helper.toArray;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityTaskManager;
import android.app.IAssistDataReceiver;
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.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.metrics.LogMaker;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.Parcelable;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.AutofillFieldClassificationService.Scores;
import android.service.autofill.AutofillService;
import android.service.autofill.CompositeUserData;
import android.service.autofill.Dataset;
import android.service.autofill.FieldClassification;
import android.service.autofill.FieldClassification.Match;
import android.service.autofill.FieldClassificationUserData;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.InternalSanitizer;
import android.service.autofill.InternalValidator;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
import android.text.TextUtils;
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.KeyEvent;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillManager.SmartSuggestionMode;
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.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.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A session for a given activity.
*
* <p>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.
*
* <p>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, ValueFinder {
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 Handler mHandler;
private final Object mLock;
private final AutoFillUI mUi;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
private static AtomicInteger sIdCounter = new AtomicInteger();
/**
* ID of the session.
*
* <p>It's always a positive number, to make it easier to embed it in a long.
*/
public final int id;
/** uid the session is for */
public final int uid;
/** ID of the task associated with this session's activity */
public final int taskId;
/** Flags used to start the session */
public final int mFlags;
@GuardedBy("mLock")
@NonNull private IBinder mActivityToken;
/** Component that's being auto-filled */
@NonNull private final ComponentName mComponentName;
/** Whether the app being autofilled is running in compat mode. */
private final boolean mCompatMode;
/** Node representing the URL bar on compat mode. */
@GuardedBy("mLock")
private ViewNode mUrlBar;
@GuardedBy("mLock")
private boolean mSaveOnAllViewsInvisible;
@GuardedBy("mLock")
private final ArrayMap<AutofillId, ViewState> mViewStates = new ArrayMap<>();
/**
* Id of the View currently being displayed.
*/
@GuardedBy("mLock")
@Nullable private AutofillId mCurrentViewId;
@GuardedBy("mLock")
private IAutoFillManagerClient mClient;
@GuardedBy("mLock")
private DeathRecipient mClientVulture;
/**
* Reference to the remote service.
*
* <p>Only {@code null} when the session is for augmented autofill only.
*/
@Nullable
private final RemoteFillService mRemoteFillService;
@GuardedBy("mLock")
private SparseArray<FillResponse> 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 were read.
*/
@GuardedBy("mLock")
private ArrayList<FillContext> 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;
/**
* List of dataset ids selected by the user.
*/
@GuardedBy("mLock")
private ArrayList<String> mSelectedDatasetIds;
/**
* 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;
@GuardedBy("mLock")
private final LocalLog mWtfHistory;
/**
* Map of {@link MetricsEvent#AUTOFILL_REQUEST} metrics, keyed by fill request id.
*/
@GuardedBy("mLock")
private final SparseArray<LogMaker> mRequestLogs = new SparseArray<>(1);
/**
* Destroys the augmented Autofill UI.
*/
// TODO(b/123099468): this runnable is called when the Autofill session is destroyed, the
// main reason being the cases where user tap HOME.
// Right now it's completely destroying the UI, but we need to decide whether / how to
// properly recover it later (for example, if the user switches back to the activity,
// should it be restored? Right now it kind of is, because Autofill's Session trigger a
// new FillRequest, which in turn triggers the Augmented Autofill request again)
@GuardedBy("mLock")
@Nullable
private Runnable mAugmentedAutofillDestroyer;
/**
* List of {@link MetricsEvent#AUTOFILL_AUGMENTED_REQUEST} metrics.
*/
@GuardedBy("mLock")
private ArrayList<LogMaker> mAugmentedRequestsLogs;
/**
* List of autofill ids of autofillable fields present in the AssistStructure that can be used
* to trigger new augmented autofill requests (because the "standard" service was not interested
* on autofilling the app.
*/
@GuardedBy("mLock")
private ArrayList<AutofillId> mAugmentedAutofillableIds;
/**
* When {@code true}, the session was created only to handle Augmented Autofill requests (i.e.,
* the session would not have existed otherwsie).
*/
@GuardedBy("mLock")
private boolean mForAugmentedAutofillOnly;
/**
* Receiver of assist data from the app's {@link Activity}.
*/
private final IAssistDataReceiver mAssistReceiver = new IAssistDataReceiver.Stub() {
@Override
public void onHandleAssistData(Bundle resultData) throws RemoteException {
if (mRemoteFillService == null) {
wtf(null, "onHandleAssistData() called without a remote service. "
+ "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly);
return;
}
final AssistStructure structure = resultData.getParcelable(ASSIST_KEY_STRUCTURE);
if (structure == null) {
Slog.e(TAG, "No assist structure - app might have crashed providing it");
return;
}
final Bundle receiverExtras = resultData.getBundle(ASSIST_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
try {
structure.ensureDataForAutofill();
} catch (RuntimeException e) {
wtf(e, "Exception lazy loading assist structure for %s: %s",
structure.getActivityComponent(), e);
return;
}
final ArrayList<AutofillId> ids = Helper.getAutofillIds(structure,
/* autofillableOnly= */false);
for (int i = 0; i < ids.size(); i++) {
ids.get(i).setSessionId(Session.this.id);
}
// Flags used to start the session.
int flags = structure.getFlags();
if (mCompatMode) {
// Sanitize URL bar, if needed
final String[] urlBarIds = mService.getUrlBarResourceIdsForCompatMode(
mComponentName.getPackageName());
if (sDebug) {
Slog.d(TAG, "url_bars in compat mode: " + Arrays.toString(urlBarIds));
}
if (urlBarIds != null) {
mUrlBar = Helper.sanitizeUrlBar(structure, urlBarIds);
if (mUrlBar != null) {
final AutofillId urlBarId = mUrlBar.getAutofillId();
if (sDebug) {
Slog.d(TAG, "Setting urlBar as id=" + urlBarId + " and domain "
+ mUrlBar.getWebDomain());
}
final ViewState viewState = new ViewState(urlBarId, Session.this,
ViewState.STATE_URL_BAR);
mViewStates.put(urlBarId, viewState);
}
}
flags |= FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST;
}
structure.sanitizeForParceling(true);
if (mContexts == null) {
mContexts = new ArrayList<>(1);
}
mContexts.add(new FillContext(requestId, structure, mCurrentViewId));
cancelCurrentRequestLocked();
final int numContexts = mContexts.size();
for (int i = 0; i < numContexts; i++) {
fillContextWithAllowedValuesLocked(mContexts.get(i), flags);
}
final ArrayList<FillContext> contexts =
mergePreviousSessionLocked(/* forSave= */ false);
request = new FillRequest(requestId, contexts, mClientState, flags);
}
mRemoteFillService.onFillRequest(request);
}
@Override
public void onHandleAssistScreenshot(Bitmap screenshot) {
// Do nothing
}
};
/**
* Returns the ids of all entries in {@link #mViewStates} in the same order.
*/
@GuardedBy("mLock")
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;
}
@Override
@Nullable
public String findByAutofillId(@NonNull AutofillId id) {
synchronized (mLock) {
AutofillValue value = findValueLocked(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, "findByAutofillId(): no autofill options for id " + id);
}
}
}
}
return null;
}
@Override
public AutofillValue findRawValueByAutofillId(AutofillId id) {
synchronized (mLock) {
return findValueLocked(id);
}
}
/**
* <p>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.
*/
@GuardedBy("mLock")
@Nullable
private AutofillValue findValueLocked(@NonNull AutofillId autofillId) {
final AutofillValue value = findValueFromThisSessionOnlyLocked(autofillId);
if (value != null) {
return getSanitizedValue(createSanitizers(getSaveInfoLocked()), autofillId, value);
}
// TODO(b/113281366): rather than explicitly look for previous session, it might be better
// to merge the sessions when created (see note on mergePreviousSessionLocked())
final ArrayList<Session> previousSessions = mService.getPreviousSessionsLocked(this);
if (previousSessions != null) {
if (sDebug) {
Slog.d(TAG, "findValueLocked(): looking on " + previousSessions.size()
+ " previous sessions for autofillId " + autofillId);
}
for (int i = 0; i < previousSessions.size(); i++) {
final Session previousSession = previousSessions.get(i);
final AutofillValue previousValue = previousSession
.findValueFromThisSessionOnlyLocked(autofillId);
if (previousValue != null) {
return getSanitizedValue(createSanitizers(previousSession.getSaveInfoLocked()),
autofillId, previousValue);
}
}
}
return null;
}
@Nullable
private AutofillValue findValueFromThisSessionOnlyLocked(@NonNull AutofillId autofillId) {
final ViewState state = mViewStates.get(autofillId);
if (state == null) {
if (sDebug) Slog.d(TAG, "findValueLocked(): no view state for " + autofillId);
return null;
}
AutofillValue value = state.getCurrentValue();
if (value == null) {
if (sDebug) Slog.d(TAG, "findValueLocked(): no current value for " + autofillId);
value = getValueFromContextsLocked(autofillId);
}
return value;
}
/**
* 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
*/
@GuardedBy("mLock")
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,
"fillContextWithAllowedValuesLocked(): 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}.
*/
@GuardedBy("mLock")
private void cancelCurrentRequestLocked() {
if (mRemoteFillService == null) {
wtf(null, "cancelCurrentRequestLocked() called without a remote service. "
+ "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly);
return;
}
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.
*/
@GuardedBy("mLock")
private void requestNewFillResponseLocked(@NonNull ViewState viewState, int newState,
int flags) {
if (mForAugmentedAutofillOnly || mRemoteFillService == null) {
if (sVerbose) {
Slog.v(TAG, "requestNewFillResponse(): triggering augmented autofill instead "
+ "(mForAugmentedAutofillOnly=" + mForAugmentedAutofillOnly
+ ", flags=" + flags + ")");
}
mForAugmentedAutofillOnly = true;
triggerAugmentedAutofillLocked();
return;
}
viewState.setState(newState);
int requestId;
do {
requestId = sIdCounter.getAndIncrement();
} while (requestId == INVALID_REQUEST_ID);
// Create a metrics log for the request
final int ordinal = mRequestLogs.size() + 1;
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_REQUEST)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL, ordinal);
if (flags != 0) {
log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags);
}
mRequestLogs.put(requestId, log);
if (sVerbose) {
Slog.v(TAG, "Requesting structure for request #" + ordinal + " ,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 (!ActivityTaskManager.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 Handler handler, int userId, @NonNull Object lock,
int sessionId, int taskId, int uid, @NonNull IBinder activityToken,
@NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory,
@NonNull LocalLog wtfHistory, @Nullable ComponentName serviceComponentName,
@NonNull ComponentName componentName, boolean compatMode,
boolean bindInstantServiceAllowed, boolean forAugmentedAutofillOnly, int flags) {
if (sessionId < 0) {
wtf(null, "Non-positive sessionId: %s", sessionId);
}
id = sessionId;
mFlags = flags;
this.taskId = taskId;
this.uid = uid;
mStartTime = SystemClock.elapsedRealtime();
mService = service;
mLock = lock;
mUi = ui;
mHandler = handler;
mRemoteFillService = serviceComponentName == null ? null
: new RemoteFillService(context, serviceComponentName, userId, this,
bindInstantServiceAllowed);
mActivityToken = activityToken;
mHasCallback = hasCallback;
mUiLatencyHistory = uiLatencyHistory;
mWtfHistory = wtfHistory;
mComponentName = componentName;
mCompatMode = compatMode;
mForAugmentedAutofillOnly = forAugmentedAutofillOnly;
setClientLocked(client);
mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_STARTED)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_FLAGS, flags));
}
/**
* Gets the currently registered activity token
*
* @return The activity token
*/
@GuardedBy("mLock")
@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;
setClientLocked(newClient);
// The tracked id are not persisted in the client, hence update them
updateTrackedIdsLocked();
}
}
@GuardedBy("mLock")
private void setClientLocked(@NonNull IBinder client) {
unlinkClientVultureLocked();
mClient = IAutoFillManagerClient.Stub.asInterface(client);
mClientVulture = () -> {
Slog.d(TAG, "handling death of " + mActivityToken + " when saving=" + mIsSaving);
synchronized (mLock) {
if (mIsSaving) {
mUi.hideFillUi(this);
} else {
mUi.destroyAll(mPendingSaveUi, this, false);
}
}
};
try {
mClient.asBinder().linkToDeath(mClientVulture, 0);
} catch (RemoteException e) {
Slog.w(TAG, "could not set binder death listener on autofill client: " + e);
mClientVulture = null;
}
}
@GuardedBy("mLock")
private void unlinkClientVultureLocked() {
if (mClient != null && mClientVulture != null) {
final boolean unlinked = mClient.asBinder().unlinkToDeath(mClientVulture, 0);
if (!unlinked) {
Slog.w(TAG, "unlinking vulture from death failed for " + mActivityToken);
}
mClientVulture = null;
}
}
// FillServiceCallbacks
@Override
public void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
@NonNull String servicePackageName, int requestFlags) {
final AutofillId[] fieldClassificationIds;
final LogMaker requestLog;
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
+ id + " destroyed");
return;
}
requestLog = mRequestLogs.get(requestId);
if (requestLog != null) {
requestLog.setType(MetricsEvent.TYPE_SUCCESS);
} else {
Slog.w(TAG, "onFillRequestSuccess(): no request log for id " + requestId);
}
if (response == null) {
if (requestLog != null) {
requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, -1);
}
processNullResponseLocked(requestId, requestFlags);
return;
}
fieldClassificationIds = response.getFieldClassificationIds();
if (fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) {
Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
processNullResponseLocked(requestId, requestFlags);
return;
}
}
mService.setLastResponse(id, response);
int sessionFinishedState = 0;
final long disableDuration = response.getDisableDuration();
if (disableDuration > 0) {
final int flags = response.getFlags();
if ((flags & FillResponse.FLAG_DISABLE_ACTIVITY_ONLY) != 0) {
mService.disableAutofillForActivity(mComponentName, disableDuration,
id, mCompatMode);
} else {
mService.disableAutofillForApp(mComponentName.getPackageName(), disableDuration,
id, mCompatMode);
}
// Although "standard" autofill is disabled, it might still trigger augmented autofill
if (triggerAugmentedAutofillLocked() != null) {
mForAugmentedAutofillOnly = true;
if (sDebug) {
Slog.d(TAG, "Service disabled autofill for " + mComponentName
+ ", but session is kept for augmented autofill only");
}
return;
}
if (sDebug) {
final StringBuilder message = new StringBuilder("Service disabled autofill for ")
.append(mComponentName)
.append(": flags=").append(flags)
.append(", duration=");
TimeUtils.formatDuration(disableDuration, message);
Slog.d(TAG, message.toString());
}
sessionFinishedState = AutofillManager.STATE_DISABLED_BY_SERVICE;
}
if (((response.getDatasets() == null || response.getDatasets().isEmpty())
&& response.getAuthentication() == null)
|| disableDuration > 0) {
// Response is "empty" from an UI point of view, need to notify client.
notifyUnavailableToClient(sessionFinishedState, /* autofillableIds= */ null);
}
if (requestLog != null) {
requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
response.getDatasets() == null ? 0 : response.getDatasets().size());
if (fieldClassificationIds != null) {
requestLog.addTaggedData(
MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS,
fieldClassificationIds.length);
}
}
synchronized (mLock) {
processResponseLocked(response, null, requestFlags);
}
}
// FillServiceCallbacks
@Override
public void onFillRequestFailure(int requestId, @Nullable CharSequence message) {
onFillRequestFailureOrTimeout(requestId, false, message);
}
// FillServiceCallbacks
@Override
public void onFillRequestTimeout(int requestId) {
onFillRequestFailureOrTimeout(requestId, true, null);
}
private void onFillRequestFailureOrTimeout(int requestId, boolean timedOut,
@Nullable CharSequence message) {
boolean showMessage = !TextUtils.isEmpty(message);
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestFailureOrTimeout(req=" + requestId
+ ") rejected - session: " + id + " destroyed");
return;
}
if (sDebug) {
Slog.d(TAG, "finishing session due to service "
+ (timedOut ? "timeout" : "failure"));
}
mService.resetLastResponse();
final LogMaker requestLog = mRequestLogs.get(requestId);
if (requestLog == null) {
Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId);
} else {
requestLog.setType(timedOut ? MetricsEvent.TYPE_CLOSE : MetricsEvent.TYPE_FAILURE);
}
if (showMessage) {
final int targetSdk = mService.getTargedSdkLocked();
if (targetSdk >= Build.VERSION_CODES.Q) {
showMessage = false;
Slog.w(TAG, "onFillRequestFailureOrTimeout(): not showing '" + message
+ "' because service's targetting API " + targetSdk);
}
if (message != null) {
requestLog.addTaggedData(MetricsEvent.FIELD_AUTOFILL_TEXT_LEN,
message.length());
}
}
}
notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED,
/* autofillableIds= */ null);
if (showMessage) {
getUiForShowing().showError(message, this);
}
removeSelf();
}
// FillServiceCallbacks
@Override
public void onSaveRequestSuccess(@NonNull String servicePackageName,
@Nullable IntentSender intentSender) {
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(intentSender == null ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_OPEN);
mMetricsLogger.write(log);
if (intentSender != null) {
if (sDebug) Slog.d(TAG, "Starting intent sender on save()");
startIntentSender(intentSender);
}
// Nothing left to do...
removeSelf();
}
// FillServiceCallbacks
@Override
public void onSaveRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName) {
boolean showMessage = !TextUtils.isEmpty(message);
synchronized (mLock) {
mIsSaving = false;
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onSaveRequestFailure() rejected - session: "
+ id + " destroyed");
return;
}
if (showMessage) {
final int targetSdk = mService.getTargedSdkLocked();
if (targetSdk >= Build.VERSION_CODES.Q) {
showMessage = false;
Slog.w(TAG, "onSaveRequestFailure(): not showing '" + message
+ "' because service's targetting API " + targetSdk);
}
}
}
final LogMaker log =
newLogMaker(MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST, servicePackageName)
.setType(MetricsEvent.TYPE_FAILURE);
if (message != null) {
log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_TEXT_LEN, message.length());
}
mMetricsLogger.write(log);
if (showMessage) {
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
*/
@GuardedBy("mLock")
@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, mClientState);
final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex);
mHandler.sendMessage(obtainMessage(
Session::startAuthentication,
this, authenticationId, intent, fillInIntent));
}
// VultureCallback
@Override
public void onServiceDied(@NonNull RemoteFillService service) {
Slog.w(TAG, "removing session because service died");
forceRemoveSelfLocked();
}
// 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;
}
}
mHandler.sendMessage(obtainMessage(
Session::autoFill,
this, 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;
}
}
mHandler.sendMessage(obtainMessage(
AutofillManagerServiceImpl::handleSessionSave,
mService, this));
}
// AutoFillUiCallback
@Override
public void cancelSave() {
synchronized (mLock) {
mIsSaving = false;
if (mDestroyed) {
Slog.w(TAG, "Call to Session#cancelSave() rejected - session: "
+ id + " destroyed");
return;
}
}
mHandler.sendMessage(obtainMessage(
Session::removeSelf, this));
}
// 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");
}
}
}
}
@Override
public void dispatchUnhandledKey(AutofillId id, KeyEvent keyEvent) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#dispatchUnhandledKey() rejected - session: "
+ id + " destroyed");
return;
}
if (id.equals(mCurrentViewId)) {
try {
mClient.dispatchUnhandledKey(this.id, id, keyEvent);
} catch (RemoteException e) {
Slog.e(TAG, "Error requesting to dispatch unhandled key", e);
}
} else {
Slog.w(TAG, "Do not dispatch unhandled key 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();
}
mHandler.sendMessage(obtainMessage(
Session::doStartIntentSender,
this, intentSender));
}
private void doStartIntentSender(IntentSender intentSender) {
try {
synchronized (mLock) {
mClient.startIntentSender(intentSender, null);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error launching auth intent", e);
}
}
@GuardedBy("mLock")
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) {
Slog.w(TAG, "no authenticated response");
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) {
Slog.w(TAG, "no dataset with index " + datasetIdx + " on fill response");
removeSelf();
return;
}
}
final Parcelable result = data.getParcelable(AutofillManager.EXTRA_AUTHENTICATION_RESULT);
final Bundle newClientState = data.getBundle(AutofillManager.EXTRA_CLIENT_STATE);
if (sDebug) {
Slog.d(TAG, "setAuthenticationResultLocked(): result=" + result
+ ", clientState=" + newClientState + ", authenticationId=" + authenticationId);
}
if (result instanceof FillResponse) {
logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_AUTHENTICATED);
replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
} else if (result instanceof Dataset) {
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
logAuthenticationStatusLocked(requestId,
MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED);
if (newClientState != null) {
if (sDebug) Slog.d(TAG, "Updating client state from auth dataset");
mClientState = newClientState;
}
final Dataset dataset = (Dataset) result;
authenticatedResponse.getDatasets().set(datasetIdx, dataset);
autoFill(requestId, datasetIdx, dataset, false);
} else {
Slog.w(TAG, "invalid index (" + datasetIdx + ") for authentication id "
+ authenticationId);
logAuthenticationStatusLocked(requestId,
MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION);
}
} else {
if (result != null) {
Slog.w(TAG, "service returned invalid auth type: " + result);
}
logAuthenticationStatusLocked(requestId,
MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION);
processNullResponseLocked(requestId, 0);
}
}
@GuardedBy("mLock")
void setHasCallbackLocked(boolean hasIt) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#setHasCallbackLocked() rejected - session: "
+ id + " destroyed");
return;
}
mHasCallback = hasIt;
}
@GuardedBy("mLock")
@Nullable
private FillResponse getLastResponseLocked(@Nullable String logPrefixFmt) {
final String logPrefix = sDebug && logPrefixFmt != null
? String.format(logPrefixFmt, this.id)
: null;
if (mContexts == null) {
if (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;
}
@GuardedBy("mLock")
@Nullable
private SaveInfo getSaveInfoLocked() {
final FillResponse response = getLastResponseLocked(null);
return response == null ? null : response.getSaveInfo();
}
@GuardedBy("mLock")
int getSaveInfoFlagsLocked() {
final SaveInfo saveInfo = getSaveInfoLocked();
return saveInfo == null ? 0 : saveInfo.getFlags();
}
/**
* Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
* when necessary.
*/
public void logContextCommitted() {
mHandler.sendMessage(obtainMessage(Session::handleLogContextCommitted, this));
}
private void handleLogContextCommitted() {
final FillResponse lastResponse;
synchronized (mLock) {
lastResponse = getLastResponseLocked("logContextCommited(%s)");
}
if (lastResponse == null) {
Slog.w(TAG, "handleLogContextCommitted(): last response is null");
return;
}
// Merge UserData if necessary.
// Fields in packageUserData will override corresponding fields in genericUserData.
final UserData genericUserData = mService.getUserData();
final UserData packageUserData = lastResponse.getUserData();
final FieldClassificationUserData userData;
if (packageUserData == null && genericUserData == null) {
userData = null;
} else if (packageUserData != null && genericUserData != null) {
userData = new CompositeUserData(genericUserData, packageUserData);
} else if (packageUserData != null) {
userData = packageUserData;
} else {
userData = mService.getUserData();
}
final FieldClassificationStrategy fcStrategy = mService.getFieldClassificationStrategy();
// Sets field classification scores
if (userData != null && fcStrategy != null) {
logFieldClassificationScore(fcStrategy, userData);
} else {
logContextCommitted(null, null);
}
}
private void logContextCommitted(@Nullable ArrayList<AutofillId> detectedFieldIds,
@Nullable ArrayList<FieldClassification> detectedFieldClassifications) {
synchronized (mLock) {
logContextCommittedLocked(detectedFieldIds, detectedFieldClassifications);
}
}
@GuardedBy("mLock")
private void logContextCommittedLocked(@Nullable ArrayList<AutofillId> detectedFieldIds,
@Nullable ArrayList<FieldClassification> detectedFieldClassifications) {
final FillResponse lastResponse = getLastResponseLocked("logContextCommited(%s)");
if (lastResponse == null) return;
final int flags = lastResponse.getFlags();
if ((flags & FillResponse.FLAG_TRACK_CONTEXT_COMMITED) == 0) {
if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): ignored by flags " + flags);
return;
}
ArraySet<String> ignoredDatasets = null;
ArrayList<AutofillId> changedFieldIds = null;
ArrayList<String> changedDatasetIds = null;
ArrayMap<AutofillId, ArraySet<String>> manuallyFilledIds = null;
boolean hasAtLeastOneDataset = false;
final int responseCount = mResponses.size();
for (int i = 0; i < responseCount; i++) {
final FillResponse response = mResponses.valueAt(i);
final List<Dataset> datasets = response.getDatasets();
if (datasets == null || datasets.isEmpty()) {
if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
} else {
for (int j = 0; j < datasets.size(); j++) {
final Dataset dataset = datasets.get(j);
final String datasetId = dataset.getId();
if (datasetId == null) {
if (sVerbose) {
Slog.v(TAG, "logContextCommitted() skipping idless dataset " + dataset);
}
} else {
hasAtLeastOneDataset = true;
if (mSelectedDatasetIds == null
|| !mSelectedDatasetIds.contains(datasetId)) {
if (sVerbose) Slog.v(TAG, "adding ignored dataset " + datasetId);
if (ignoredDatasets == null) {
ignoredDatasets = new ArraySet<>();
}
ignoredDatasets.add(datasetId);
}
}
}
}
}
final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
if (sVerbose) {
Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
+ "classification ids)");
}
return;
}
for (int i = 0; i < mViewStates.size(); i++) {
final ViewState viewState = mViewStates.valueAt(i);
final int state = viewState.getState();
// When value changed, we need to log if it was:
// - autofilled -> changedDatasetIds
// - not autofilled but matches a dataset value -> manuallyFilledIds
if ((state & ViewState.STATE_CHANGED) != 0) {
// Check if autofilled value was changed
if ((state & ViewState.STATE_AUTOFILLED_ONCE) != 0) {
final String datasetId = viewState.getDatasetId();
if (datasetId == null) {
// Sanity check - should never happen.
Slog.w(TAG, "logContextCommitted(): no dataset id on " + viewState);
continue;
}
// Must first check if final changed value is not the same as value sent by
// service.
final AutofillValue autofilledValue = viewState.getAutofilledValue();
final AutofillValue currentValue = viewState.getCurrentValue();
if (autofilledValue != null && autofilledValue.equals(currentValue)) {
if (sDebug) {
Slog.d(TAG, "logContextCommitted(): ignoring changed " + viewState
+ " because it has same value that was autofilled");
}
continue;
}
if (sDebug) {
Slog.d(TAG, "logContextCommitted() found changed state: " + viewState);
}
if (changedFieldIds == null) {
changedFieldIds = new ArrayList<>();
changedDatasetIds = new ArrayList<>();
}
changedFieldIds.add(viewState.id);
changedDatasetIds.add(datasetId);
} else {
final AutofillValue currentValue = viewState.getCurrentValue();
if (currentValue == null) {
if (sDebug) {
Slog.d(TAG, "logContextCommitted(): skipping view without current "
+ "value ( " + viewState + ")");
}
continue;
}
// Check if value match a dataset.
if (hasAtLeastOneDataset) {
for (int j = 0; j < responseCount; j++) {
final FillResponse response = mResponses.valueAt(j);
final List<Dataset> datasets = response.getDatasets();
if (datasets == null || datasets.isEmpty()) {
if (sVerbose) {
Slog.v(TAG, "logContextCommitted() no datasets at " + j);
}
} else {
for (int k = 0; k < datasets.size(); k++) {
final Dataset dataset = datasets.get(k);
final String datasetId = dataset.getId();
if (datasetId == null) {
if (sVerbose) {
Slog.v(TAG, "logContextCommitted() skipping idless "
+ "dataset " + dataset);
}
} else {
final ArrayList<AutofillValue> values =
dataset.getFieldValues();
for (int l = 0; l < values.size(); l++) {
final AutofillValue candidate = values.get(l);
if (currentValue.equals(candidate)) {
if (sDebug) {
Slog.d(TAG, "field " + viewState.id + " was "
+ "manually filled with value set by "
+ "dataset " + datasetId);
}
if (manuallyFilledIds == null) {
manuallyFilledIds = new ArrayMap<>();
}
ArraySet<String> datasetIds =
manuallyFilledIds.get(viewState.id);
if (datasetIds == null) {
datasetIds = new ArraySet<>(1);
manuallyFilledIds.put(viewState.id, datasetIds);
}
datasetIds.add(datasetId);
}
} // for l
if (mSelectedDatasetIds == null
|| !mSelectedDatasetIds.contains(datasetId)) {
if (sVerbose) {
Slog.v(TAG, "adding ignored dataset " + datasetId);
}
if (ignoredDatasets == null) {
ignoredDatasets = new ArraySet<>();
}
ignoredDatasets.add(datasetId);
} // if
} // if
} // for k
} // else
} // for j
}
} // else
} // else
}
ArrayList<AutofillId> manuallyFilledFieldIds = null;
ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null;
// Must "flatten" the map to the parcelable collection primitives
if (manuallyFilledIds != null) {
final int size = manuallyFilledIds.size();
manuallyFilledFieldIds = new ArrayList<>(size);
manuallyFilledDatasetIds = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
final AutofillId fieldId = manuallyFilledIds.keyAt(i);
final ArraySet<String> datasetIds = manuallyFilledIds.valueAt(i);
manuallyFilledFieldIds.add(fieldId);
manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds));
}
}
mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
ignoredDatasets, changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds,
detectedFieldClassifications, mComponentName, mCompatMode);
}
/**
* Adds the matches to {@code detectedFieldsIds} and {@code detectedFieldClassifications} for
* {@code fieldId} based on its {@code currentValue} and {@code userData}.
*/
private void logFieldClassificationScore(@NonNull FieldClassificationStrategy fcStrategy,
@NonNull FieldClassificationUserData userData) {
final String[] userValues = userData.getValues();
final String[] categoryIds = userData.getCategoryIds();
final String defaultAlgorithm = userData.getFieldClassificationAlgorithm();
final Bundle defaultArgs = userData.getDefaultFieldClassificationArgs();
final ArrayMap<String, String> algorithms = userData.getFieldClassificationAlgorithms();
final ArrayMap<String, Bundle> args = userData.getFieldClassificationArgs();
// Sanity check
if (userValues == null || categoryIds == null || userValues.length != categoryIds.length) {
final int valuesLength = userValues == null ? -1 : userValues.length;
final int idsLength = categoryIds == null ? -1 : categoryIds.length;
Slog.w(TAG, "setScores(): user data mismatch: values.length = "
+ valuesLength + ", ids.length = " + idsLength);
return;
}
final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize();
final ArrayList<AutofillId> detectedFieldIds = new ArrayList<>(maxFieldsSize);
final ArrayList<FieldClassification> detectedFieldClassifications = new ArrayList<>(
maxFieldsSize);
final Collection<ViewState> viewStates;
synchronized (mLock) {
viewStates = mViewStates.values();
}
final int viewsSize = viewStates.size();
// First, we get all scores.
final AutofillId[] autofillIds = new AutofillId[viewsSize];
final ArrayList<AutofillValue> currentValues = new ArrayList<>(viewsSize);
int k = 0;
for (ViewState viewState : viewStates) {
currentValues.add(viewState.getCurrentValue());
autofillIds[k++] = viewState.id;
}
// Then use the results, asynchronously
final RemoteCallback callback = new RemoteCallback((result) -> {
if (result == null) {
if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results");
logContextCommitted(null, null);
return;
}
final Scores scores = result.getParcelable(EXTRA_SCORES);
if (scores == null) {
Slog.w(TAG, "No field classification score on " + result);
return;
}
int i = 0, j = 0;
try {
// Iteract over all autofill fields first
for (i = 0; i < viewsSize; i++) {
final AutofillId autofillId = autofillIds[i];
// Search the best scores for each category (as some categories could have
// multiple user values
ArrayMap<String, Float> scoresByField = null;
for (j = 0; j < userValues.length; j++) {
final String categoryId = categoryIds[j];
final float score = scores.scores[i][j];
if (score > 0) {
if (scoresByField == null) {
scoresByField = new ArrayMap<>(userValues.length);
}
final Float currentScore = scoresByField.get(categoryId);
if (currentScore != null && currentScore > score) {
if (sVerbose) {
Slog.v(TAG, "skipping score " + score
+ " because it's less than " + currentScore);
}
continue;
}
if (sVerbose) {
Slog.v(TAG, "adding score " + score + " at index " + j + " and id "
+ autofillId);
}
scoresByField.put(categoryId, score);
} else if (sVerbose) {
Slog.v(TAG, "skipping score 0 at index " + j + " and id " + autofillId);
}
}
if (scoresByField == null) {
if (sVerbose) Slog.v(TAG, "no score for autofillId=" + autofillId);
continue;
}
// Then create the matches for that autofill id
final ArrayList<Match> matches = new ArrayList<>(scoresByField.size());
for (j = 0; j < scoresByField.size(); j++) {
final String fieldId = scoresByField.keyAt(j);
final float score = scoresByField.valueAt(j);
matches.add(new Match(fieldId, score));
}
detectedFieldIds.add(autofillId);
detectedFieldClassifications.add(new FieldClassification(matches));
} // for i
} catch (ArrayIndexOutOfBoundsException e) {
wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e);
return;
}
logContextCommitted(detectedFieldIds, detectedFieldClassifications);
});
fcStrategy.calculateScores(callback, currentValues, userValues, categoryIds,
defaultAlgorithm, defaultArgs, algorithms, args);
}
/**
* Shows the save UI, when session can be saved.
*
* @return {@code true} if session is done and could be removed, or {@code false} if it's
* pending user action or the service asked to keep it alive (for multi-screens workflow).
*/
@GuardedBy("mLock")
public boolean showSaveLocked() {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#showSaveLocked() rejected - session: "
+ id + " destroyed");
return false;
}
final FillResponse response = getLastResponseLocked("showSaveLocked(%s)");
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.
* - server didn't ask to keep session alive
*/
if (saveInfo == null) {
if (sVerbose) Slog.v(TAG, "showSaveLocked(" + this.id + "): no saveInfo from service");
return true;
}
if ((saveInfo.getFlags() & SaveInfo.FLAG_DELAY_SAVE) != 0) {
// TODO(b/113281366): log metrics
if (sDebug) Slog.v(TAG, "showSaveLocked(" + this.id + "): service asked to delay save");
return false;
}
final ArrayMap<AutofillId, InternalSanitizer> sanitizers = createSanitizers(saveInfo);
// Cache used to make sure changed fields do not belong to a dataset.
final ArrayMap<AutofillId, AutofillValue> currentValues = new ArrayMap<>();
// Savable (optional or required) ids that will be checked against the dataset ids.
final ArraySet<AutofillId> savableIds = new ArraySet<>();
final AutofillId[] requiredIds = saveInfo.getRequiredIds();
boolean allRequiredAreNotEmpty = true;
boolean atLeastOneChanged = false;
// If an autofilled field is changed, we need to change isUpdate to true so the proper UI is
// shown.
boolean isUpdate = 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;
}
savableIds.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;
}
}
value = getSanitizedValue(sanitizers, id, value);
if (value == null) {
if (sDebug) {
Slog.d(TAG, "value of required field " + id + " failed sanitization");
}
allRequiredAreNotEmpty = false;
break;
}
viewState.setSanitizedValue(value);
currentValues.put(id, value);
final AutofillValue filledValue = viewState.getAutofilledValue();
if (!value.equals(filledValue)) {
boolean changed = true;
if (filledValue == null) {
// Dataset was not autofilled, make sure initial value didn't change.
final AutofillValue initialValue = getValueFromContextsLocked(id);
if (initialValue != null && initialValue.equals(value)) {
if (sDebug) {
Slog.d(TAG, "id " + id + " is part of dataset but initial value "
+ "didn't change: " + value);
}
changed = false;
}
} else {
isUpdate = true;
}
if (changed) {
if (sDebug) {
Slog.d(TAG, "found a change on required " + id + ": " + filledValue
+ " => " + value);
}
atLeastOneChanged = true;
}
}
}
}
final AutofillId[] optionalIds = saveInfo.getOptionalIds();
if (sVerbose) {
Slog.v(TAG, "allRequiredAreNotEmpty: " + allRequiredAreNotEmpty + " hasOptional: "
+ (optionalIds != null));
}
if (allRequiredAreNotEmpty) {
// Must look up all optional ids in 2 scenarios:
// - if no required id changed but an optional id did, it should trigger save / update
// - if at least one required id changed but it was not part of a filled dataset, we
// need to check if an optional id is part of a filled datased (in which case we show
// Update instead of Save)
if (optionalIds!= null && (!atLeastOneChanged || !isUpdate)) {
// 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];
savableIds.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();
final AutofillValue value = getSanitizedValue(sanitizers, id, currentValue);
if (value == null) {
if (sDebug) {
Slog.d(TAG, "value of opt. field " + id + " failed sanitization");
}
continue;
}
currentValues.put(id, value);
final AutofillValue filledValue = viewState.getAutofilledValue();
if (value != null && !value.equals(filledValue)) {
if (sDebug) {
Slog.d(TAG, "found a change on optional " + id + ": " + filledValue
+ " => " + value);
}
if (filledValue != null) {
isUpdate = true;
}
atLeastOneChanged = true;
}
} 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 InternalValidator validator = saveInfo.getValidator();
if (validator != null) {
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SAVE_VALIDATION);
boolean isValid;
try {
isValid = validator.isValid(this);
if (sDebug) Slog.d(TAG, validator + " returned " + isValid);
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<Dataset> datasets = response.getDatasets();
if (datasets != null) {
datasets_loop: for (int i = 0; i < datasets.size(); i++) {
final Dataset dataset = datasets.get(i);
final ArrayMap<AutofillId, AutofillValue> datasetValues =
Helper.getFields(dataset);
if (sVerbose) {
Slog.v(TAG, "Checking if saved fields match contents of dataset #" + i
+ ": " + dataset + "; savableIds=" + savableIds);
}
savable_ids_loop: for (int j = 0; j < savableIds.size(); j++) {
final AutofillId id = savableIds.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 savable_ids_loop;
}
final AutofillValue datasetValue = datasetValues.get(id);
if (!currentValue.equals(datasetValue)) {
if (sDebug) {
Slog.d(TAG, "found a dataset change on id " + id + ": from "
+ datasetValue + " to " + currentValue);
}
continue datasets_loop;
}
if (sVerbose) Slog.v(TAG, "no dataset 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 for "
+ id + "!");
}
// Use handler so logContextCommitted() is logged first
mHandler.sendMessage(obtainMessage(Session::logSaveShown, this));
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
final CharSequence serviceLabel;
final Drawable serviceIcon;
synchronized (mLock) {
serviceLabel = mService.getServiceLabelLocked();
serviceIcon = mService.getServiceIconLocked();
}
if (serviceLabel == null || serviceIcon == null) {
wtf(null, "showSaveLocked(): no service label or icon");
return true;
}
getUiForShowing().showSaveUi(serviceLabel, serviceIcon,
mService.getServicePackageName(), saveInfo, this,
mComponentName, this, mPendingSaveUi, isUpdate, mCompatMode);
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(" + id +"): with no changes, comes no responsibilities."
+ "allRequiredAreNotNull=" + allRequiredAreNotEmpty
+ ", atLeastOneChanged=" + atLeastOneChanged);
}
return true;
}
private void logSaveShown() {
mService.logSaveShown(id, mClientState);
}
@Nullable
private ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) {
if (saveInfo == null) return null;
final InternalSanitizer[] sanitizerKeys = saveInfo.getSanitizerKeys();
if (sanitizerKeys == null) return null;
final int size = sanitizerKeys.length ;
final ArrayMap<AutofillId, InternalSanitizer> sanitizers = new ArrayMap<>(size);
if (sDebug) Slog.d(TAG, "Service provided " + size + " sanitizers");
final AutofillId[][] sanitizerValues = saveInfo.getSanitizerValues();
for (int i = 0; i < size; i++) {
final InternalSanitizer sanitizer = sanitizerKeys[i];
final AutofillId[] ids = sanitizerValues[i];
if (sDebug) {
Slog.d(TAG, "sanitizer #" + i + " (" + sanitizer + ") for ids "
+ Arrays.toString(ids));
}
for (AutofillId id : ids) {
sanitizers.put(id, sanitizer);
}
}
return sanitizers;
}
@Nullable
private AutofillValue getSanitizedValue(
@Nullable ArrayMap<AutofillId, InternalSanitizer> sanitizers,
@NonNull AutofillId id,
@Nullable AutofillValue value) {
if (sanitizers == null || value == null) return value;
final ViewState state = mViewStates.get(id);
AutofillValue sanitized = state == null ? null : state.getSanitizedValue();
if (sanitized == null) {
final InternalSanitizer sanitizer = sanitizers.get(id);
if (sanitizer == null) {
return value;
}
sanitized = sanitizer.sanitize(value);
if (sDebug) {
Slog.d(TAG, "Value for " + id + "(" + value + ") sanitized to " + sanitized);
}
if (state != null) {
state.setSanitizedValue(sanitized);
}
}
return sanitized;
}
/**
* Returns whether the session is currently showing the save UI
*/
@GuardedBy("mLock")
boolean isSavingLocked() {
return mIsSaving;
}
/**
* Gets the latest non-empty value for the given id in the autofill contexts.
*/
@GuardedBy("mLock")
@Nullable
private AutofillValue getValueFromContextsLocked(@NonNull AutofillId autofillId) {
final int numContexts = mContexts.size();
for (int i = numContexts - 1; i >= 0; i--) {
final FillContext context = mContexts.get(i);
final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(),
autofillId);
if (node != null) {
final AutofillValue value = node.getAutofillValue();
if (sDebug) {
Slog.d(TAG, "getValueFromContexts(" + this.id + "/" + autofillId + ") 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.
*/
@GuardedBy("mLock")
@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 = Helper.findViewNodeByAutofillId(context.getStructure(), id);
if (node != null && node.getAutofillOptions() != null) {
return node.getAutofillOptions();
}
}
return null;
}
/**
* Update the {@link AutofillValue values} of the {@link AssistStructure} before sending it to
* the service on save().
*/
private void updateValuesForSaveLocked() {
final ArrayMap<AutofillId, InternalSanitizer> sanitizers =
createSanitizers(getSaveInfoLocked());
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, "updateValuesForSaveLocked(): updating " + context);
for (int viewStateNum = 0; viewStateNum < mViewStates.size(); viewStateNum++) {
final ViewState viewState = mViewStates.valueAt(viewStateNum);
final AutofillId id = viewState.id;
final AutofillValue value = viewState.getCurrentValue();
if (value == null) {
if (sVerbose) Slog.v(TAG, "updateValuesForSaveLocked(): 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, "updateValuesForSaveLocked(): updating " + id + " to " + value);
}
AutofillValue sanitizedValue = viewState.getSanitizedValue();
if (sanitizedValue == null) {
// Field is optional and haven't been sanitized yet.
sanitizedValue = getSanitizedValue(sanitizers, id, value);
}
if (sanitizedValue != null) {
node.updateAutofillValue(sanitizedValue);
} else if (sDebug) {
Slog.d(TAG, "updateValuesForSaveLocked(): not updating field " + id
+ " because it failed sanitization");
}
}
// Sanitize structure before it's sent to service.
context.getStructure().sanitizeForParceling(false);
if (sVerbose) {
Slog.v(TAG, "updateValuesForSaveLocked(): dumping structure of " + context
+ " before calling service.save()");
context.getStructure().dump(false);
}
}
}
/**
* Calls service when user requested save.
*/
@GuardedBy("mLock")
void callSaveLocked() {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#callSaveLocked() rejected - session: "
+ id + " destroyed");
return;
}
if (mRemoteFillService == null) {
wtf(null, "callSaveLocked() called without a remote service. "
+ "mForAugmentedAutofillOnly: %s", mForAugmentedAutofillOnly);
return;
}
if (sVerbose) Slog.v(TAG, "callSaveLocked(" + this.id + "): mViewStates=" + mViewStates);
if (mContexts == null) {
Slog.w(TAG, "callSaveLocked(): no contexts");
return;
}
updateValuesForSaveLocked();
// Remove pending fill requests as the session is finished.
cancelCurrentRequestLocked();
final ArrayList<FillContext> contexts = mergePreviousSessionLocked( /* forSave= */ true);
final SaveRequest saveRequest =
new SaveRequest(contexts, mClientState, mSelectedDatasetIds);
mRemoteFillService.onSaveRequest(saveRequest);
}
// TODO(b/113281366): rather than merge it here, it might be better to simply reuse the old
// session instead of creating a new one. But we need to consider what would happen on corner
// cases such as "Main Activity M -> activity A with username -> activity B with password"
// If user follows the normal workflow, than session A would be merged with session B as
// expected. But if when on Activity A the user taps back or somehow launches another activity,
// session A could be merged with the wrong session.
/**
* Gets a list of contexts that includes not only this session's contexts but also the contexts
* from previous sessions that were asked by the service to be delayed (if any).
*
* <p>As a side-effect:
* <ul>
* <li>If the current {@link #mClientState} is {@code null}, sets it with the last non-
* {@code null} client state from previous sessions.
* <li>When {@code forSave} is {@code true}, calls {@link #updateValuesForSaveLocked()} in the
* previous sessions.
* </ul>
*/
@NonNull
private ArrayList<FillContext> mergePreviousSessionLocked(boolean forSave) {
final ArrayList<Session> previousSessions = mService.getPreviousSessionsLocked(this);
final ArrayList<FillContext> contexts;
if (previousSessions != null) {
if (sDebug) {
Slog.d(TAG, "mergeSessions(" + this.id + "): Merging the content of "
+ previousSessions.size() + " sessions for task " + taskId);
}
contexts = new ArrayList<>();
for (int i = 0; i < previousSessions.size(); i++) {
final Session previousSession = previousSessions.get(i);
final ArrayList<FillContext> previousContexts = previousSession.mContexts;
if (previousContexts == null) {
Slog.w(TAG, "mergeSessions(" + this.id + "): Not merging null contexts from "
+ previousSession.id);
continue;
}
if (forSave) {
previousSession.updateValuesForSaveLocked();
}
if (sDebug) {
Slog.d(TAG, "mergeSessions(" + this.id + "): adding " + previousContexts.size()
+ " context from previous session #" + previousSession.id);
}
contexts.addAll(previousContexts);
if (mClientState == null && previousSession.mClientState != null) {
if (sDebug) {
Slog.d(TAG, "mergeSessions(" + this.id + "): setting client state from "
+ "previous session" + previousSession.id);
}
mClientState = previousSession.mClientState;
}
}
contexts.addAll(mContexts);
} else {
// 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.
contexts = new ArrayList<>(mContexts);
}
return contexts;
}
/**
* Starts (if necessary) a new fill request upon entering a view.
*
* <p>A new request will be started in 2 scenarios:
* <ol>
* <li>If the user manually requested autofill.
* <li>If the view is part of a new partition.
* </ol>
*
* @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.
*/
@GuardedBy("mLock")
private void requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id,
@NonNull ViewState viewState, int flags) {
if ((flags & FLAG_MANUAL_REQUEST) != 0) {
mForAugmentedAutofillOnly = false;
if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags);
requestNewFillResponseLocked(viewState, ViewState.STATE_RESTARTED_SESSION, 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 or augmented request for view id " + id + ": "
+ viewState.getStateAsString());
}
requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_PARTITION, flags);
} else {
if (sVerbose) {
Slog.v(TAG, "Not starting new partition for view " + id + ": "
+ viewState.getStateAsString());
}
}
}
/**
* 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
*/
@GuardedBy("mLock")
private boolean shouldStartNewPartitionLocked(@NonNull AutofillId id) {
if (mResponses == null) {
return true;
}
final int numResponses = mResponses.size();
if (numResponses >= AutofillManagerService.getPartitionMaxCount()) {
Slog.e(TAG, "Not starting a new partition on " + id + " because session " + this.id
+ " reached maximum of " + AutofillManagerService.getPartitionMaxCount());
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<Dataset> datasets = response.getDatasets();
if (datasets != null) {
final int numDatasets = datasets.size();
for (int dataSetNum = 0; dataSetNum < numDatasets; dataSetNum++) {
final ArrayList<AutofillId> fields = datasets.get(dataSetNum).getFieldIds();
if (fields != null && fields.contains(id)) {
return false;
}
}
}
if (ArrayUtils.contains(response.getAuthenticationIds(), id)) {
return false;
}
}
return true;
}
@GuardedBy("mLock")
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;
}
id.setSessionId(this.id);
if (sVerbose) {
Slog.v(TAG, "updateLocked(" + this.id + "): id=" + id + ", action="
+ actionAsString(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);
boolean isIgnored = isIgnoredLocked(id);
viewState = new ViewState(id, this,
isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL);
mViewStates.put(id, viewState);
// TODO(b/73648631): for optimization purposes, should also ignore if change is
// detectable, and batch-send them when the session is finished (but that will
// require tracking detectable fields on AutofillManager)
if (isIgnored) {
if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + viewState);
return;
}
} else {
if (sVerbose) Slog.v(TAG, "Ignoring specific action when viewState=null");
return;
}
}
switch(action) {
case ACTION_START_SESSION:
// View is triggering autofill.
mCurrentViewId = viewState.id;
viewState.update(value, virtualBounds, flags);
requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_SESSION, flags);
break;
case ACTION_VALUE_CHANGED:
if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
// Must cancel the session if the value of the URL bar changed
final String currentUrl = mUrlBar == null ? null
: mUrlBar.getText().toString().trim();
if (currentUrl == null) {
// Sanity check - shouldn't happen.
wtf(null, "URL bar value changed, but current value is null");
return;
}
if (value == null || ! value.isText()) {
// Sanity check - shouldn't happen.
wtf(null, "URL bar value changed to null or non-text: %s", value);
return;
}
final String newUrl = value.getTextValue().toString();
if (newUrl.equals(currentUrl)) {
if (sDebug) Slog.d(TAG, "Ignoring change on URL bar as it's the same");
return;
}
if (mSaveOnAllViewsInvisible) {
// We cannot cancel the session because it could hinder Save when all views
// are finished, as the URL bar changed callback is usually called before
// the virtual views become invisible.
if (sDebug) {
Slog.d(TAG, "Ignoring change on URL because session will finish when "
+ "views are gone");
}
return;
}
if (sDebug) Slog.d(TAG, "Finishing session because URL bar changed");
forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN_COMPAT_MODE);
return;
}
if (!Objects.equals(value, viewState.getCurrentValue())) {
if ((value == null || 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 (filledValue != null) {
if (filledValue.equals(value)) {
if (sVerbose) {
Slog.v(TAG, "ignoring autofilled change on id " + id);
}
viewState.resetState(ViewState.STATE_CHANGED);
return;
}
else {
if ((viewState.id.equals(this.mCurrentViewId)) &&
(viewState.getState() & ViewState.STATE_AUTOFILLED) != 0) {
// Remove autofilled state once field is changed after autofilling.
if (sVerbose) {
Slog.v(TAG, "field changed after autofill on id " + id);
}
viewState.resetState(ViewState.STATE_AUTOFILLED);
final ViewState currentView = mViewStates.get(mCurrentViewId);
currentView.maybeCallOnFillReady(flags);
}
}
}
// Update the internal state...
viewState.setState(ViewState.STATE_CHANGED);
//..and the UI
final String filterText;
if (value == null || !value.isText()) {
filterText = null;
} else {
final CharSequence text = value.getTextValue();
// Text should never be null, but it doesn't hurt to check to avoid a
// system crash...
filterText = (text == null) ? null : text.toString();
}
getUiForShowing().filterFillUi(filterText, this);
}
break;
case ACTION_VIEW_ENTERED:
if (sVerbose && virtualBounds != null) {
Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds);
}
// Update the view states first...
mCurrentViewId = viewState.id;
viewState.setCurrentValue(value);
if (mCompatMode && (viewState.getState() & ViewState.STATE_URL_BAR) != 0) {
if (sDebug) Slog.d(TAG, "Ignoring VIEW_ENTERED on URL BAR (id=" + id + ")");
return;
}
if ((flags & FLAG_MANUAL_REQUEST) == 0 && mAugmentedAutofillableIds != null
&& mAugmentedAutofillableIds.contains(id)) {
// View was already reported when server could not handle a response, but it
// triggered augmented autofill
if (sDebug) Slog.d(TAG, "updateLocked(" + id + "): augmented-autofillable");
// ...then trigger the augmented autofill UI
triggerAugmentedAutofillLocked();
return;
}
requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags);
// Remove the UI if the ViewState has changed.
if (!Objects.equals(mCurrentViewId, viewState.id)) {
mUi.hideFillUi(this);
mCurrentViewId = viewState.id;
hideAugmentedAutofillLocked(viewState);
}
// If the ViewState is ready to be displayed, onReady() will be called.
viewState.update(value, virtualBounds, flags);
break;
case ACTION_VIEW_EXITED:
if (Objects.equals(mCurrentViewId, viewState.id)) {
if (sVerbose) Slog.v(TAG, "Exiting view " + id);
mUi.hideFillUi(this);
hideAugmentedAutofillLocked(viewState);
mCurrentViewId = null;
}
break;
default:
Slog.w(TAG, "updateLocked(): unknown action: " + action);
}
}
@GuardedBy("mLock")
private void hideAugmentedAutofillLocked(@NonNull ViewState viewState) {
if ((viewState.getState()
& ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) {
viewState.resetState(ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL);
cancelAugmentedAutofillLocked();
}
}
/**
* Checks whether a view should be ignored.
*/
@GuardedBy("mLock")
private boolean isIgnoredLocked(AutofillId id) {
// Always check the latest response only
final FillResponse response = getLastResponseLocked(null);
if (response == null) return false;
return ArrayUtils.contains(response.getIgnoredIds(), id);
}
@Override
public void onFillReady(@NonNull FillResponse response, @NonNull 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();
}
final CharSequence serviceLabel;
final Drawable serviceIcon;
synchronized (mLock) {
serviceLabel = mService.getServiceLabelLocked();
serviceIcon = mService.getServiceIconLocked();
}
if (serviceLabel == null || serviceIcon == null) {
wtf(null, "onFillReady(): no service label or icon");
return;
}
getUiForShowing().showFillUi(filledId, response, filterText,
mService.getServicePackageName(), mComponentName,
serviceLabel, serviceIcon, this, id, mCompatMode);
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());
addTaggedDataToRequestLogLocked(response.getRequestId(),
MetricsEvent.FIELD_AUTOFILL_DURATION, duration);
}
}
}
boolean isDestroyed() {
synchronized (mLock) {
return mDestroyed;
}
}
IAutoFillManagerClient getClient() {
synchronized (mLock) {
return mClient;
}
}
private void notifyUnavailableToClient(int sessionFinishedState,
@Nullable ArrayList<AutofillId> autofillableIds) {
synchronized (mLock) {
if (mCurrentViewId == null) return;
try {
if (mHasCallback) {
mClient.notifyNoFillUi(id, mCurrentViewId, sessionFinishedState);
} else if (sessionFinishedState != AutofillManager.STATE_UNKNOWN) {
mClient.setSessionFinished(sessionFinishedState, autofillableIds);
}
} catch (RemoteException e) {
Slog.e(TAG, "Error notifying client no fill UI: id=" + mCurrentViewId, e);
}
}
}
@GuardedBy("mLock")
private void updateTrackedIdsLocked() {
// Only track the views of the last response as only those are reported back to the
// service, see #showSaveLocked
final FillResponse response = getLastResponseLocked(null);
if (response == null) return;
ArraySet<AutofillId> trackedViews = null;
mSaveOnAllViewsInvisible = false;
boolean saveOnFinish = true;
final SaveInfo saveInfo = response.getSaveInfo();
final AutofillId saveTriggerId;
final int flags;
if (saveInfo != null) {
saveTriggerId = saveInfo.getTriggerId();
if (saveTriggerId != null) {
writeLog(MetricsEvent.AUTOFILL_EXPLICIT_SAVE_TRIGGER_DEFINITION);
}
flags = saveInfo.getFlags();
mSaveOnAllViewsInvisible = (flags & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0;
// We only need to track views if we want to save once they become invisible.
if (mSaveOnAllViewsInvisible) {
if (trackedViews == null) {
trackedViews = new ArraySet<>();
}
if (saveInfo.getRequiredIds() != null) {
Collections.addAll(trackedViews, saveInfo.getRequiredIds());
}
if (saveInfo.getOptionalIds() != null) {
Collections.addAll(trackedViews, saveInfo.getOptionalIds());
}
}
if ((flags & SaveInfo.FLAG_DONT_SAVE_ON_FINISH) != 0) {
saveOnFinish = false;
}
} else {
flags = 0;
saveTriggerId = null;
}
// 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<Dataset> datasets = response.getDatasets();
ArraySet<AutofillId> fillableIds = null;
if (datasets != null) {
for (int i = 0; i < datasets.size(); i++) {
final Dataset dataset = datasets.get(i);
final ArrayList<AutofillId> 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
+ " triggerId: " + saveTriggerId + " saveOnFinish:" + saveOnFinish
+ " flags: " + flags + " hasSaveInfo: " + (saveInfo != null));
}
mClient.setTrackedViews(id, toArray(trackedViews), mSaveOnAllViewsInvisible,
saveOnFinish, toArray(fillableIds), saveTriggerId);
} catch (RemoteException e) {
Slog.w(TAG, "Cannot set tracked ids", e);
}
}
/**
* Sets the state of views that failed to autofill.
*/
@GuardedBy("mLock")
void setAutofillFailureLocked(@NonNull List<AutofillId> ids) {
for (int i = 0; i < ids.size(); i++) {
final AutofillId id = ids.get(i);
final ViewState viewState = mViewStates.get(id);
if (viewState == null) {
Slog.w(TAG, "setAutofillFailure(): no view for id " + id);
continue;
}
viewState.resetState(ViewState.STATE_AUTOFILLED);
final int state = viewState.getState();
viewState.setState(state | ViewState.STATE_AUTOFILL_FAILED);
if (sVerbose) {
Slog.v(TAG, "Changed state of " + id + " to " + viewState.getStateAsString());
}
}
}
@GuardedBy("mLock")
private void replaceResponseLocked(@NonNull FillResponse oldResponse,
@NonNull FillResponse newResponse, @Nullable Bundle newClientState) {
// 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, newClientState, 0);
}
@GuardedBy("mLock")
private void processNullResponseLocked(int requestId, int flags) {
if ((flags & FLAG_MANUAL_REQUEST) != 0) {
getUiForShowing().showError(R.string.autofill_error_cannot_autofill, this);
}
final FillContext context = getFillContextByRequestIdLocked(requestId);
final ArrayList<AutofillId> autofillableIds;
if (context != null) {
final AssistStructure structure = context.getStructure();
autofillableIds = Helper.getAutofillIds(structure, /* autofillableOnly= */true);
} else {
Slog.w(TAG, "processNullResponseLocked(): no context for req " + requestId);
autofillableIds = null;
}
mService.resetLastResponse();
// The default autofill service cannot fullfill the request, let's check if the augmented
// autofill service can.
mAugmentedAutofillDestroyer = triggerAugmentedAutofillLocked();
if (mAugmentedAutofillDestroyer == null) {
if (sVerbose) {
Slog.v(TAG, "canceling session " + id + " when service returned null and it cannot "
+ "be augmented. AutofillableIds: " + autofillableIds);
}
// Nothing to be done, but need to notify client.
notifyUnavailableToClient(AutofillManager.STATE_FINISHED, autofillableIds);
removeSelf();
} else {
if (sVerbose) {
Slog.v(TAG, "keeping session " + id + " when service returned null but "
+ "it can be augmented. AutofillableIds: " + autofillableIds);
}
mAugmentedAutofillableIds = autofillableIds;
try {
mClient.setState(AutofillManager.SET_STATE_FLAG_FOR_AUTOFILL_ONLY);
} catch (RemoteException e) {
Slog.e(TAG, "Error setting client to autofill-only", e);
}
}
}
/**
* Tries to trigger Augmented Autofill when the standard service could not fulfill a request.
*
* @return callback to destroy the autofill UI, or {@code null} if not supported.
*/
// TODO(b/123099468): might need to call it in other places, like when the service returns a
// non-null response but without datasets (for example, just SaveInfo)
@GuardedBy("mLock")
private Runnable triggerAugmentedAutofillLocked() {
// Check if Smart Suggestions is supported...
final @SmartSuggestionMode int supportedModes = mService
.getSupportedSmartSuggestionModesLocked();
if (supportedModes == 0) {
if (sVerbose) Slog.v(TAG, "triggerAugmentedAutofillLocked(): no supported modes");
return null;
}
// ...then if the service is set for the user
final RemoteAugmentedAutofillService remoteService = mService
.getRemoteAugmentedAutofillServiceLocked();
if (remoteService == null) {
if (sVerbose) Slog.v(TAG, "triggerAugmentedAutofillLocked(): no service for user");
return null;
}
// Define which mode will be used
final int mode;
if ((supportedModes & FLAG_SMART_SUGGESTION_SYSTEM) != 0) {
mode = FLAG_SMART_SUGGESTION_SYSTEM;
} else {
Slog.w(TAG, "Unsupported Smart Suggestion mode: " + supportedModes);
return null;
}
if (mCurrentViewId == null) {
Slog.w(TAG, "triggerAugmentedAutofillLocked(): no view currently focused");
return null;
}
final boolean isWhitelisted = mService
.isWhitelistedForAugmentedAutofillLocked(mComponentName);
final String historyItem =
"aug:id=" + id + " u=" + uid + " m=" + mode
+ " a=" + ComponentName.flattenToShortString(mComponentName)
+ " f=" + mCurrentViewId
+ " s=" + remoteService.getComponentName()
+ " w=" + isWhitelisted;
mService.getMaster().logRequestLocked(historyItem);
if (!isWhitelisted) {
if (sVerbose) {
Slog.v(TAG, "triggerAugmentedAutofillLocked(): "
+ ComponentName.flattenToShortString(mComponentName) + " not whitelisted ");
}
return null;
}
if (sVerbose) {
Slog.v(TAG, "calling Augmented Autofill Service ("
+ ComponentName.flattenToShortString(remoteService.getComponentName())
+ ") on view " + mCurrentViewId + " using suggestion mode "
+ getSmartSuggestionModeToString(mode)
+ " when server returned null for session " + this.id);
}
final ViewState viewState = mViewStates.get(mCurrentViewId);
viewState.setState(ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL);
final AutofillValue currentValue = viewState.getCurrentValue();
if (mAugmentedRequestsLogs == null) {
mAugmentedRequestsLogs = new ArrayList<>();
}
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_AUGMENTED_REQUEST,
remoteService.getComponentName().getPackageName());
mAugmentedRequestsLogs.add(log);
final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId);
remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, focusedId,
currentValue);
if (mAugmentedAutofillDestroyer == null) {
mAugmentedAutofillDestroyer = () -> remoteService.onDestroyAutofillWindowsRequest();
}
return mAugmentedAutofillDestroyer;
}
@GuardedBy("mLock")
private void cancelAugmentedAutofillLocked() {
final RemoteAugmentedAutofillService remoteService = mService
.getRemoteAugmentedAutofillServiceLocked();
if (remoteService == null) {
Slog.w(TAG, "cancelAugmentedAutofillLocked(): no service for user");
return;
}
if (sVerbose) Slog.v(TAG, "cancelAugmentedAutofillLocked() on " + mCurrentViewId);
remoteService.onDestroyAutofillWindowsRequest();
}
@GuardedBy("mLock")
private void processResponseLocked(@NonNull FillResponse newResponse,
@Nullable Bundle newClientState, 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
+ ",newClientState=" + newClientState);
}
if (mResponses == null) {
// Set initial capacity as 2 to handle cases where service always requires auth.
// TODO: add a metric for number of responses set by server, so we can use its average
// as the initial array capacitiy.
mResponses = new SparseArray<>(2);
}
mResponses.put(requestId, newResponse);
mClientState = newClientState != null ? newClientState : 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.
*/
@GuardedBy("mLock")
private void setViewStatesLocked(FillResponse response, int state, boolean clearResponse) {
final List<Dataset> 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.
*/
@GuardedBy("mLock")
private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset,
int state, boolean clearResponse) {
final ArrayList<AutofillId> ids = dataset.getFieldIds();
final ArrayList<AutofillValue> 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);
final String datasetId = dataset.getId();
if (datasetId != null) {
viewState.setDatasetId(datasetId);
}
if (response != null) {
viewState.setResponse(response);
} else if (clearResponse) {
viewState.setResponse(null);
}
}
}
@GuardedBy("mLock")
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(id, this, state);
if (sVerbose) {
Slog.v(TAG, "Adding autofillable view with id " + id + " and state " + state);
}
viewState.setCurrentValue(findValueLocked(id));
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, mClientState);
}
autoFillApp(dataset);
return;
}
// ...or handle authentication.
mService.logDatasetAuthenticationSelected(dataset.getId(), id, mClientState);
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);
}
}
// TODO: this should never be null, but we got at least one occurrence, probably due to a race.
@GuardedBy("mLock")
@Nullable
private Intent createAuthFillInIntentLocked(int requestId, Bundle extras) {
final Intent fillInIntent = new Intent();
final FillContext context = getFillContextByRequestIdLocked(requestId);
if (context == null) {
wtf(null, "createAuthFillInIntentLocked(): no FillContext. requestId=%d; mContexts=%s",
requestId, 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 + ", component=" + mComponentName + "]";
}
@GuardedBy("mLock")
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("taskId: "); pw.println(taskId);
pw.print(prefix); pw.print("flags: "); pw.println(mFlags);
pw.print(prefix); pw.print("mComponentName: "); pw.println(mComponentName);
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();
}
final int requestLogsSizes = mRequestLogs.size();
pw.print(prefix); pw.print("mSessionLogs: "); pw.println(requestLogsSizes);
for (int i = 0; i < requestLogsSizes; i++) {
final int requestId = mRequestLogs.keyAt(i);
final LogMaker log = mRequestLogs.valueAt(i);
pw.print(prefix2); pw.print('#'); pw.print(i); pw.print(": req=");
pw.print(requestId); pw.print(", log=" ); dumpRequestLog(pw, log); 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("mDestroyed: "); pw.println(mDestroyed);
pw.print(prefix); pw.print("mIsSaving: "); pw.println(mIsSaving);
pw.print(prefix); pw.print("mPendingSaveUi: "); pw.println(mPendingSaveUi);
final int numberViews = mViewStates.size();
pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size());
for (int i = 0; i < numberViews; i++) {
pw.print(prefix); pw.print("ViewState at #"); pw.println(i);
mViewStates.valueAt(i).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("AssistStructure dumped 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);
if (mClientState != null) {
pw.print(prefix); pw.print("mClientState: "); pw.print(mClientState.getSize()); pw
.println(" bytes");
}
pw.print(prefix); pw.print("mCompatMode: "); pw.println(mCompatMode);
pw.print(prefix); pw.print("mUrlBar: ");
if (mUrlBar == null) {
pw.println("N/A");
} else {
pw.print("id="); pw.print(mUrlBar.getAutofillId());
pw.print(" domain="); pw.print(mUrlBar.getWebDomain());
pw.print(" text="); Helper.printlnRedactedText(pw, mUrlBar.getText());
}
pw.print(prefix); pw.print("mSaveOnAllViewsInvisible: "); pw.println(
mSaveOnAllViewsInvisible);
pw.print(prefix); pw.print("mSelectedDatasetIds: "); pw.println(mSelectedDatasetIds);
if (mForAugmentedAutofillOnly) {
pw.print(prefix); pw.println("For Augmented Autofill Only");
}
if (mAugmentedAutofillDestroyer != null) {
pw.print(prefix); pw.println("has mAugmentedAutofillDestroyer");
}
if (mAugmentedRequestsLogs != null) {
pw.print(prefix); pw.print("number augmented requests: ");
pw.println(mAugmentedRequestsLogs.size());
}
if (mAugmentedAutofillableIds != null) {
pw.print(prefix); pw.print("mAugmentedAutofillableIds: ");
pw.println(mAugmentedAutofillableIds);
}
if (mRemoteFillService != null) {
mRemoteFillService.dump(prefix, pw);
}
}
private static void dumpRequestLog(@NonNull PrintWriter pw, @NonNull LogMaker log) {
pw.print("CAT="); pw.print(log.getCategory());
pw.print(", TYPE=");
final int type = log.getType();
switch (type) {
case MetricsEvent.TYPE_SUCCESS: pw.print("SUCCESS"); break;
case MetricsEvent.TYPE_FAILURE: pw.print("FAILURE"); break;
case MetricsEvent.TYPE_CLOSE: pw.print("CLOSE"); break;
default: pw.print("UNSUPPORTED");
}
pw.print('('); pw.print(type); pw.print(')');
pw.print(", PKG="); pw.print(log.getPackageName());
pw.print(", SERVICE="); pw.print(log
.getTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE));
pw.print(", ORDINAL="); pw.print(log
.getTaggedData(MetricsEvent.FIELD_AUTOFILL_REQUEST_ORDINAL));
dumpNumericValue(pw, log, "FLAGS", MetricsEvent.FIELD_AUTOFILL_FLAGS);
dumpNumericValue(pw, log, "NUM_DATASETS", MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS);
dumpNumericValue(pw, log, "UI_LATENCY", MetricsEvent.FIELD_AUTOFILL_DURATION);
final int authStatus =
getNumericValue(log, MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS);
if (authStatus != 0) {
pw.print(", AUTH_STATUS=");
switch (authStatus) {
case MetricsEvent.AUTOFILL_AUTHENTICATED:
pw.print("AUTHENTICATED"); break;
case MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED:
pw.print("DATASET_AUTHENTICATED"); break;
case MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION:
pw.print("INVALID_AUTHENTICATION"); break;
case MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION:
pw.print("INVALID_DATASET_AUTHENTICATION"); break;
default: pw.print("UNSUPPORTED");
}
pw.print('('); pw.print(authStatus); pw.print(')');
}
dumpNumericValue(pw, log, "FC_IDS",
MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS);
dumpNumericValue(pw, log, "COMPAT_MODE",
MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE);
}
private static void dumpNumericValue(@NonNull PrintWriter pw, @NonNull LogMaker log,
@NonNull String field, int tag) {
final int value = getNumericValue(log, tag);
if (value != 0) {
pw.print(", "); pw.print(field); pw.print('='); pw.print(value);
}
}
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<AutofillId> ids = new ArrayList<>(entryCount);
final List<AutofillValue> 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);
if (dataset.getId() != null) {
if (mSelectedDatasetIds == null) {
mSelectedDatasetIds = new ArrayList<>();
}
mSelectedDatasetIds.add(dataset.getId());
}
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.
*
* <p>Typically called in 2 scenarios:
*
* <ul>
* <li>When the session naturally finishes (i.e., from {@link #removeSelfLocked()}.
* <li>When the service hosting the session is finished (for example, because the user
* disabled it).
* </ul>
*/
@GuardedBy("mLock")
RemoteFillService destroyLocked() {
if (mDestroyed) {
return null;
}
unlinkClientVultureLocked();
mUi.destroyAll(mPendingSaveUi, this, true);
mUi.clearCallback(this);
mDestroyed = true;
// Log metrics
final int totalRequests = mRequestLogs.size();
if (totalRequests > 0) {
if (sVerbose) Slog.v(TAG, "destroyLocked(): logging " + totalRequests + " requests");
for (int i = 0; i < totalRequests; i++) {
final LogMaker log = mRequestLogs.valueAt(i);
mMetricsLogger.write(log);
}
}
final int totalAugmentedRequests = mAugmentedRequestsLogs == null ? 0
: mAugmentedRequestsLogs.size();
if (totalAugmentedRequests > 0) {
if (sVerbose) {
Slog.v(TAG, "destroyLocked(): logging " + totalRequests + " augmented requests");
}
for (int i = 0; i < totalAugmentedRequests; i++) {
final LogMaker log = mAugmentedRequestsLogs.get(i);
mMetricsLogger.write(log);
}
}
final LogMaker log = newLogMaker(MetricsEvent.AUTOFILL_SESSION_FINISHED)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_REQUESTS, totalRequests);
if (totalAugmentedRequests > 0) {
log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_AUGMENTED_REQUESTS,
totalAugmentedRequests);
}
if (mForAugmentedAutofillOnly) {
log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_AUGMENTED_ONLY, 1);
}
mMetricsLogger.write(log);
return mRemoteFillService;
}
/**
* Cleans up this session and remove it from the service always, even if it does have a pending
* Save UI.
*/
@GuardedBy("mLock")
void forceRemoveSelfLocked() {
forceRemoveSelfLocked(AutofillManager.STATE_UNKNOWN);
}
@GuardedBy("mLock")
void forceRemoveSelfIfForAugmentedAutofillOnlyLocked() {
if (sVerbose) {
Slog.v(TAG, "forceRemoveSelfIfForAugmentedAutofillOnly(" + this.id + "): "
+ mForAugmentedAutofillOnly);
}
if (!mForAugmentedAutofillOnly) return;
forceRemoveSelfLocked();
}
@GuardedBy("mLock")
void forceRemoveSelfLocked(int clientState) {
if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi);
final boolean isPendingSaveUi = isSaveUiPendingLocked();
mPendingSaveUi = null;
removeSelfLocked();
mUi.destroyAll(mPendingSaveUi, this, false);
if (!isPendingSaveUi) {
try {
mClient.setSessionFinished(clientState, /* autofillableIds= */ null);
} catch (RemoteException e) {
Slog.e(TAG, "Error notifying client to finish session", e);
}
}
destroyAugmentedAutofillWindowsLocked();
}
@GuardedBy("mLock")
void destroyAugmentedAutofillWindowsLocked() {
if (mAugmentedAutofillDestroyer != null) {
mAugmentedAutofillDestroyer.run();
mAugmentedAutofillDestroyer = null;
}
}
/**
* 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.
*/
@GuardedBy("mLock")
void removeSelfLocked() {
if (sVerbose) Slog.v(TAG, "removeSelfLocked(" + this.id + "): " + 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)}.
*/
@GuardedBy("mLock")
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.
*/
@GuardedBy("mLock")
private boolean isSaveUiPendingLocked() {
return mPendingSaveUi != null && mPendingSaveUi.getState() == PendingUi.STATE_PENDING;
}
@GuardedBy("mLock")
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, mComponentName, servicePackageName, id, mCompatMode);
}
private void writeLog(int category) {
mMetricsLogger.write(newLogMaker(category));
}
@GuardedBy("mLock")
private void logAuthenticationStatusLocked(int requestId, int status) {
addTaggedDataToRequestLogLocked(requestId,
MetricsEvent.FIELD_AUTOFILL_AUTHENTICATION_STATUS, status);
}
@GuardedBy("mLock")
private void addTaggedDataToRequestLogLocked(int requestId, int tag, @Nullable Object value) {
final LogMaker requestLog = mRequestLogs.get(requestId);
if (requestLog == null) {
Slog.w(TAG,
"addTaggedDataToRequestLogLocked(tag=" + tag + "): no log for id " + requestId);
return;
}
requestLog.addTaggedData(tag, value);
}
private void wtf(@Nullable Exception e, String fmt, Object...args) {
final String message = String.format(fmt, args);
synchronized (mLock) {
mWtfHistory.log(message);
}
if (e != null) {
Slog.wtf(TAG, message, e);
} else {
Slog.wtf(TAG, message);
}
}
private static String actionAsString(int action) {
switch (action) {
case ACTION_START_SESSION:
return "START_SESSION";
case ACTION_VIEW_ENTERED:
return "VIEW_ENTERED";
case ACTION_VIEW_EXITED:
return "VIEW_EXITED";
case ACTION_VALUE_CHANGED:
return "VALUE_CHANGED";
default:
return "UNKNOWN_" + action;
}
}
}