Merge "Refactor AutoFill server code."

This commit is contained in:
TreeHugger Robot
2017-01-31 23:18:09 +00:00
committed by Android (Google) Code Review
3 changed files with 370 additions and 477 deletions

View File

@@ -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);
}

View File

@@ -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);
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);
}
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);
return;
}
session.mCurrentAutoFillId = autoFillId;
session.mCurrentBounds = bounds;
mUi.showResponse(mUserId, session.mId, autoFillId, bounds, session.mCurrentResponse);
return;
}
final int sessionId = ++sSessionIdCounter;
if (DEBUG) {
Slog.d(TAG, "requestAutoFillLocked(): new session (id=" + sessionId + " for token "
+ activityToken + " and autoFillId " + autoFillId);
}
final Session newSession = new Session(sessionId, activityToken, autoFillId, bounds);
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): Handle partitioning
Session session = getOrCreateSessionByTokenLocked(activityToken);
if (DEBUG) Slog.d(TAG, "using Session: " + session.mId);
session.updateAutoFillInput(flags, autoFillId, null, bounds);
}
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;
}
}
return createSessionByTokenLocked(activityToken);
}
private Session createSessionByTokenLocked(IBinder activityToken) {
final int sessionId = ++sSessionIdCounter;
if (DEBUG) Slog.d(TAG, "creating Session: " + sessionId);
final Session newSession = new Session(sessionId, activityToken);
mSessions.put(sessionId, newSession);
/*
* 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;
@GuardedBy("mLock")
private final Map<AutoFillId, ViewState> mViewStates = new ArrayMap<>();
@GuardedBy("mLock")
@Nullable
private ViewState mCurrentViewState;
private IAutoFillAppCallback mAppCallback;
// Current view where the auto-fill bar is displayed
@GuardedBy("mLock")
private AutoFillId mCurrentAutoFillId;
@GuardedBy("mLock")
private Rect mCurrentBounds;
// TODO(b/33197203): Get a response per view instead of per activity.
@GuardedBy("mLock")
private FillResponse mCurrentResponse;
private final IFingerprintService mFingerprintService;
@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);
// TODO(b/33197203): temporarily hack: show the save notification after autofilled,
// since save is not automatically detected yet.
mUi.showSaveNotification(mUserId, mId); removeSelf = false;
} catch (RemoteException e) {
Slog.w(TAG, "Error auto-filling activity: " + e);
/**
* 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);
}
}
if (removeSelf) {
removeSelf();
}
/**
* 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 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);
}
}
}
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.
}
}

View File

@@ -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);
}
// TODO(b/33197203): request width/height properly.
mFillWindow = new AnchoredWindow(mWm, fillView, 800,
ViewGroup.LayoutParams.WRAP_CONTENT);
mFillWindow.show(bounds != null ? bounds : new Rect());
/**
* 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);
}
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() {