diff options
| author | 2017-03-28 00:28:56 +0000 | |
|---|---|---|
| committer | 2017-03-28 00:28:59 +0000 | |
| commit | 6e42a1d0ce86dda0b69dcfeed9c1804db1531680 (patch) | |
| tree | 7b1153266b94535891609107106e90236918a6d4 | |
| parent | 1d34605fcb411fb66e59c12feeaa36903319e9d9 (diff) | |
| parent | 6fa8a07143d1984ff42750079cf596a868644663 (diff) | |
Merge "Moved Session and ViewState to its own classes."
3 files changed, 917 insertions, 824 deletions
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 4d783503cb5f..3d1c2511db27 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 @@ package com.android.server.autofill; 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.Activity; 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.Log; 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 @@ final class AutofillManagerServiceImpl { 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 @@ final class AutofillManagerServiceImpl { 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 @@ final class AutofillManagerServiceImpl { 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 @@ final class AutofillManagerServiceImpl { 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 @@ final class AutofillManagerServiceImpl { + ", 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 000000000000..1093e9e27d93 --- /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 000000000000..d31dcfd061c0 --- /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 |