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;
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;
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;
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
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<>();
// 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;
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");
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
*/
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);
} 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() {
}
}
- /**
- * 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);
// 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: ");
}
/**
- * 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
// - 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")
notifyDatasetAuthenticationResult(mDatasetRequiringAuth.getExtras(),
FLAG_AUTHENTICATION_SUCCESS);
} else {
- autoFillAppLocked(mDatasetRequiringAuth, true);
+ autoFillApp(mDatasetRequiringAuth);
}
} else if (mResponseRequiringAuth != null) {
final List<Dataset> datasets = mResponseRequiringAuth.getDatasets();
Slog.w(TAG, "onAuthenticationSucceeded(): no response or dataset");
}
- mUi.dismissFingerprintRequest(mUserId, true);
+ mUi.dismissFingerprintRequest(true);
}
@Override
Slog.w(TAG, "onError(): no response or dataset");
}
- mUi.dismissFingerprintRequest(mUserId, false);
+ mUi.dismissFingerprintRequest(false);
}
@Override
// TODO(b/33197203): add MetricsLogger call
if (response == null) {
if (DEBUG) Slog.d(TAG, "showResponse(): null response");
-
removeSelf();
return;
}
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) {
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);
final Dataset dataset = datasets.get(0);
if (DEBUG) Slog.d(TAG, "auto-filling directly from auth: " + dataset);
- autoFillAppLocked(dataset, true);
+ autoFillApp(dataset);
return;
}
}
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;
}
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());
}
synchronized (mLock) {
// Autofill it directly...
if (!dataset.isAuthRequired()) {
- autoFillAppLocked(dataset, true);
+ autoFillApp(dataset);
return;
}
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.
}
}
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;
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();
}
/**
}
/**
- * 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);
}
* <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);
}
/**
/**
* 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() {
/**
* 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)
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
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;
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);
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: {
}
}
- 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);
}
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() {