Merge "Moved Session and ViewState to its own classes."
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 4d78350..3d1c251 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -19,15 +19,10 @@
import static android.service.autofill.AutofillService.EXTRA_ACTIVITY_TOKEN;
import static android.service.voice.VoiceInteractionSession.KEY_RECEIVER_EXTRAS;
import static android.service.voice.VoiceInteractionSession.KEY_STRUCTURE;
-import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED;
-import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED;
import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
-import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED;
-import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST;
import static com.android.server.autofill.Helper.DEBUG;
import static com.android.server.autofill.Helper.VERBOSE;
-import static com.android.server.autofill.Helper.findValue;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -35,32 +30,23 @@
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.assist.AssistStructure;
-import android.app.assist.AssistStructure.ViewNode;
-import android.app.assist.AssistStructure.WindowNode;
import android.content.ComponentName;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.graphics.Rect;
-import android.metrics.LogMaker;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
-import android.os.Parcelable;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
-import android.service.autofill.Dataset;
-import android.service.autofill.FillResponse;
import android.service.autofill.IAutoFillService;
-import android.service.autofill.SaveInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.LocalLog;
@@ -68,21 +54,16 @@
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.view.autofill.AutofillId;
-import android.view.autofill.AutofillManager;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutoFillManagerClient;
-import android.view.autofill.IAutofillWindowPresenter;
+
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.IResultReceiver;
import com.android.server.autofill.ui.AutoFillUI;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Map;
-import java.util.Map.Entry;
/**
* Bridge between the {@code system_server}'s {@link AutofillManagerService} and the
@@ -93,13 +74,12 @@
private static final String TAG = "AutofillManagerServiceImpl";
- private static final int MSG_SERVICE_SAVE = 1;
+ static final int MSG_SERVICE_SAVE = 1;
private final int mUserId;
private final Context mContext;
private final Object mLock;
private final AutoFillUI mUi;
- private final MetricsLogger mMetricsLogger = new MetricsLogger();
private RemoteCallbackList<IAutoFillManagerClient> mClients;
private AutofillServiceInfo mInfo;
@@ -358,8 +338,9 @@
private Session createSessionByTokenLocked(@NonNull IBinder activityToken,
@Nullable IBinder windowToken, @NonNull IBinder appCallbackToken, boolean hasCallback,
int flags, @NonNull String packageName) {
- final Session newSession = new Session(mContext, activityToken,
- windowToken, appCallbackToken, hasCallback, flags, packageName);
+ final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
+ activityToken, windowToken, appCallbackToken, hasCallback, flags,
+ mInfo.getServiceInfo().getComponentName(), packageName);
mSessions.put(activityToken, newSession);
/*
@@ -400,6 +381,10 @@
session.updateLocked(autofillId, virtualBounds, value, flags);
}
+ void removeSessionLocked(IBinder activityToken) {
+ mSessions.remove(activityToken);
+ }
+
private void handleSessionSave(IBinder activityToken) {
synchronized (mLock) {
final Session session = mSessions.get(activityToken);
@@ -423,6 +408,25 @@
mSessions.clear();
}
+ void disableSelf() {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final String autoFillService = Settings.Secure.getStringForUser(
+ mContext.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, mUserId);
+ if (mInfo.getServiceInfo().getComponentName().equals(
+ ComponentName.unflattenFromString(autoFillService))) {
+ Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ CharSequence getServiceLabel() {
+ return mInfo.getServiceInfo().loadLabel(mContext.getPackageManager());
+ }
+
void dumpLocked(String prefix, PrintWriter pw) {
final String prefix2 = prefix + " ";
@@ -495,804 +499,4 @@
+ ", component=" + (mInfo != null
? mInfo.getServiceInfo().getComponentName() : null) + "]";
}
-
- /**
- * State for a given view with a AutofillId.
- *
- * <p>This class holds state about a view and calls its listener when the fill UI is ready to
- * be displayed for the view.
- */
- static final class ViewState {
- interface Listener {
- /**
- * Called when the fill UI is ready to be shown for this view.
- */
- void onFillReady(FillResponse fillResponse, AutofillId focusedId,
- @Nullable AutofillValue value);
- }
-
- final AutofillId mId;
- private final Listener mListener;
- // TODO(b/33197203): would not need a reference to response and session if it was an inner
- // class of Session...
- private final Session mSession;
- // TODO(b/33197203): encapsulate access so it's not called by UI
- FillResponse mResponse;
- Intent mAuthIntent;
-
- private AutofillValue mAutofillValue;
-
- // Bounds if a virtual view, null otherwise
- private Rect mVirtualBounds;
-
- private boolean mValueUpdated;
-
- ViewState(Session session, AutofillId id, Listener listener) {
- mSession = session;
- mId = id;
- mListener = listener;
- }
-
- /**
- * Response should only be set once.
- */
- void setResponse(FillResponse response) {
- mResponse = response;
- maybeCallOnFillReady();
- }
-
- /**
- * Used when a {@link FillResponse} requires authentication to be unlocked.
- */
- void setResponse(FillResponse response, Intent authIntent) {
- mAuthIntent = authIntent;
- setResponse(response);
- }
-
- CharSequence getServiceName() {
- return mSession.getServiceName();
- }
-
- // TODO(b/33197203): need to refactor / rename / document this method to make it clear that
- // it can change the value and update the UI; similarly, should replace code that
- // directly sets mAutoFilLValue to use encapsulation.
- void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) {
- if (autofillValue != null) {
- mAutofillValue = autofillValue;
- }
- if (virtualBounds != null) {
- mVirtualBounds = virtualBounds;
- }
-
- maybeCallOnFillReady();
- }
-
- /**
- * Calls {@link
- * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the
- * fill UI is ready to be displayed (i.e. when response and bounds are set).
- */
- void maybeCallOnFillReady() {
- if (mResponse != null && (mResponse.getAuthentication() != null
- || mResponse.getDatasets() != null)) {
- mListener.onFillReady(mResponse, mId, mAutofillValue);
- }
- }
-
- @Override
- public String toString() {
- return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds
- + ", updated = " + mValueUpdated + "]";
- }
-
- void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.print("id:" ); pw.println(mId);
- pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue);
- pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
- pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds);
- pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
- }
- }
-
- /**
- * 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.
- */
- // TODO(b/33197203): make sure sessions are removed (and tested by CTS):
- // - On all authentication scenarios.
- // - When user does not interact back after a while.
- // - When service is unbound.
- final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
- AutoFillUI.AutoFillUiCallback {
- private final IBinder mActivityToken;
- private final IBinder mWindowToken;
-
- /** Package name of the app that is auto-filled */
- @NonNull private final String mPackageName;
-
- @GuardedBy("mLock")
- private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>();
-
- @GuardedBy("mLock")
- @Nullable
- private ViewState mCurrentViewState;
-
- private final IAutoFillManagerClient mClient;
-
- @GuardedBy("mLock")
- RemoteFillService mRemoteFillService;
-
- // TODO(b/33197203): Get a response per view instead of per activity.
- @GuardedBy("mLock")
- private FillResponse mCurrentResponse;
-
- /**
- * Used to remember which {@link Dataset} filled the session.
- */
- // TODO(b/33197203): might need more than one once we support partitions
- @GuardedBy("mLock")
- private Dataset mAutoFilledDataset;
-
- /**
- * Assist structure sent by the app; it will be updated (sanitized, change values for save)
- * before sent to {@link AutofillService}.
- */
- @GuardedBy("mLock")
- private AssistStructure mStructure;
-
- /**
- * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
- */
- private boolean mHasCallback;
-
- /**
- * Flags used to start the session.
- */
- private int mFlags;
- private Session(@NonNull Context context, @NonNull IBinder activityToken,
- @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback,
- int flags, @NonNull String packageName) {
- mRemoteFillService = new RemoteFillService(context,
- mInfo.getServiceInfo().getComponentName(), mUserId, this);
- mActivityToken = activityToken;
- mWindowToken = windowToken;
- mHasCallback = hasCallback;
- mFlags = flags;
- mPackageName = packageName;
-
- mClient = IAutoFillManagerClient.Stub.asInterface(client);
- try {
- client.linkToDeath(() -> {
- if (DEBUG) {
- Slog.d(TAG, "app binder died");
- }
-
- removeSelf();
- }, 0);
- } catch (RemoteException e) {
- Slog.w(TAG, "linkToDeath() on mClient failed: " + e);
- }
-
- mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName);
- }
-
- // FillServiceCallbacks
- @Override
- public void onFillRequestSuccess(@Nullable FillResponse response,
- @NonNull String servicePackageName) {
- if (response == null) {
- // Nothing to be done, but need to notify client.
- notifyUnavailableToClient();
- removeSelf();
- return;
- }
-
- if ((response.getDatasets() == null || response.getDatasets().isEmpty())
- && response.getAuthentication() == null) {
- // Response is "empty" from an UI point of view, need to notify client.
- notifyUnavailableToClient();
- }
- synchronized (mLock) {
- processResponseLocked(response);
- }
-
- LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST))
- .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS)
- .setPackageName(mPackageName)
- .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
- response.getDatasets() == null ? 0 : response.getDatasets().size())
- .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
- servicePackageName);
- mMetricsLogger.write(log);
- }
-
- // FillServiceCallbacks
- @Override
- public void onFillRequestFailure(@Nullable CharSequence message,
- @NonNull String servicePackageName) {
- LogMaker log = (new LogMaker(MetricsProto.MetricsEvent.AUTOFILL_REQUEST))
- .setType(MetricsProto.MetricsEvent.TYPE_FAILURE)
- .setPackageName(mPackageName)
- .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
- servicePackageName);
- mMetricsLogger.write(log);
-
- getUiForShowing().showError(message);
- removeSelf();
- }
-
- // FillServiceCallbacks
- @Override
- public void onSaveRequestSuccess(@NonNull String servicePackageName) {
- LogMaker log = (new LogMaker(
- MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
- .setType(MetricsProto.MetricsEvent.TYPE_SUCCESS)
- .setPackageName(mPackageName)
- .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
- servicePackageName);
- mMetricsLogger.write(log);
-
- // Nothing left to do...
- removeSelf();
- }
-
- // FillServiceCallbacks
- @Override
- public void onSaveRequestFailure(@Nullable CharSequence message,
- @NonNull String servicePackageName) {
- LogMaker log = (new LogMaker(
- MetricsProto.MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
- .setType(MetricsProto.MetricsEvent.TYPE_FAILURE)
- .setPackageName(mPackageName)
- .addTaggedData(MetricsProto.MetricsEvent.FIELD_AUTOFILL_SERVICE,
- servicePackageName);
- mMetricsLogger.write(log);
-
- getUiForShowing().showError(message);
- removeSelf();
- }
-
- // FillServiceCallbacks
- @Override
- public void authenticate(IntentSender intent) {
- final Intent fillInIntent;
- synchronized (mLock) {
- fillInIntent = createAuthFillInIntent(mStructure);
- }
- mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent));
- }
-
- // FillServiceCallbacks
- @Override
- public void onDisableSelf() {
- final long identity = Binder.clearCallingIdentity();
- try {
- final String autoFillService = Settings.Secure.getStringForUser(
- mContext.getContentResolver(),
- Settings.Secure.AUTOFILL_SERVICE, mUserId);
- if (mInfo.getServiceInfo().getComponentName().equals(
- ComponentName.unflattenFromString(autoFillService))) {
- Settings.Secure.putStringForUser(mContext.getContentResolver(),
- Settings.Secure.AUTOFILL_SERVICE, null, mUserId);
- }
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- synchronized (mLock) {
- removeSelfLocked();
- }
- }
-
- // FillServiceCallbacks
- @Override
- public void onServiceDied(RemoteFillService service) {
- // TODO(b/33197203): implement
- }
-
- // AutoFillUiCallback
- @Override
- public void fill(Dataset dataset) {
- mHandlerCaller.getHandler().post(() -> autoFill(dataset));
- }
-
- // AutoFillUiCallback
- @Override
- public void save() {
- mHandlerCaller.getHandler().obtainMessage(MSG_SERVICE_SAVE, mActivityToken)
- .sendToTarget();
- }
-
- // AutoFillUiCallback
- @Override
- public void cancelSave() {
- mHandlerCaller.getHandler().post(() -> removeSelf());
- }
-
- // AutoFillUiCallback
- @Override
- public void requestShowFillUi(AutofillId id, int width, int height,
- IAutofillWindowPresenter presenter) {
- try {
- mClient.requestShowFillUi(mWindowToken, id, width, height,
- mCurrentViewState.mVirtualBounds, presenter);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error requesting to show fill UI", e);
- }
- }
-
- // AutoFillUiCallback
- @Override
- public void requestHideFillUi(AutofillId id) {
- try {
- mClient.requestHideFillUi(mWindowToken, id);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error requesting to hide fill UI", e);
- }
- }
-
- public void setAuthenticationResultLocked(Bundle data) {
- if (mCurrentResponse == null || data == null) {
- removeSelf();
- } else {
- Parcelable result = data.getParcelable(
- AutofillManager.EXTRA_AUTHENTICATION_RESULT);
- if (result instanceof FillResponse) {
- mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_AUTHENTICATED,
- mPackageName);
-
- mCurrentResponse = (FillResponse) result;
- processResponseLocked(mCurrentResponse);
- } else if (result instanceof Dataset) {
- Dataset dataset = (Dataset) result;
- final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset);
- if (index >= 0) {
- mCurrentResponse.getDatasets().set(index, dataset);
- autoFill(dataset);
- }
- }
- }
- }
-
- public void setHasCallback(boolean hasIt) {
- mHasCallback = hasIt;
- }
-
- /**
- * Shows the save UI, when session can be saved.
- *
- * @return {@code true} if session is done, or {@code false} if it's pending user action.
- */
- public boolean showSaveLocked() {
- if (mStructure == null) {
- Slog.wtf(TAG, "showSaveLocked(): no mStructure");
- return true;
- }
- if (mCurrentResponse == null) {
- // Happens when the activity / session was finished before the service replied, or
- // when the service cannot autofill it (and returned a null response).
- if (DEBUG) {
- Slog.d(TAG, "showSaveLocked(): no mCurrentResponse");
- }
- return true;
- }
- final SaveInfo saveInfo = mCurrentResponse.getSaveInfo();
- if (DEBUG) {
- Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo);
- }
-
- /*
- * 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.
- */
-
- if (saveInfo == null) {
- return true;
- }
-
- final AutofillId[] requiredIds = saveInfo.getRequiredIds();
- if (requiredIds == null || requiredIds.length == 0) {
- Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo");
- return true;
- }
-
- boolean allRequiredAreNotEmpty = true;
- boolean atLeastOneChanged = false;
- for (int i = 0; i < requiredIds.length; i++) {
- final AutofillId id = requiredIds[i];
- final ViewState state = mViewStates.get(id);
- if (state == null || state.mAutofillValue == null
- || state.mAutofillValue.isEmpty()) {
- final ViewNode node = findViewNodeByIdLocked(id);
- if (node == null) {
- Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id);
- allRequiredAreNotEmpty = false;
- break;
- }
- final AutofillValue initialValue = node.getAutofillValue();
- if (initialValue == null || initialValue.isEmpty()) {
- if (DEBUG) {
- Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id );
- }
- allRequiredAreNotEmpty = false;
- break;
- }
- }
- if (state.mValueUpdated) {
- final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
- if (!state.mAutofillValue.equals(filledValue)) {
- if (DEBUG) {
- Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": "
- + filledValue + " => " + state.mAutofillValue);
- }
- atLeastOneChanged = true;
- }
- } else {
- if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) {
- if (DEBUG) {
- Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": "
- + state.mAutofillValue);
- }
- allRequiredAreNotEmpty = false;
- break;
-
- }
- }
- }
-
- if (allRequiredAreNotEmpty) {
- if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) {
- for (int i = 0; i < saveInfo.getOptionalIds().length; i++) {
- final AutofillId id = saveInfo.getOptionalIds()[i];
- final ViewState state = mViewStates.get(id);
- if (state != null && state.mAutofillValue != null && state.mValueUpdated) {
- final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
- if (!state.mAutofillValue.equals(filledValue)) {
- if (DEBUG) {
- Slog.d(TAG, "finishSessionLocked(): found a change on optional "
- + id + ": " + filledValue + " => "
- + state.mAutofillValue);
- }
- atLeastOneChanged = true;
- break;
- }
- }
- }
- }
- if (atLeastOneChanged) {
- getUiForShowing().showSaveUi(
- mInfo.getServiceInfo().loadLabel(mContext.getPackageManager()),
- saveInfo, mPackageName);
- return false;
- }
- }
- // Nothing changed...
- if (DEBUG) {
- Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities."
- + "allRequiredAreNotNull=" + allRequiredAreNotEmpty
- + ", atLeastOneChanged=" + atLeastOneChanged);
- }
- return true;
- }
-
- /**
- * Calls service when user requested save.
- */
- private void callSaveLocked() {
- if (DEBUG) {
- Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
- }
-
- final Bundle extras = this.mCurrentResponse.getExtras();
-
- for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
- final AutofillValue value = entry.getValue().mAutofillValue;
- if (value == null) {
- if (VERBOSE) {
- Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
- }
- continue;
- }
- final AutofillId id = entry.getKey();
- final ViewNode node = findViewNodeByIdLocked(id);
- if (node == null) {
- Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
- continue;
- }
- if (VERBOSE) {
- Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
- }
-
- node.updateAutofillValue(value);
- }
-
- // Sanitize structure before it's sent to service.
- mStructure.sanitizeForParceling(false);
-
- if (VERBOSE) {
- Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()");
- mStructure.dump();
- }
-
- mRemoteFillService.onSaveRequest(mStructure, extras);
- }
-
- void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) {
- if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) {
- // TODO(b/33197203): ignoring because we don't support partitions yet
- Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled");
- return;
- }
-
- ViewState viewState = mViewStates.get(id);
- if (viewState == null) {
- viewState = new ViewState(this, id, this);
- mViewStates.put(id, viewState);
- }
-
- if ((flags & FLAG_START_SESSION) != 0) {
- // View is triggering autofill.
- mCurrentViewState = viewState;
- viewState.update(value, virtualBounds);
- return;
- }
-
- if ((flags & FLAG_VALUE_CHANGED) != 0) {
- if (value != null && !value.equals(viewState.mAutofillValue)) {
- viewState.mValueUpdated = true;
-
- // Must check if this update was caused by autofilling the view, in which
- // case we just update the value, but not the UI.
- if (mAutoFilledDataset != null) {
- final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
- if (value.equals(filledValue)) {
- viewState.mAutofillValue = value;
- return;
- }
- }
-
- // Change value
- viewState.mAutofillValue = value;
-
- // Update the chooser UI
- if (value.isText()) {
- getUiForShowing().filterFillUi(value.getTextValue().toString());
- } else {
- getUiForShowing().filterFillUi(null);
- }
- }
-
- return;
- }
-
- if ((flags & FLAG_VIEW_ENTERED) != 0) {
- // Remove the UI if the ViewState has changed.
- if (mCurrentViewState != viewState) {
- mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null);
- mCurrentViewState = viewState;
- }
-
- // If the ViewState is ready to be displayed, onReady() will be called.
- viewState.update(value, virtualBounds);
-
- // TODO(b/33197203): Remove when there is a response per activity.
- if (mCurrentResponse != null) {
- viewState.setResponse(mCurrentResponse);
- }
-
- return;
- }
-
- if ((flags & FLAG_VIEW_EXITED) != 0) {
- if (mCurrentViewState == viewState) {
- mUi.hideFillUi(viewState.mId);
- mCurrentViewState = null;
- }
- return;
- }
-
- Slog.w(TAG, "updateLocked(): unknown flags " + flags);
- }
-
- @Override
- public void onFillReady(FillResponse response, AutofillId filledId,
- @Nullable AutofillValue value) {
- String filterText = null;
- if (value != null && value.isText()) {
- filterText = value.getTextValue().toString();
- }
-
- getUiForShowing().showFillUi(filledId, response, filterText, mPackageName);
- }
-
- private void notifyUnavailableToClient() {
- if (mCurrentViewState == null) {
- // TODO(b/33197203): temporary sanity check; should never happen
- Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null");
- return;
- }
- if (!mHasCallback) return;
- try {
- mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken
- + " id=" + mCurrentViewState.mId, e);
- }
- }
-
- private void processResponseLocked(FillResponse response) {
- if (DEBUG) {
- Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication()
- + "):" + response);
- }
-
- if (mCurrentViewState == null) {
- // TODO(b/33197203): temporary sanity check; should never happen
- Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null");
- return;
- }
-
- mCurrentResponse = response;
-
- if (mCurrentResponse.getAuthentication() != null) {
- // Handle authentication.
- final Intent fillInIntent = createAuthFillInIntent(mStructure);
- mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
- return;
- }
-
- if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null
- && response.getDatasets().size() == 1) {
- Slog.d(TAG, "autofilling manual request directly");
- autoFill(response.getDatasets().get(0));
- return;
- }
-
- mCurrentViewState.setResponse(mCurrentResponse);
- }
-
- void autoFill(Dataset dataset) {
- synchronized (mLock) {
- mAutoFilledDataset = dataset;
-
- // Autofill it directly...
- if (dataset.getAuthentication() == null) {
- autoFillApp(dataset);
- return;
- }
-
- // ...or handle authentication.
- Intent fillInIntent = createAuthFillInIntent(mStructure);
- startAuthentication(dataset.getAuthentication(), fillInIntent);
- }
- }
-
- CharSequence getServiceName() {
- return AutofillManagerServiceImpl.this.getServiceName();
- }
-
- private Intent createAuthFillInIntent(AssistStructure structure) {
- Intent fillInIntent = new Intent();
- fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure);
- return fillInIntent;
- }
-
- private void startAuthentication(IntentSender intent, Intent fillInIntent) {
- try {
- mClient.authenticate(intent, fillInIntent);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error launching auth intent", e);
- }
- }
-
- void dumpLocked(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
- pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags);
- pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse);
- pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset);
- pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState);
- pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size());
- final String prefix2 = prefix + " ";
- for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
- pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
- entry.getValue().dump(prefix2, pw);
- }
- if (VERBOSE) {
- pw.print(prefix); pw.print("mStructure: " );
- // TODO(b/33197203): add method do dump AssistStructure on pw
- if (mStructure != null) {
- pw.println("look at logcat" );
- mStructure.dump(); // dumps to logcat
- } else {
- pw.println("null");
- }
- }
- pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
- mRemoteFillService.dump(prefix, pw);
- }
-
- void autoFillApp(Dataset dataset) {
- synchronized (mLock) {
- try {
- if (DEBUG) {
- Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
- }
- mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues());
- } catch (RemoteException e) {
- Slog.w(TAG, "Error autofilling activity: " + e);
- }
- }
- }
-
- private AutoFillUI getUiForShowing() {
- synchronized (mLock) {
- mUi.setCallback(this);
- return mUi;
- }
- }
-
- private ViewNode findViewNodeByIdLocked(AutofillId id) {
- final int size = mStructure.getWindowNodeCount();
- for (int i = 0; i < size; i++) {
- final WindowNode window = mStructure.getWindowNodeAt(i);
- final ViewNode root = window.getRootViewNode();
- if (id.equals(root.getAutofillId())) {
- return root;
- }
- final ViewNode child = findViewNodeByIdLocked(root, id);
- if (child != null) {
- return child;
- }
- }
- return null;
- }
-
- private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) {
- final int childrenSize = parent.getChildCount();
- if (childrenSize > 0) {
- for (int i = 0; i < childrenSize; i++) {
- final ViewNode child = parent.getChildAt(i);
- if (id.equals(child.getAutofillId())) {
- return child;
- }
- final ViewNode grandChild = findViewNodeByIdLocked(child, id);
- if (grandChild != null && id.equals(grandChild.getAutofillId())) {
- return grandChild;
- }
- }
- }
- return null;
- }
-
- private void destroyLocked() {
- mRemoteFillService.destroy();
- mUi.setCallback(null);
- mMetricsLogger.action(MetricsProto.MetricsEvent.AUTOFILL_SESSION_FINISHED,
- mPackageName);
- }
-
- void removeSelf() {
- synchronized (mLock) {
- removeSelfLocked();
- }
- }
-
- private void removeSelfLocked() {
- if (VERBOSE) {
- Slog.v(TAG, "removeSelfLocked()");
- }
- destroyLocked();
- mSessions.remove(mActivityToken);
- }
- }
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
new file mode 100644
index 0000000..1093e9e
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -0,0 +1,764 @@
+/*
+ * 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.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST;
+import static android.view.autofill.AutofillManager.FLAG_START_SESSION;
+import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED;
+import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED;
+import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED;
+
+import static com.android.server.autofill.Helper.DEBUG;
+import static com.android.server.autofill.Helper.VERBOSE;
+import static com.android.server.autofill.Helper.findValue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.app.assist.AssistStructure.WindowNode;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.graphics.Rect;
+import android.metrics.LogMaker;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveInfo;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.autofill.IAutoFillManagerClient;
+import android.view.autofill.IAutofillWindowPresenter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.os.HandlerCaller;
+import com.android.server.autofill.ui.AutoFillUI;
+
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * 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.
+ */
+// TODO(b/33197203): make sure sessions are removed (and tested by CTS):
+// - On all authentication scenarios.
+// - When user does not interact back after a while.
+// - When service is unbound.
+final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener,
+ AutoFillUI.AutoFillUiCallback {
+ private static final String TAG = "AutofillSession";
+
+ private final AutofillManagerServiceImpl mService;
+ private final IBinder mActivityToken;
+ private final IBinder mWindowToken;
+ private final HandlerCaller mHandlerCaller;
+ private final Object mLock;
+ private final AutoFillUI mUi;
+
+ private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+ /** Package name of the app that is auto-filled */
+ @NonNull private final String mPackageName;
+
+ @GuardedBy("mLock")
+ private final Map<AutofillId, ViewState> mViewStates = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ @Nullable
+ private ViewState mCurrentViewState;
+
+ private final IAutoFillManagerClient mClient;
+
+ @GuardedBy("mLock")
+ RemoteFillService mRemoteFillService;
+
+ // TODO(b/33197203): Get a response per view instead of per activity.
+ @GuardedBy("mLock")
+ private FillResponse mCurrentResponse;
+
+ /**
+ * Used to remember which {@link Dataset} filled the session.
+ */
+ // TODO(b/33197203): might need more than one once we support partitions
+ @GuardedBy("mLock")
+ private Dataset mAutoFilledDataset;
+
+ /**
+ * Assist structure sent by the app; it will be updated (sanitized, change values for save)
+ * before sent to {@link AutofillService}.
+ */
+ @GuardedBy("mLock") AssistStructure mStructure;
+
+ /**
+ * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}.
+ */
+ private boolean mHasCallback;
+
+ /**
+ * Flags used to start the session.
+ */
+ int mFlags;
+
+ Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui,
+ @NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId,
+ @NonNull Object lock, @NonNull IBinder activityToken,
+ @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback,
+ int flags, @NonNull ComponentName componentName, @NonNull String packageName) {
+ mService = service;
+ mLock = lock;
+ mUi = ui;
+ mHandlerCaller = handlerCaller;
+ mRemoteFillService = new RemoteFillService(context, componentName, userId, this);
+ mActivityToken = activityToken;
+ mWindowToken = windowToken;
+ mHasCallback = hasCallback;
+ mPackageName = packageName;
+ mFlags = flags;
+
+ mClient = IAutoFillManagerClient.Stub.asInterface(client);
+ try {
+ client.linkToDeath(() -> {
+ if (DEBUG) {
+ Slog.d(TAG, "app binder died");
+ }
+
+ removeSelf();
+ }, 0);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "linkToDeath() on mClient failed: " + e);
+ }
+
+ mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName);
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onFillRequestSuccess(@Nullable FillResponse response,
+ @NonNull String servicePackageName) {
+ if (response == null) {
+ // Nothing to be done, but need to notify client.
+ notifyUnavailableToClient();
+ removeSelf();
+ return;
+ }
+
+ if ((response.getDatasets() == null || response.getDatasets().isEmpty())
+ && response.getAuthentication() == null) {
+ // Response is "empty" from an UI point of view, need to notify client.
+ notifyUnavailableToClient();
+ }
+ synchronized (mLock) {
+ processResponseLocked(response);
+ }
+
+ LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST))
+ .setType(MetricsEvent.TYPE_SUCCESS)
+ .setPackageName(mPackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
+ response.getDatasets() == null ? 0 : response.getDatasets().size())
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE,
+ servicePackageName);
+ mMetricsLogger.write(log);
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onFillRequestFailure(@Nullable CharSequence message,
+ @NonNull String servicePackageName) {
+ LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST))
+ .setType(MetricsEvent.TYPE_FAILURE)
+ .setPackageName(mPackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+ mMetricsLogger.write(log);
+
+ getUiForShowing().showError(message);
+ removeSelf();
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onSaveRequestSuccess(@NonNull String servicePackageName) {
+ LogMaker log = (new LogMaker(
+ MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
+ .setType(MetricsEvent.TYPE_SUCCESS)
+ .setPackageName(mPackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+ mMetricsLogger.write(log);
+
+ // Nothing left to do...
+ removeSelf();
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onSaveRequestFailure(@Nullable CharSequence message,
+ @NonNull String servicePackageName) {
+ LogMaker log = (new LogMaker(
+ MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST))
+ .setType(MetricsEvent.TYPE_FAILURE)
+ .setPackageName(mPackageName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName);
+ mMetricsLogger.write(log);
+
+ getUiForShowing().showError(message);
+ removeSelf();
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void authenticate(IntentSender intent) {
+ final Intent fillInIntent;
+ synchronized (mLock) {
+ fillInIntent = createAuthFillInIntent(mStructure);
+ }
+ mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent));
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onDisableSelf() {
+ mService.disableSelf();
+ synchronized (mLock) {
+ removeSelfLocked();
+ }
+ }
+
+ // FillServiceCallbacks
+ @Override
+ public void onServiceDied(RemoteFillService service) {
+ // TODO(b/33197203): implement
+ }
+
+ // AutoFillUiCallback
+ @Override
+ public void fill(Dataset dataset) {
+ mHandlerCaller.getHandler().post(() -> autoFill(dataset));
+ }
+
+ // AutoFillUiCallback
+ @Override
+ public void save() {
+ mHandlerCaller.getHandler()
+ .obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, mActivityToken)
+ .sendToTarget();
+ }
+
+ // AutoFillUiCallback
+ @Override
+ public void cancelSave() {
+ mHandlerCaller.getHandler().post(() -> removeSelf());
+ }
+
+ // AutoFillUiCallback
+ @Override
+ public void requestShowFillUi(AutofillId id, int width, int height,
+ IAutofillWindowPresenter presenter) {
+ try {
+ mClient.requestShowFillUi(mWindowToken, id, width, height,
+ mCurrentViewState.mVirtualBounds, presenter);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error requesting to show fill UI", e);
+ }
+ }
+
+ // AutoFillUiCallback
+ @Override
+ public void requestHideFillUi(AutofillId id) {
+ try {
+ mClient.requestHideFillUi(mWindowToken, id);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error requesting to hide fill UI", e);
+ }
+ }
+
+ public void setAuthenticationResultLocked(Bundle data) {
+ if (mCurrentResponse == null || data == null) {
+ removeSelf();
+ } else {
+ Parcelable result = data.getParcelable(
+ AutofillManager.EXTRA_AUTHENTICATION_RESULT);
+ if (result instanceof FillResponse) {
+ mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName);
+
+ mCurrentResponse = (FillResponse) result;
+ processResponseLocked(mCurrentResponse);
+ } else if (result instanceof Dataset) {
+ Dataset dataset = (Dataset) result;
+ final int index = mCurrentResponse.getDatasets().indexOf(mAutoFilledDataset);
+ if (index >= 0) {
+ mCurrentResponse.getDatasets().set(index, dataset);
+ autoFill(dataset);
+ }
+ }
+ }
+ }
+
+ public void setHasCallback(boolean hasIt) {
+ mHasCallback = hasIt;
+ }
+
+ /**
+ * Shows the save UI, when session can be saved.
+ *
+ * @return {@code true} if session is done, or {@code false} if it's pending user action.
+ */
+ public boolean showSaveLocked() {
+ if (mStructure == null) {
+ Slog.wtf(TAG, "showSaveLocked(): no mStructure");
+ return true;
+ }
+ if (mCurrentResponse == null) {
+ // Happens when the activity / session was finished before the service replied, or
+ // when the service cannot autofill it (and returned a null response).
+ if (DEBUG) {
+ Slog.d(TAG, "showSaveLocked(): no mCurrentResponse");
+ }
+ return true;
+ }
+ final SaveInfo saveInfo = mCurrentResponse.getSaveInfo();
+ if (DEBUG) {
+ Slog.d(TAG, "showSaveLocked(): saveInfo=" + saveInfo);
+ }
+
+ /*
+ * 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.
+ */
+
+ if (saveInfo == null) {
+ return true;
+ }
+
+ final AutofillId[] requiredIds = saveInfo.getRequiredIds();
+ if (requiredIds == null || requiredIds.length == 0) {
+ Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo");
+ return true;
+ }
+
+ boolean allRequiredAreNotEmpty = true;
+ boolean atLeastOneChanged = false;
+ for (int i = 0; i < requiredIds.length; i++) {
+ final AutofillId id = requiredIds[i];
+ final ViewState state = mViewStates.get(id);
+ if (state == null || state.mAutofillValue == null
+ || state.mAutofillValue.isEmpty()) {
+ final ViewNode node = findViewNodeByIdLocked(id);
+ if (node == null) {
+ Slog.w(TAG, "Service passed invalid id on SavableInfo: " + id);
+ allRequiredAreNotEmpty = false;
+ break;
+ }
+ final AutofillValue initialValue = node.getAutofillValue();
+ if (initialValue == null || initialValue.isEmpty()) {
+ if (DEBUG) {
+ Slog.d(TAG, "finishSessionLocked(): empty initial value for " + id );
+ }
+ allRequiredAreNotEmpty = false;
+ break;
+ }
+ }
+ if (state.mValueUpdated) {
+ final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+ if (!state.mAutofillValue.equals(filledValue)) {
+ if (DEBUG) {
+ Slog.d(TAG, "finishSessionLocked(): found a change on " + id + ": "
+ + filledValue + " => " + state.mAutofillValue);
+ }
+ atLeastOneChanged = true;
+ }
+ } else {
+ if (state.mAutofillValue == null || state.mAutofillValue.isEmpty()) {
+ if (DEBUG) {
+ Slog.d(TAG, "finishSessionLocked(): empty value for " + id + ": "
+ + state.mAutofillValue);
+ }
+ allRequiredAreNotEmpty = false;
+ break;
+
+ }
+ }
+ }
+
+ if (allRequiredAreNotEmpty) {
+ if (!atLeastOneChanged && saveInfo.getOptionalIds() != null) {
+ for (int i = 0; i < saveInfo.getOptionalIds().length; i++) {
+ final AutofillId id = saveInfo.getOptionalIds()[i];
+ final ViewState state = mViewStates.get(id);
+ if (state != null && state.mAutofillValue != null && state.mValueUpdated) {
+ final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+ if (!state.mAutofillValue.equals(filledValue)) {
+ if (DEBUG) {
+ Slog.d(TAG, "finishSessionLocked(): found a change on optional "
+ + id + ": " + filledValue + " => "
+ + state.mAutofillValue);
+ }
+ atLeastOneChanged = true;
+ break;
+ }
+ }
+ }
+ }
+ if (atLeastOneChanged) {
+ getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName);
+ return false;
+ }
+ }
+ // Nothing changed...
+ if (DEBUG) {
+ Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities."
+ + "allRequiredAreNotNull=" + allRequiredAreNotEmpty
+ + ", atLeastOneChanged=" + atLeastOneChanged);
+ }
+ return true;
+ }
+
+ /**
+ * Calls service when user requested save.
+ */
+ void callSaveLocked() {
+ if (DEBUG) {
+ Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates);
+ }
+
+ final Bundle extras = this.mCurrentResponse.getExtras();
+
+ for (Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
+ final AutofillValue value = entry.getValue().mAutofillValue;
+ if (value == null) {
+ if (VERBOSE) {
+ Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey());
+ }
+ continue;
+ }
+ final AutofillId id = entry.getKey();
+ final ViewNode node = findViewNodeByIdLocked(id);
+ if (node == null) {
+ Slog.w(TAG, "callSaveLocked(): did not find node with id " + id);
+ continue;
+ }
+ if (VERBOSE) {
+ Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value);
+ }
+
+ node.updateAutofillValue(value);
+ }
+
+ // Sanitize structure before it's sent to service.
+ mStructure.sanitizeForParceling(false);
+
+ if (VERBOSE) {
+ Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()");
+ mStructure.dump();
+ }
+
+ mRemoteFillService.onSaveRequest(mStructure, extras);
+ }
+
+ void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) {
+ if (mAutoFilledDataset != null && (flags & FLAG_VALUE_CHANGED) == 0) {
+ // TODO(b/33197203): ignoring because we don't support partitions yet
+ Slog.d(TAG, "updateLocked(): ignoring " + flags + " after app was autofilled");
+ return;
+ }
+
+ ViewState viewState = mViewStates.get(id);
+ if (viewState == null) {
+ viewState = new ViewState(this, id, this);
+ mViewStates.put(id, viewState);
+ }
+
+ if ((flags & FLAG_START_SESSION) != 0) {
+ // View is triggering autofill.
+ mCurrentViewState = viewState;
+ viewState.update(value, virtualBounds);
+ return;
+ }
+
+ if ((flags & FLAG_VALUE_CHANGED) != 0) {
+ if (value != null && !value.equals(viewState.mAutofillValue)) {
+ viewState.mValueUpdated = true;
+
+ // Must check if this update was caused by autofilling the view, in which
+ // case we just update the value, but not the UI.
+ if (mAutoFilledDataset != null) {
+ final AutofillValue filledValue = findValue(mAutoFilledDataset, id);
+ if (value.equals(filledValue)) {
+ viewState.mAutofillValue = value;
+ return;
+ }
+ }
+
+ // Change value
+ viewState.mAutofillValue = value;
+
+ // Update the chooser UI
+ if (value.isText()) {
+ getUiForShowing().filterFillUi(value.getTextValue().toString());
+ } else {
+ getUiForShowing().filterFillUi(null);
+ }
+ }
+
+ return;
+ }
+
+ if ((flags & FLAG_VIEW_ENTERED) != 0) {
+ // Remove the UI if the ViewState has changed.
+ if (mCurrentViewState != viewState) {
+ mUi.hideFillUi(mCurrentViewState != null ? mCurrentViewState.mId : null);
+ mCurrentViewState = viewState;
+ }
+
+ // If the ViewState is ready to be displayed, onReady() will be called.
+ viewState.update(value, virtualBounds);
+
+ // TODO(b/33197203): Remove when there is a response per activity.
+ if (mCurrentResponse != null) {
+ viewState.setResponse(mCurrentResponse);
+ }
+
+ return;
+ }
+
+ if ((flags & FLAG_VIEW_EXITED) != 0) {
+ if (mCurrentViewState == viewState) {
+ mUi.hideFillUi(viewState.mId);
+ mCurrentViewState = null;
+ }
+ return;
+ }
+
+ Slog.w(TAG, "updateLocked(): unknown flags " + flags);
+ }
+
+ @Override
+ public void onFillReady(FillResponse response, AutofillId filledId,
+ @Nullable AutofillValue value) {
+ String filterText = null;
+ if (value != null && value.isText()) {
+ filterText = value.getTextValue().toString();
+ }
+
+ getUiForShowing().showFillUi(filledId, response, filterText, mPackageName);
+ }
+
+ private void notifyUnavailableToClient() {
+ if (mCurrentViewState == null) {
+ // TODO(b/33197203): temporary sanity check; should never happen
+ Slog.w(TAG, "notifyUnavailable(): mCurrentViewState is null");
+ return;
+ }
+ if (!mHasCallback) return;
+ try {
+ mClient.notifyNoFillUi(mWindowToken, mCurrentViewState.mId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken
+ + " id=" + mCurrentViewState.mId, e);
+ }
+ }
+
+ private void processResponseLocked(FillResponse response) {
+ if (DEBUG) {
+ Slog.d(TAG, "processResponseLocked(auth=" + response.getAuthentication()
+ + "):" + response);
+ }
+
+ if (mCurrentViewState == null) {
+ // TODO(b/33197203): temporary sanity check; should never happen
+ Slog.w(TAG, "processResponseLocked(): mCurrentViewState is null");
+ return;
+ }
+
+ mCurrentResponse = response;
+
+ if (mCurrentResponse.getAuthentication() != null) {
+ // Handle authentication.
+ final Intent fillInIntent = createAuthFillInIntent(mStructure);
+ mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
+ return;
+ }
+
+ if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null
+ && response.getDatasets().size() == 1) {
+ Slog.d(TAG, "autofilling manual request directly");
+ autoFill(response.getDatasets().get(0));
+ return;
+ }
+
+ mCurrentViewState.setResponse(mCurrentResponse);
+ }
+
+ void autoFill(Dataset dataset) {
+ synchronized (mLock) {
+ mAutoFilledDataset = dataset;
+
+ // Autofill it directly...
+ if (dataset.getAuthentication() == null) {
+ autoFillApp(dataset);
+ return;
+ }
+
+ // ...or handle authentication.
+ Intent fillInIntent = createAuthFillInIntent(mStructure);
+ startAuthentication(dataset.getAuthentication(), fillInIntent);
+ }
+ }
+
+ CharSequence getServiceName() {
+ return mService.getServiceName();
+ }
+
+ private Intent createAuthFillInIntent(AssistStructure structure) {
+ Intent fillInIntent = new Intent();
+ fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure);
+ return fillInIntent;
+ }
+
+ private void startAuthentication(IntentSender intent, Intent fillInIntent) {
+ try {
+ mClient.authenticate(intent, fillInIntent);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error launching auth intent", e);
+ }
+ }
+
+ void dumpLocked(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
+ pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags);
+ pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse);
+ pw.print(prefix); pw.print("mAutoFilledDataset: "); pw.println(mAutoFilledDataset);
+ pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState);
+ pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size());
+ final String prefix2 = prefix + " ";
+ for (Map.Entry<AutofillId, ViewState> entry : mViewStates.entrySet()) {
+ pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey());
+ entry.getValue().dump(prefix2, pw);
+ }
+ if (VERBOSE) {
+ pw.print(prefix); pw.print("mStructure: " );
+ // TODO(b/33197203): add method do dump AssistStructure on pw
+ if (mStructure != null) {
+ pw.println("look at logcat" );
+ mStructure.dump(); // dumps to logcat
+ } else {
+ pw.println("null");
+ }
+ }
+ pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback);
+ mRemoteFillService.dump(prefix, pw);
+ }
+
+ void autoFillApp(Dataset dataset) {
+ synchronized (mLock) {
+ try {
+ if (DEBUG) {
+ Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
+ }
+ mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues());
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error autofilling activity: " + e);
+ }
+ }
+ }
+
+ private AutoFillUI getUiForShowing() {
+ synchronized (mLock) {
+ mUi.setCallback(this);
+ return mUi;
+ }
+ }
+
+ private ViewNode findViewNodeByIdLocked(AutofillId id) {
+ final int size = mStructure.getWindowNodeCount();
+ for (int i = 0; i < size; i++) {
+ final WindowNode window = mStructure.getWindowNodeAt(i);
+ final ViewNode root = window.getRootViewNode();
+ if (id.equals(root.getAutofillId())) {
+ return root;
+ }
+ final ViewNode child = findViewNodeByIdLocked(root, id);
+ if (child != null) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) {
+ final int childrenSize = parent.getChildCount();
+ if (childrenSize > 0) {
+ for (int i = 0; i < childrenSize; i++) {
+ final ViewNode child = parent.getChildAt(i);
+ if (id.equals(child.getAutofillId())) {
+ return child;
+ }
+ final ViewNode grandChild = findViewNodeByIdLocked(child, id);
+ if (grandChild != null && id.equals(grandChild.getAutofillId())) {
+ return grandChild;
+ }
+ }
+ }
+ return null;
+ }
+
+ void destroyLocked() {
+ mRemoteFillService.destroy();
+ mUi.setCallback(null);
+ mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName);
+ }
+
+ void removeSelf() {
+ synchronized (mLock) {
+ removeSelfLocked();
+ }
+ }
+
+ void removeSelfLocked() {
+ if (VERBOSE) {
+ Slog.v(TAG, "removeSelfLocked()");
+ }
+ destroyLocked();
+ mService.removeSessionLocked(mActivityToken);
+ }
+}
\ No newline at end of file
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
new file mode 100644
index 0000000..d31dcfd
--- /dev/null
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.annotation.Nullable;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.service.autofill.FillResponse;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+
+import java.io.PrintWriter;
+
+/**
+ * State for a given view with a AutofillId.
+ *
+ * <p>This class holds state about a view and calls its listener when the fill UI is ready to
+ * be displayed for the view.
+ */
+final class ViewState {
+ interface Listener {
+ /**
+ * Called when the fill UI is ready to be shown for this view.
+ */
+ void onFillReady(FillResponse fillResponse, AutofillId focusedId,
+ @Nullable AutofillValue value);
+ }
+
+ final AutofillId mId;
+ private final Listener mListener;
+ // TODO(b/33197203): would not need a reference to response and session if it was an inner
+ // class of Session...
+ private final Session mSession;
+ private FillResponse mResponse;
+ private Intent mAuthIntent;
+
+ // TODO(b/33197203): encapsulate access so it's not called by UI
+ AutofillValue mAutofillValue;
+
+ // TODO(b/33197203): encapsulate access so it's not called by UI
+ // Bounds if a virtual view, null otherwise
+ Rect mVirtualBounds;
+
+ boolean mValueUpdated;
+
+ ViewState(Session session, AutofillId id, Listener listener) {
+ mSession = session;
+ mId = id;
+ mListener = listener;
+ }
+
+ /**
+ * Response should only be set once.
+ */
+ void setResponse(FillResponse response) {
+ mResponse = response;
+ maybeCallOnFillReady();
+ }
+
+ /**
+ * Used when a {@link FillResponse} requires authentication to be unlocked.
+ */
+ void setResponse(FillResponse response, Intent authIntent) {
+ mAuthIntent = authIntent;
+ setResponse(response);
+ }
+
+ CharSequence getServiceName() {
+ return mSession.getServiceName();
+ }
+
+ // TODO(b/33197203): need to refactor / rename / document this method to make it clear that
+ // it can change the value and update the UI; similarly, should replace code that
+ // directly sets mAutoFilLValue to use encapsulation.
+ void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds) {
+ if (autofillValue != null) {
+ mAutofillValue = autofillValue;
+ }
+ if (virtualBounds != null) {
+ mVirtualBounds = virtualBounds;
+ }
+
+ maybeCallOnFillReady();
+ }
+
+ /**
+ * Calls {@link
+ * Listener#onFillReady(FillResponse, AutofillId, AutofillValue)} if the
+ * fill UI is ready to be displayed (i.e. when response and bounds are set).
+ */
+ void maybeCallOnFillReady() {
+ if (mResponse != null && (mResponse.getAuthentication() != null
+ || mResponse.getDatasets() != null)) {
+ mListener.onFillReady(mResponse, mId, mAutofillValue);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewState: [id=" + mId + ", value=" + mAutofillValue + ", bounds=" + mVirtualBounds
+ + ", updated = " + mValueUpdated + "]";
+ }
+
+ void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("id:" ); pw.println(mId);
+ pw.print(prefix); pw.print("value:" ); pw.println(mAutofillValue);
+ pw.print(prefix); pw.print("updated:" ); pw.println(mValueUpdated);
+ pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds);
+ pw.print(prefix); pw.print("authIntent:" ); pw.println(mAuthIntent);
+ }
+}
\ No newline at end of file