diff options
3 files changed, 362 insertions, 469 deletions
diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java index 8ce52782d114..392290c27a1a 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java @@ -81,7 +81,6 @@ public final class AutoFillManagerService extends SystemService { protected static final int MSG_REQUEST_AUTO_FILL = 3; private final AutoFillManagerServiceStub mServiceStub; - private final AutoFillUI mUi; private final Context mContext; private final ContentResolver mResolver; @@ -146,7 +145,7 @@ public final class AutoFillManagerService extends SystemService { mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(), mHandlerCallback, true); mContext = context; - mUi = new AutoFillUI(context, this, mLock); + mResolver = context.getContentResolver(); mServiceStub = new AutoFillManagerServiceStub(); } @@ -186,7 +185,7 @@ public final class AutoFillManagerService extends SystemService { if (DEBUG) Slog.d(TAG, "no service info for " + serviceComponent); return null; } - return new AutoFillManagerServiceImpl(this, mUi, mContext, mLock, mRequestsHistory, + return new AutoFillManagerServiceImpl(this, mContext, mLock, mRequestsHistory, FgThread.getHandler(), userId, serviceInfo.applicationInfo.uid, serviceComponent, SERVICE_BINDING_LIFETIME_MS); } @@ -326,7 +325,6 @@ public final class AutoFillManagerService extends SystemService { } } } - mUi.dump(pw); pw.println("Requests history:"); mRequestsHistory.reverseDump(fd, pw, args); } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java index 0dd891cd4235..2dcb31ccc229 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -19,7 +19,9 @@ package com.android.server.autofill; import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_ERROR; import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_REQUESTED; import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_SUCCESS; +import static android.view.View.AUTO_FILL_FLAG_TYPE_FILL; import static android.view.View.AUTO_FILL_FLAG_TYPE_SAVE; +import static android.view.autofill.AutoFillManager.FLAG_UPDATE_UI_SHOW; import static android.view.autofill.AutoFillManager.FLAG_UPDATE_UI_HIDE; import static com.android.server.autofill.Helper.DEBUG; @@ -56,6 +58,7 @@ import android.service.autofill.IAutoFillAppCallback; import android.service.autofill.IAutoFillServerCallback; import android.service.autofill.IAutoFillService; import android.service.voice.VoiceInteractionSession; +import android.util.ArrayMap; import android.util.LocalLog; import android.util.Log; import android.util.PrintWriterPrinter; @@ -63,6 +66,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.view.autofill.AutoFillId; +import android.view.autofill.AutoFillValue; import android.view.autofill.Dataset; import android.view.autofill.FillResponse; @@ -74,6 +78,7 @@ import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Map; /** * Bridge between the {@code system_server}'s {@link AutoFillManagerService} and the @@ -96,12 +101,14 @@ final class AutoFillManagerServiceImpl { private final Object mLock; private final AutoFillServiceInfo mInfo; private final AutoFillManagerService mManagerService; - private final AutoFillUI mUi; // Token used for fingerprint authentication // TODO(b/33197203): create on demand? private final IBinder mAuthToken = new Binder(); + private final IFingerprintService mFingerprintService = + IFingerprintService.Stub.asInterface(ServiceManager.getService("fingerprint")); + @GuardedBy("mLock") private final List<QueuedRequest> mQueuedRequests = new LinkedList<>(); @@ -204,11 +211,10 @@ final class AutoFillManagerServiceImpl { // Estimated time when the service will be evicted from the cache. long mEstimateTimeOfDeath; - AutoFillManagerServiceImpl(AutoFillManagerService managerService, AutoFillUI ui, - Context context, Object lock, LocalLog requestsHistory, Handler handler, int userId, - int uid, ComponentName component, long ttl) { + AutoFillManagerServiceImpl(AutoFillManagerService managerService, Context context, Object lock, + LocalLog requestsHistory, Handler handler, int userId, int uid, ComponentName component, + long ttl) { mManagerService = managerService; - mUi = ui; mContext = context; mLock = lock; mRequestsHistory = requestsHistory; @@ -277,8 +283,8 @@ final class AutoFillManagerServiceImpl { requestAutoFillLocked(activityToken, autoFillId, bounds, flags, true); } - private void requestAutoFillLocked(IBinder activityToken, AutoFillId autoFillId, Rect bounds, - int flags, boolean queueIfNecessary) { + private void requestAutoFillLocked(IBinder activityToken, @Nullable AutoFillId autoFillId, + @Nullable Rect bounds, int flags, boolean queueIfNecessary) { if (mService == null) { if (!queueIfNecessary) { Slog.w(TAG, "requestAutoFillLocked(): service is null"); @@ -294,56 +300,35 @@ final class AutoFillManagerServiceImpl { return; } - final Session session = getSessionByTokenLocked(activityToken); + final String historyItem = "s=" + mComponentName + " u=" + mUserId + " f=" + flags + + " a=" + activityToken + " i=" + autoFillId + " b=" + bounds; + mRequestsHistory.log(historyItem); - if (session != null) { - // Session already exist, update UI instead... - /* - * TODO(b/33197203): currently, it's always reusing the session, regardless of the - * requested autoFillId, but it should start a new session for views that - * were not part of the initial auto-fill dataset returned by the service. For example: - * - * 1.Activity has 4 fields, `first_name`, `last_name`, and `address`. - * 2.User taps `first_name`. - * 3.Service returns a dataset with ids for `first_name` and `last_name`. - * 4.When user taps `first_name` (again) or `last_name`, session should be reused, but - * when user taps `address`, it should start a new session (since that field was - * not part of the initial dataset). - * - * Similarly, once the activity is auto-filled, the flag logic should be reset (so if - * the user taps the view again, a new auto-fill request is made) - */ - if (DEBUG) { - Slog.d(TAG, "requestAutoFillLocked(): reusing session for token " - + activityToken + ", id " + autoFillId + " and flags " + flags); - } + // TODO(b/33197203): Handle partitioning + Session session = getOrCreateSessionByTokenLocked(activityToken); + if (DEBUG) Slog.d(TAG, "using Session: " + session.mId); - if ((flags & FLAG_UPDATE_UI_HIDE) != 0) { - // TODO(b/33197203): handle it? - if (DEBUG) Slog.d(TAG, "ignoring FLAG_UPDATE_UI_HIDE request for " + autoFillId); + session.updateAutoFillInput(flags, autoFillId, null, bounds); + } - return; + private Session getOrCreateSessionByTokenLocked(IBinder activityToken) { + final int size = mSessions.size(); + for (int i = 0; i < size; i++) { + final Session session = mSessions.valueAt(i); + if (activityToken.equals(session.mActivityToken.get())) { + return session; } - - session.mCurrentAutoFillId = autoFillId; - session.mCurrentBounds = bounds; - mUi.showResponse(mUserId, session.mId, autoFillId, bounds, session.mCurrentResponse); - return; } + return createSessionByTokenLocked(activityToken); + } + private Session createSessionByTokenLocked(IBinder activityToken) { final int sessionId = ++sSessionIdCounter; - if (DEBUG) { - Slog.d(TAG, "requestAutoFillLocked(): new session (id=" + sessionId + " for token " - + activityToken + " and autoFillId " + autoFillId); - } + if (DEBUG) Slog.d(TAG, "creating Session: " + sessionId); - final Session newSession = new Session(sessionId, activityToken, autoFillId, bounds); + final Session newSession = new Session(sessionId, activityToken); mSessions.put(sessionId, newSession); - final String historyItem = "s=" + mComponentName + " u=" + mUserId + " f=" + flags - + " a=" + activityToken + " i=" + autoFillId + " b=" + bounds; - mRequestsHistory.log(historyItem); - /* * TODO(b/33197203): apply security checks below: * - checks if disabled by secure settings / device policy @@ -353,7 +338,8 @@ final class AutoFillManagerServiceImpl { */ try { // TODO(b/33197203): add MetricsLogger call - if (!mAm.requestAutoFillData(mAssistReceiver, null, sessionId, activityToken, flags)) { + if (!mAm.requestAutoFillData( + mAssistReceiver, null, sessionId, activityToken, AUTO_FILL_FLAG_TYPE_FILL)) { // TODO(b/33197203): might need a way to warn user (perhaps a new method on // AutoFillService). Slog.w(TAG, "failed to request auto-fill data for " + activityToken); @@ -361,75 +347,7 @@ final class AutoFillManagerServiceImpl { } catch (RemoteException e) { // Should not happen, it's a local call. } - } - - /** - * Called by UI to trigger a save request to the service. - */ - void requestSaveLocked(int sessionId) { - // TODO(b/33197203): add MetricsLogger call - // TODO(b/33197203): use handler? - // TODO(b/33197203): show error on UI on Slog.w situations below??? - - if (mService == null) { - Slog.w(TAG, "requestSave(): service is null"); - return; - } - final Session session = mSessions.get(sessionId); - if (session == null) { - Slog.w(TAG, "requestSave(): no session with id " + sessionId); - return; - } - final IBinder activityToken = session.mActivityToken.get(); - if (activityToken == null) { - Slog.w(TAG, "activity token for session " + sessionId + " already GCed"); - return; - } - - /* - * TODO(b/33197203): apply security checks below: - * - checks if disabled by secure settings / device policy - * - log operation using noteOp() - * - check flags - * - display disclosure if needed - */ - try { - /* TODO(b/33197203): refactor save logic so it uses a cached AssistStructure, and get - the extras to be sent to the service based on the response / dataset in the session. - Something like: - final Bundle extras = (responseExtras == null && datasetExtras == null) - ? null : new Bundle(); - if (responseExtras != null) { - if (DEBUG) Slog.d(TAG, "response extras on save notification: " + - bundleToString(responseExtras)); - extras.putBundle(AutoFillService.EXTRA_RESPONSE_EXTRAS, responseExtras); - } - if (datasetExtras != null) { - if (DEBUG) Slog.d(TAG, "dataset extras on save notification: " + - bundleToString(datasetExtras)); - extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetExtras); - } - - */ - - if (!mAm.requestAutoFillData(mAssistReceiver, null, sessionId, activityToken, - AUTO_FILL_FLAG_TYPE_SAVE)) { - Slog.w(TAG, "failed to save for " + activityToken); - } - } catch (RemoteException e) { - // Should not happen, it's a local call. - } - } - - private Session getSessionByTokenLocked(IBinder activityToken) { - final int size = mSessions.size(); - for (int i = 0; i < size; i++) { - final Session session = mSessions.valueAt(i); - if (activityToken.equals(session.mActivityToken.get())) { - return session; - } - } - return null; + return newSession; } void stopLocked() { @@ -459,35 +377,6 @@ final class AutoFillManagerServiceImpl { } } - /** - * Called by {@link AutoFillUI} to fill an activity after the user selected a dataset. - */ - void autoFillApp(int sessionId, Dataset dataset) { - // TODO(b/33197203): add MetricsLogger call - - if (dataset == null) { - Slog.w(TAG, "autoFillApp(): no dataset for callback id " + sessionId); - return; - } - - - final Session session; - synchronized (mLock) { - session = mSessions.get(sessionId); - if (session == null) { - Slog.w(TAG, "autoFillApp(): no session with id " + sessionId); - return; - } - if (session.mAppCallback == null) { - Slog.w(TAG, "autoFillApp(): no app callback for session " + sessionId); - return; - } - - // TODO(b/33197203): use a handler? - session.autoFill(dataset); - } - } - void removeSessionLocked(int id) { if (DEBUG) Slog.d(TAG, "Removing session " + id); mSessions.remove(id); @@ -495,43 +384,6 @@ final class AutoFillManagerServiceImpl { // TODO(b/33197203): notify mService so it can invalidate the FillCallback / SaveCallback? } - /** - * Notifies the result of a {@link FillResponse} authentication request to the service. - * - * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after user - * used the fingerprint sensors to authenticate. - */ - void notifyResponseAuthenticationResult(Bundle extras, int flags) { - if (DEBUG) Slog.d(TAG, "notifyResponseAuthenticationResult(): flags=" + flags - + ", extras=" + bundleToString(extras)); - - synchronized (mLock) { - try { - mService.authenticateFillResponse(extras, flags); - } catch (RemoteException e) { - Slog.w(TAG, "Error sending authentication result back to service: " + e); - } - } - } - - /** - * Notifies the result of a {@link Dataset} authentication request to the service. - * - * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after - * it gets the results from a fingerprint authentication. - */ - void notifyDatasetAuthenticationResult(Bundle extras, int flags) { - if (DEBUG) Slog.d(TAG, "notifyDatasetAuthenticationResult(): flags=" + flags - + ", extras=" + bundleToString(extras)); - synchronized (mLock) { - try { - mService.authenticateDataset(extras, flags); - } catch (RemoteException e) { - Slog.w(TAG, "Error sending authentication result back to service: " + e); - } - } - } - void dumpLocked(String prefix, PrintWriter pw) { if (!mValid) { pw.print(" NOT VALID: "); @@ -612,8 +464,78 @@ final class AutoFillManagerServiceImpl { } /** - * A bridge between the {@link AutoFillService} implementation and the activity being - * auto-filled (represented through the {@link IAutoFillAppCallback}). + * 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(ViewState viewState, FillResponse fillResponse, Rect bounds, + @Nullable AutoFillValue value); + } + + private final Listener mListener; + @Nullable + private FillResponse mResponse; + private AutoFillValue mAutoFillValue; + private Rect mBounds; + + ViewState(Listener listener) { + mListener = listener; + } + + /** + * Response should only be set once. + */ + void setResponse(FillResponse response) { + if (mResponse != null) { + Slog.e(TAG, "ViewState response set more than once"); + return; + } + mResponse = response; + + maybeCallOnFillReady(); + } + + void update(@Nullable AutoFillValue autoFillValue, @Nullable Rect bounds) { + if (autoFillValue != null) { + mAutoFillValue = autoFillValue; + } + if (bounds != null) { + mBounds = bounds; + } + + maybeCallOnFillReady(); + } + + /** + * Calls {@link Listener#onFillReady(ViewState, FillResponse, Rect, AutoFillValue)} if the + * fill UI is ready to be displayed (i.e. when response and bounds are set). + */ + void maybeCallOnFillReady() { + if (mResponse != null && mBounds != null) { + mListener.onFillReady(this, mResponse, mBounds, mAutoFillValue); + } + } + + @Override + public String toString() { + if (!DEBUG) return super.toString(); + + return "ViewState: [response=" + mResponse + ", value=" + mAutoFillValue + + ", bounds=" + mBounds + "]"; + } + } + + /** + * 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 view session to display the appropriate UI. * * <p>Although the auto-fill 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 @@ -625,23 +547,23 @@ final class AutoFillManagerServiceImpl { // - On all authentication scenarios. // - When user does not interact back after a while. // - When service is unbound. - private final class Session { + final class Session implements ViewState.Listener { - private final int mId; + private final AutoFillUI mUi; + final int mId; private final WeakReference<IBinder> mActivityToken; - private IAutoFillAppCallback mAppCallback; - - // Current view where the auto-fill bar is displayed @GuardedBy("mLock") - private AutoFillId mCurrentAutoFillId; + private final Map<AutoFillId, ViewState> mViewStates = new ArrayMap<>(); @GuardedBy("mLock") - private Rect mCurrentBounds; - @GuardedBy("mLock") - private FillResponse mCurrentResponse; + @Nullable + private ViewState mCurrentViewState; - private final IFingerprintService mFingerprintService; + private IAutoFillAppCallback mAppCallback; + // TODO(b/33197203): Get a response per view instead of per activity. + @GuardedBy("mLock") + private FillResponse mCurrentResponse; @GuardedBy("mLock") private FillResponse mResponseRequiringAuth; @GuardedBy("mLock") @@ -683,7 +605,7 @@ final class AutoFillManagerServiceImpl { notifyDatasetAuthenticationResult(mDatasetRequiringAuth.getExtras(), FLAG_AUTHENTICATION_SUCCESS); } else { - autoFillAppLocked(mDatasetRequiringAuth, true); + autoFillApp(mDatasetRequiringAuth); } } else if (mResponseRequiringAuth != null) { final List<Dataset> datasets = mResponseRequiringAuth.getDatasets(); @@ -697,7 +619,7 @@ final class AutoFillManagerServiceImpl { Slog.w(TAG, "onAuthenticationSucceeded(): no response or dataset"); } - mUi.dismissFingerprintRequest(mUserId, true); + mUi.dismissFingerprintRequest(true); } @Override @@ -721,7 +643,7 @@ final class AutoFillManagerServiceImpl { Slog.w(TAG, "onError(): no response or dataset"); } - mUi.dismissFingerprintRequest(mUserId, false); + mUi.dismissFingerprintRequest(false); } @Override @@ -741,7 +663,6 @@ final class AutoFillManagerServiceImpl { // TODO(b/33197203): add MetricsLogger call if (response == null) { if (DEBUG) Slog.d(TAG, "showResponse(): null response"); - removeSelf(); return; } @@ -805,20 +726,16 @@ final class AutoFillManagerServiceImpl { if (DEBUG) Log.d(TAG, "unlockDataset(): dataset=" + dataset + ", flags=" + flags); if ((flags & FLAG_AUTHENTICATION_SUCCESS) != 0) { - autoFillAppLocked(dataset != null ? dataset : mDatasetRequiringAuth, true); + autoFillApp(dataset != null ? dataset : mDatasetRequiringAuth); return; } - removeSelf(); } }; - private Session(int id, IBinder activityToken, AutoFillId autoFillId, Rect bounds) { - this.mId = id; - this.mActivityToken = new WeakReference<>(activityToken); - this.mCurrentAutoFillId = autoFillId; - this.mCurrentBounds = bounds; - this.mFingerprintService = IFingerprintService.Stub - .asInterface(ServiceManager.getService("fingerprint")); + private Session(int id, IBinder activityToken) { + mUi = new AutoFillUI(mContext, this); + mId = id; + mActivityToken = new WeakReference<>(activityToken); } void setAppCallback(IBinder appBinder) { @@ -834,6 +751,54 @@ final class AutoFillManagerServiceImpl { mAppCallback = IAutoFillAppCallback.Stub.asInterface(appBinder); } + void updateAutoFillInput(int flags, AutoFillId autoFillId, + @Nullable AutoFillValue autoFillValue, @Nullable Rect bounds) { + synchronized (mLock) { + ViewState viewState = mViewStates.get(autoFillId); + if (viewState == null) { + viewState = new ViewState(this); + mViewStates.put(autoFillId, viewState); + } + + if ((flags & FLAG_UPDATE_UI_SHOW) != 0) { + // Remove the UI if the ViewState has changed. + if (mCurrentViewState != viewState) { + mUi.hideFillUi(); + mCurrentViewState = viewState; + } + + // If the ViewState is ready to be displayed, onReady() will be called. + viewState.update(autoFillValue, bounds); + + // TODO(b/33197203): Remove when there is a response per activity. + if (mCurrentResponse != null) { + viewState.setResponse(mCurrentResponse); + } + } else if ((flags & FLAG_UPDATE_UI_HIDE) != 0) { + if (mCurrentViewState == viewState) { + mUi.hideFillUi(); + mCurrentViewState = null; + } + } else { + Slog.w(TAG, "unknown flags " + flags); + } + } + } + + @Override + public void onFillReady(ViewState viewState, FillResponse response, Rect bounds, + @Nullable AutoFillValue value) { + String filterText = ""; + if (value != null) { + // TODO(b/33197203): Handle other AutoFillValue types + final CharSequence text = value.getTextValue(); + if (text != null) { + filterText = text.toString(); + } + } + mUi.showFillUi(viewState, response.getDatasets(), bounds, filterText); + } + private void showResponseLocked(FillResponse response, boolean authRequired) { if (DEBUG) Slog.d(TAG, "showResponse(directly=" + mAutoFillDirectly + ", authRequired=" + authRequired +"):" + response); @@ -845,7 +810,7 @@ final class AutoFillManagerServiceImpl { final Dataset dataset = datasets.get(0); if (DEBUG) Slog.d(TAG, "auto-filling directly from auth: " + dataset); - autoFillAppLocked(dataset, true); + autoFillApp(dataset); return; } } @@ -853,7 +818,10 @@ final class AutoFillManagerServiceImpl { if (!authRequired) { // TODO(b/33197203): add MetricsLogger call mCurrentResponse = response; - mUi.showResponse(mUserId, mId, mCurrentAutoFillId, mCurrentBounds, mCurrentResponse); + // TODO(b/33197203): Consider using mCurrentResponse, depends on partitioning design + if (mCurrentViewState != null) { + mCurrentViewState.setResponse(mCurrentResponse); + } return; } @@ -869,7 +837,7 @@ final class AutoFillManagerServiceImpl { scanFingerprint(response.getCryptoObjectOpId()); } // Displays the message asking the user to tap (or fingerprint) for AutoFill. - mUi.showFillResponseAuthenticationRequest(mUserId, mId, requiresFingerprint, + mUi.showFillResponseAuthenticationRequest(requiresFingerprint, response.getExtras(), response.getFlags()); } @@ -877,7 +845,7 @@ final class AutoFillManagerServiceImpl { synchronized (mLock) { // Autofill it directly... if (!dataset.isAuthRequired()) { - autoFillAppLocked(dataset, true); + autoFillApp(dataset); return; } @@ -906,30 +874,114 @@ final class AutoFillManagerServiceImpl { void dumpLocked(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("mId: "); pw.println(mId); pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken.get()); - pw.print(prefix); pw.print("mCurrentAutoFillId: "); pw.println(mCurrentAutoFillId); - pw.print(prefix); pw.print("mCurrentBounds: "); pw.println(mCurrentBounds); pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse); pw.print(prefix); pw.print("mResponseRequiringAuth: "); pw.println(mResponseRequiringAuth); pw.print(prefix); pw.print("mDatasetRequiringAuth: "); pw.println(mDatasetRequiringAuth); pw.print(prefix); pw.print("mAutoFillDirectly: "); pw.println(mAutoFillDirectly); + 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(prefix2); + pw.print(entry.getKey()); pw.print(": " ); pw.println(entry.getValue()); + } } - private void autoFillAppLocked(Dataset dataset, boolean removeSelf) { - try { - if (DEBUG) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); - mAppCallback.autoFill(dataset); + /** + * Notifies the result of a {@link FillResponse} authentication request to the service. + * + * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after user + * used the fingerprint sensors to authenticate. + */ + void notifyResponseAuthenticationResult(Bundle extras, int flags) { + if (DEBUG) Slog.d(TAG, "notifyResponseAuthenticationResult(): flags=" + flags + + ", extras=" + bundleToString(extras)); + synchronized (mLock) { + try { + mService.authenticateFillResponse(extras, flags); + } catch (RemoteException e) { + Slog.w(TAG, "Error sending authentication result back to service: " + e); + } + } + } - // TODO(b/33197203): temporarily hack: show the save notification after autofilled, - // since save is not automatically detected yet. - mUi.showSaveNotification(mUserId, mId); removeSelf = false; + /** + * Notifies the result of a {@link Dataset} authentication request to the service. + * + * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after + * it gets the results from a fingerprint authentication. + */ + void notifyDatasetAuthenticationResult(Bundle extras, int flags) { + if (DEBUG) Slog.d(TAG, "notifyDatasetAuthenticationResult(): flags=" + flags + + ", extras=" + bundleToString(extras)); + synchronized (mLock) { + try { + mService.authenticateDataset(extras, flags); + } catch (RemoteException e) { + Slog.w(TAG, "Error sending authentication result back to service: " + e); + } + } + } - } catch (RemoteException e) { - Slog.w(TAG, "Error auto-filling activity: " + e); + void autoFillApp(Dataset dataset) { + synchronized (mLock) { + try { + if (DEBUG) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); + mAppCallback.autoFill(dataset); + } catch (RemoteException e) { + Slog.w(TAG, "Error auto-filling activity: " + e); + } } - if (removeSelf) { - removeSelf(); + } + + void requestSave() { + synchronized (mLock) { + requestSaveLocked(mId); + } + } + + /** + * Called by UI to trigger a save request to the service. + */ + void requestSaveLocked(int sessionId) { + // TODO(b/33197203): add MetricsLogger call + // TODO(b/33197203): use handler? + // TODO(b/33197203): show error on UI on Slog.w situations below??? + + if (mService == null) { + Slog.w(TAG, "requestSave(): service is null"); + return; + } + final Session session = mSessions.get(sessionId); + if (session == null) { + Slog.w(TAG, "requestSave(): no session with id " + sessionId); + return; + } + final IBinder activityToken = session.mActivityToken.get(); + if (activityToken == null) { + Slog.w(TAG, "activity token for session " + sessionId + " already GCed"); + return; + } + + /* + * TODO(b/33197203): apply security checks below: + * - checks if disabled by secure settings / device policy + * - log operation using noteOp() + * - check flags + * - display disclosure if needed + */ + try { + /* TODO(b/33197203): refactor save logic so it uses a cached AssistStructure, and + get the extras to be sent to the service based on the response / dataset in the + session. */ + if (!mAm.requestAutoFillData(mAssistReceiver, null, sessionId, activityToken, + AUTO_FILL_FLAG_TYPE_SAVE)) { + Slog.w(TAG, "failed to save for " + activityToken); + } + } catch (RemoteException e) { + // Should not happen, it's a local call. } } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index 96f340840542..86e04ccd6a83 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -45,6 +45,8 @@ import android.widget.Toast; import com.android.internal.annotations.GuardedBy; import com.android.server.UiThread; +import com.android.server.autofill.AutoFillManagerServiceImpl.Session; +import com.android.server.autofill.AutoFillManagerServiceImpl.ViewState; import java.io.PrintWriter; import java.util.Arrays; @@ -59,24 +61,25 @@ final class AutoFillUI { private static final String TAG = "AutoFillUI"; private final Context mContext; - + private final Session mSession; private final WindowManager mWm; - @Nullable + // Fill UI variables private AnchoredWindow mFillWindow; + private DatasetPicker mFillView; + private ViewState mViewState; + private Rect mBounds; + private String mFilterText; /** * Custom snackbar UI used for saving autofill or other informational messages. */ private View mSnackbar; - AutoFillUI(Context context, AutoFillManagerService service, Object lock) { + AutoFillUI(Context context, Session session) { mContext = context; + mSession = session; mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - mService = service; - mLock = lock; - - setNotificationListener(); } /** @@ -101,29 +104,63 @@ final class AutoFillUI { } /** - * Shows the options from a {@link FillResponse} so the user can pick up the proper - * {@link Dataset} (when the response has one) for a given view (identified by - * {@code autoFillId}). + * Hides the fill UI. */ - void showResponse(int userId, int sessionId, AutoFillId autoFillId, Rect bounds, - FillResponse response) { - if (DEBUG) Slog.d(TAG, "showResponse: id=" + autoFillId + ", bounds=" + bounds); - + void hideFillUi() { UiThread.getHandler().runWithScissors(() -> { if (mFillWindow != null) { + if (DEBUG) Slog.d(TAG, "remove FillUi remove " + mFillWindow); mFillWindow.hide(); } - final DatasetPicker fillView = new DatasetPicker(mContext, response.getDatasets(), - (dataset) -> { - mFillWindow.hide(); - onDatasetPicked(userId, dataset, sessionId); - }); + mViewState = null; + mBounds = null; + mFilterText = null; + mFillView = null; + mFillWindow = null; + }, 0); + } + + /** + * Shows the fill UI, removing the previous fill UI if the has changed. + * + * @param viewState the view state, compared by reference to know if new UI should be shown + * @param response the response to show, not used if viewState is the same + * @param bounds bounds of the view to be filled, used if changed + * @param filterText text of the view to be filled, used if changed + */ + void showFillUi(ViewState viewState, List<Dataset> datasets, Rect bounds, + String filterText) { + UiThread.getHandler().runWithScissors(() -> { + if (mViewState != viewState) { + // new + hideFillUi(); + + mViewState = viewState; + + mFillView = new DatasetPicker(mContext, datasets, + (dataset) -> { + mSession.autoFillApp(dataset); + hideFillUi(); + showSaveUi(); + }); + mFillWindow = new AnchoredWindow( + mWm, mFillView, 800, ViewGroup.LayoutParams.WRAP_CONTENT); + + if (DEBUG) Slog.d(TAG, "show FillUi"); + } + + if (!bounds.equals(mBounds)) { + if (DEBUG) Slog.d(TAG, "update FillUi bounds: " + mBounds); + mBounds = bounds; + mFillWindow.show(mBounds); + } - // TODO(b/33197203): request width/height properly. - mFillWindow = new AnchoredWindow(mWm, fillView, 800, - ViewGroup.LayoutParams.WRAP_CONTENT); - mFillWindow.show(bounds != null ? bounds : new Rect()); + if (!filterText.equals(mFilterText)) { + if (DEBUG) Slog.d(TAG, "update FillUi filter text: " + mFilterText); + mFilterText = filterText; + mFillView.update(mFilterText); + } }, 0); } @@ -134,10 +171,10 @@ final class AutoFillUI { * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint or tap to * autofill" or "Tap to autofill", depending on the value of {@code usesFingerprint}. */ - void showFillResponseAuthenticationRequest(int userId, int sessionId, boolean usesFingerprint, + void showFillResponseAuthenticationRequest(boolean usesFingerprint, Bundle extras, int flags) { // TODO(b/33197203): proper implementation - showAuthNotification(userId, sessionId, usesFingerprint, extras, flags); + showAuthNotification(usesFingerprint, extras, flags); } /** @@ -161,15 +198,13 @@ final class AutoFillUI { /** * Shows the UI asking the user to save for auto-fill. */ - void showSaveUI(int userId, int sessionId) { + void showSaveUi() { showSnackbar(new SavePrompt(mContext, new SavePrompt.OnSaveListener() { @Override public void onSaveClick() { hideSnackbar(); - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceLocked(userId); - service.requestSaveLocked(sessionId); - } + + mSession.requestSave(); } @Override public void onCancelClick() { @@ -181,10 +216,10 @@ final class AutoFillUI { /** * Called by service after the user user the fingerprint sensors to authenticate. */ - void dismissFingerprintRequest(int userId, boolean success) { + void dismissFingerprintRequest(boolean success) { if (DEBUG) Slog.d(TAG, "dismissFingerprintRequest(): ok=" + success); - dismissAuthNotification(userId); + dismissAuthNotification(); if (!success) { // TODO(b/33197203): proper implementation (snack bar / i18n string) @@ -198,48 +233,11 @@ final class AutoFillUI { pw.println("AufoFill UI"); final String prefix = " "; pw.print(prefix); pw.print("sResultCode: "); pw.println(sResultCode); + pw.print(prefix); pw.print("mSessionId: "); pw.println(mSession.mId); pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar); - mFillWindow.dump(pw); - } - - private AutoFillManagerServiceImpl getServiceLocked(int userId) { - final AutoFillManagerServiceImpl service = mService.getServiceForUserLocked(userId); - if (service == null) { - Slog.w(TAG, "no auto-fill service for user " + userId); - } - return service; - } - - private void onSaveRequested(int userId, int sessionId) { - // TODO(b/33197203): displays the snack bar, until save notification is refactored - showSaveUI(userId, sessionId); - } - - private void onDatasetPicked(int userId, Dataset dataset, int sessionId) { - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceLocked(userId); - if (service == null) return; - - service.autoFillApp(sessionId, dataset); - } - } - - private void onSessionDone(int userId, int sessionId) { - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceLocked(userId); - if (service == null) return; - - service.removeSessionLocked(sessionId); - } - } - - private void onResponseAuthenticationRequested(int userId, Bundle extras, int flags) { - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceLocked(userId); - if (service == null) return; - - service.notifyResponseAuthenticationResult(extras, flags); - } + pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState); + pw.print(prefix); pw.print("mBounds: "); pw.println(mBounds); + pw.print(prefix); pw.print("mFilterText: "); pw.println(mFilterText); } //similar to a snackbar, but can be a bit custom since it is more than just text. This will @@ -289,16 +287,10 @@ final class AutoFillUI { private static final String EXTRA_FLAGS = "flags"; private static final String TYPE_OPTIONS = "options"; - private static final String TYPE_FINISH_SESSION = "finish_session"; - private static final String TYPE_PICK_DATASET = "pick_dataset"; - private static final String TYPE_SAVE = "save"; private static final String TYPE_AUTH_RESPONSE = "auth_response"; - @GuardedBy("mServiceLock") private BroadcastReceiver mNotificationReceiver; - @GuardedBy("mServiceLock") - private final AutoFillManagerService mService; - private final Object mLock; + private final Object mLock = new Object(); // Hack used to generate unique pending intents static int sResultCode = 0; @@ -316,8 +308,6 @@ final class AutoFillUI { final class NotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - final int userId = intent.getIntExtra(EXTRA_USER_ID, -1); - final int sessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1); final String type = intent.getStringExtra(EXTRA_NOTIFICATION_TYPE); if (type == null) { Slog.wtf(TAG, "No extra " + EXTRA_NOTIFICATION_TYPE + " on intent " + intent); @@ -326,26 +316,12 @@ final class AutoFillUI { final Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET); final int flags = intent.getIntExtra(EXTRA_FLAGS, 0); - if (DEBUG) Slog.d(TAG, "Notification received: type=" + type + ", userId=" + userId - + ", sessionId=" + sessionId); + if (DEBUG) Slog.d(TAG, "Notification received: type=" + type + + ", sessionId=" + mSession.mId); synchronized (mLock) { switch (type) { - case TYPE_SAVE: - onSaveRequested(userId, sessionId); - break; - case TYPE_FINISH_SESSION: - onSessionDone(userId, sessionId); - break; - case TYPE_PICK_DATASET: - onDatasetPicked(userId, dataset, sessionId); - - // Must cancel notification because it might be comming from action - if (DEBUG) Slog.d(TAG, "Cancelling notification"); - NotificationManager.from(mContext).cancel(TYPE_OPTIONS, userId); - - break; case TYPE_AUTH_RESPONSE: - onResponseAuthenticationRequested(userId, + mSession.notifyResponseAuthenticationResult( intent.getBundleExtra(EXTRA_AUTH_REQUIRED_EXTRAS), flags); break; default: { @@ -357,161 +333,28 @@ final class AutoFillUI { } } - private static Intent newNotificationIntent(int userId, String type) { + private static Intent newNotificationIntent(String type) { final Intent intent = new Intent(NOTIFICATION_AUTO_FILL_INTENT); - intent.putExtra(EXTRA_USER_ID, userId); intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); return intent; } - private PendingIntent newPickDatasetPI(int userId, int sessionId, FillResponse response, - Dataset dataset) { - final int resultCode = ++ sResultCode; - if (DEBUG) Slog.d(TAG, "newPickDatasetPI: userId=" + userId + ", sessionId=" + sessionId - + ", resultCode=" + resultCode); - - final Intent intent = newNotificationIntent(userId, TYPE_PICK_DATASET); - intent.putExtra(EXTRA_SESSION_ID, sessionId); - intent.putExtra(EXTRA_FILL_RESPONSE, response); - intent.putExtra(EXTRA_DATASET, dataset); - return PendingIntent.getBroadcast(mContext, resultCode, intent, - PendingIntent.FLAG_ONE_SHOT); - } - - /** - * Shows a notification with the results of an auto-fill request, using notications actions - * to emulate the auto-fill bar buttons displaying the dataset names. - */ - private void showOptionsNotification(int userId, int callbackId, AutoFillId autoFillId, - FillResponse response) { - final long token = Binder.clearCallingIdentity(); - try { - showOptionsNotificationAsSystem(userId, callbackId, autoFillId, response); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private void showOptionsNotificationAsSystem(int userId, int sessionId, - AutoFillId autoFillId, FillResponse response) { - // Make sure server callback is removed from cache if user cancels the notification. - final Intent deleteIntent = newNotificationIntent(userId, TYPE_FINISH_SESSION) - .putExtra(EXTRA_SESSION_ID, sessionId); - final PendingIntent deletePendingIntent = PendingIntent.getBroadcast(mContext, - ++sResultCode, deleteIntent, PendingIntent.FLAG_ONE_SHOT); - - final String title = "AutoFill Options"; - - final Notification.Builder notification = newNotificationBuilder() - .setOngoing(false) - .setDeleteIntent(deletePendingIntent) - .setContentTitle(title); - - boolean autoCancel = true; - final String subTitle; - final List<Dataset> datasets; - final AutoFillId[] savableIds; - if (response != null) { - datasets = response.getDatasets(); - savableIds = response.getSavableIds(); - } else { - datasets = null; - savableIds = null; - } - boolean showSave = false; - if (datasets == null ) { - subTitle = "No options to auto-fill " + autoFillId; - } else if (datasets.isEmpty()) { - if (savableIds.length == 0) { - subTitle = "No options to auto-fill " + autoFillId; - } else { - subTitle = "No options to auto-fill " + autoFillId - + ", but provider can save ids:\n" + Arrays.toString(savableIds); - showSave = true; - } - } else { - final AutoFillManagerServiceImpl service = mService.getServiceForUserLocked(userId); - if (service == null) { - subTitle = "No auto-fill service for user " + userId; - Slog.w(TAG, subTitle); - } else { - autoCancel = false; - final int size = datasets.size(); - subTitle = "There are " + size + " option(s) to fill " + autoFillId + ".\n" - + "Use the notification action(s) to select the proper one." - + "Actions with (F) require fingerprint unlock, and with (P) require" - + "provider authentication to unlock"; - for (Dataset dataset : datasets) { - final StringBuilder name = new StringBuilder(dataset.getName()); - if (dataset.isAuthRequired()) { - if (dataset.hasCryptoObject()) { - name.append("(F)"); - } else { - name.append("(P)"); - } - } - final PendingIntent pi = newPickDatasetPI(userId, sessionId, response, dataset); - notification.addAction(new Action.Builder(null, name, pi).build()); - } - } - } - - notification.setAutoCancel(autoCancel); - notification.setStyle(new Notification.BigTextStyle().bigText(subTitle)); - - NotificationManager.from(mContext).notify(TYPE_OPTIONS, userId, notification.build()); - - if (showSave) { - showSaveNotification(userId, sessionId); - } - } - - void showSaveNotification(int userId, int sessionId) { - final long token = Binder.clearCallingIdentity(); - try { - showSaveNotificationAsSystem(userId, sessionId); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private void showSaveNotificationAsSystem(int userId, int sessionId) { - final Intent saveIntent = newNotificationIntent(userId, TYPE_SAVE) - .putExtra(EXTRA_SESSION_ID, sessionId); - - final PendingIntent savePendingIntent = PendingIntent.getBroadcast(mContext, - ++sResultCode, saveIntent, PendingIntent.FLAG_ONE_SHOT); - - final String title = "AutoFill Save Emulation"; - final String subTitle = "Tap notification to launch the save snackbar."; - - final Notification notification = newNotificationBuilder() - .setAutoCancel(true) - .setOngoing(false) - .setContentTitle(title) - .setContentIntent(savePendingIntent) - .setStyle(new Notification.BigTextStyle().bigText(subTitle)) - .build(); - NotificationManager.from(mContext).notify(TYPE_SAVE, userId, notification); - } - - private void showAuthNotification(int userId, int sessionId, boolean usesFingerprint, + private void showAuthNotification(boolean usesFingerprint, Bundle extras, int flags) { final long token = Binder.clearCallingIdentity(); try { - showAuthNotificationAsSystem(userId, sessionId, usesFingerprint, extras, flags); + showAuthNotificationAsSystem(usesFingerprint, extras, flags); } finally { Binder.restoreCallingIdentity(token); } } - private void showAuthNotificationAsSystem(int userId, int sessionId, + private void showAuthNotificationAsSystem( boolean usesFingerprint, Bundle extras, int flags) { final String title = "AutoFill Authentication"; final StringBuilder subTitle = new StringBuilder("Provider require user authentication.\n"); - final Intent authIntent = newNotificationIntent(userId, TYPE_AUTH_RESPONSE) - .putExtra(EXTRA_SESSION_ID, sessionId); + final Intent authIntent = newNotificationIntent(TYPE_AUTH_RESPONSE); if (extras != null) { authIntent.putExtra(EXTRA_AUTH_REQUIRED_EXTRAS, extras); } @@ -537,11 +380,11 @@ final class AutoFillUI { if (authPendingIntent != null) { notification.setContentIntent(authPendingIntent); } - NotificationManager.from(mContext).notify(TYPE_AUTH_RESPONSE, userId, notification.build()); + NotificationManager.from(mContext).notify(mSession.mId, notification.build()); } - private void dismissAuthNotification(int userId) { - NotificationManager.from(mContext).cancel(TYPE_AUTH_RESPONSE, userId); + private void dismissAuthNotification() { + NotificationManager.from(mContext).cancel(mSession.mId); } private Notification.Builder newNotificationBuilder() { |