ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
method public void onConnected();
+ method public void onDatasetAuthenticationRequest(android.os.Bundle, int);
method public void onDisconnected();
method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback);
+ method public void onFillResponseAuthenticationRequest(android.os.Bundle, int);
method public abstract void onSaveRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.SaveCallback);
field public static final java.lang.String EXTRA_DATASET_EXTRAS = "android.service.autofill.extra.DATASET_EXTRAS";
field public static final java.lang.String EXTRA_RESPONSE_EXTRAS = "android.service.autofill.extra.RESPONSE_EXTRAS";
+ field public static final int FLAG_AUTHENTICATION_ERROR = 4; // 0x4
+ field public static final int FLAG_AUTHENTICATION_REQUESTED = 1; // 0x1
+ field public static final int FLAG_AUTHENTICATION_SUCCESS = 2; // 0x2
+ field public static final int FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE = 8; // 0x8
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
field public static final java.lang.String SERVICE_META_DATA = "android.autofill";
}
public final class FillCallback {
+ method public void onDatasetAuthentication(android.view.autofill.Dataset, int);
method public void onFailure(java.lang.CharSequence);
+ method public void onFillResponseAuthentication(int);
method public void onSuccess(android.view.autofill.FillResponse);
}
public static final class Dataset.Builder {
ctor public Dataset.Builder(java.lang.CharSequence);
method public android.view.autofill.Dataset build();
+ method public android.view.autofill.Dataset.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.Dataset.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.Dataset.Builder setExtras(android.os.Bundle);
method public android.view.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
}
method public android.view.autofill.FillResponse.Builder addDataset(android.view.autofill.Dataset);
method public android.view.autofill.FillResponse.Builder addSavableFields(android.view.autofill.AutoFillId...);
method public android.view.autofill.FillResponse build();
+ method public android.view.autofill.FillResponse.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.FillResponse.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle);
}
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
method public void onConnected();
+ method public void onDatasetAuthenticationRequest(android.os.Bundle, int);
method public void onDisconnected();
method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback);
+ method public void onFillResponseAuthenticationRequest(android.os.Bundle, int);
method public abstract void onSaveRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.SaveCallback);
field public static final java.lang.String EXTRA_DATASET_EXTRAS = "android.service.autofill.extra.DATASET_EXTRAS";
field public static final java.lang.String EXTRA_RESPONSE_EXTRAS = "android.service.autofill.extra.RESPONSE_EXTRAS";
+ field public static final int FLAG_AUTHENTICATION_ERROR = 4; // 0x4
+ field public static final int FLAG_AUTHENTICATION_REQUESTED = 1; // 0x1
+ field public static final int FLAG_AUTHENTICATION_SUCCESS = 2; // 0x2
+ field public static final int FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE = 8; // 0x8
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
field public static final java.lang.String SERVICE_META_DATA = "android.autofill";
}
public final class FillCallback {
+ method public void onDatasetAuthentication(android.view.autofill.Dataset, int);
method public void onFailure(java.lang.CharSequence);
+ method public void onFillResponseAuthentication(int);
method public void onSuccess(android.view.autofill.FillResponse);
}
public static final class Dataset.Builder {
ctor public Dataset.Builder(java.lang.CharSequence);
method public android.view.autofill.Dataset build();
+ method public android.view.autofill.Dataset.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.Dataset.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.Dataset.Builder setExtras(android.os.Bundle);
method public android.view.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
}
method public android.view.autofill.FillResponse.Builder addDataset(android.view.autofill.Dataset);
method public android.view.autofill.FillResponse.Builder addSavableFields(android.view.autofill.AutoFillId...);
method public android.view.autofill.FillResponse build();
+ method public android.view.autofill.FillResponse.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.FillResponse.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle);
}
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
method public void onConnected();
+ method public void onDatasetAuthenticationRequest(android.os.Bundle, int);
method public void onDisconnected();
method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback);
+ method public void onFillResponseAuthenticationRequest(android.os.Bundle, int);
method public abstract void onSaveRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.SaveCallback);
field public static final java.lang.String EXTRA_DATASET_EXTRAS = "android.service.autofill.extra.DATASET_EXTRAS";
field public static final java.lang.String EXTRA_RESPONSE_EXTRAS = "android.service.autofill.extra.RESPONSE_EXTRAS";
+ field public static final int FLAG_AUTHENTICATION_ERROR = 4; // 0x4
+ field public static final int FLAG_AUTHENTICATION_REQUESTED = 1; // 0x1
+ field public static final int FLAG_AUTHENTICATION_SUCCESS = 2; // 0x2
+ field public static final int FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE = 8; // 0x8
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
field public static final java.lang.String SERVICE_META_DATA = "android.autofill";
}
public final class FillCallback {
+ method public void onDatasetAuthentication(android.view.autofill.Dataset, int);
method public void onFailure(java.lang.CharSequence);
+ method public void onFillResponseAuthentication(int);
method public void onSuccess(android.view.autofill.FillResponse);
}
public static final class Dataset.Builder {
ctor public Dataset.Builder(java.lang.CharSequence);
method public android.view.autofill.Dataset build();
+ method public android.view.autofill.Dataset.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.Dataset.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.Dataset.Builder setExtras(android.os.Bundle);
method public android.view.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
}
method public android.view.autofill.FillResponse.Builder addDataset(android.view.autofill.Dataset);
method public android.view.autofill.FillResponse.Builder addSavableFields(android.view.autofill.AutoFillId...);
method public android.view.autofill.FillResponse build();
+ method public android.view.autofill.FillResponse.Builder requiresCustomAuthentication(android.os.Bundle, int);
+ method public android.view.autofill.FillResponse.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle);
}
@Override // binder call
public void onAuthenticationFailed(long deviceId) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();;
+ mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
}
@Override // binder call
import android.os.Message;
import android.util.Log;
import android.view.autofill.AutoFillId;
+import android.view.autofill.Dataset;
import android.view.autofill.FillResponse;
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
-// TODO(b/33197203): improve javadoc (of both class and methods); in particular, make sure the
-// life-cycle (and how state could be maintained on server-side) is well documented.
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+//TODO(b/33197203): improve javadoc (of both class and methods); in particular, make sure the
+//life-cycle (and how state could be maintained on server-side) is well documented.
/**
* Top-level service of the current auto-fill service for a given user.
private static final String TAG = "AutoFillService";
static final boolean DEBUG = true; // TODO(b/33197203): set to false once stable
+ // TODO(b/33197203): check for device's memory size instead of DEBUG?
+ static final boolean DEBUG_PENDING_CALLBACKS = DEBUG;
+
/**
* The {@link Intent} that must be declared as handled by the service.
* To be supported, the service must also require the
// Internal bundle keys.
/** @hide */ public static final String KEY_CALLBACK = "callback";
/** @hide */ public static final String KEY_SAVABLE_IDS = "savable_ids";
+ /** @hide */ public static final String EXTRA_CRYPTO_OBJECT_ID = "crypto_object_id";
// Prefix for public bundle keys.
private static final String KEY_PREFIX = "android.service.autofill.extra.";
*/
public static final String EXTRA_DATASET_EXTRAS = KEY_PREFIX + "DATASET_EXTRAS";
+ /**
+ * Used to indicate the user selected an action that requires authentication.
+ */
+ public static final int FLAG_AUTHENTICATION_REQUESTED = 1 << 0;
+
+ /**
+ * Used to indicate the user authentication succeeded.
+ */
+ public static final int FLAG_AUTHENTICATION_SUCCESS = 1 << 1;
+
+ /**
+ * Used to indicate the user authentication failed.
+ */
+ public static final int FLAG_AUTHENTICATION_ERROR = 1 << 2;
+
+ /**
+ * Used when the service requested Fingerprint authentication but such option is not available.
+ */
+ public static final int FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE = 1 << 3;
+
// Handler messages.
private static final int MSG_CONNECT = 1;
- private static final int MSG_AUTO_FILL_ACTIVITY = 2;
- private static final int MSG_DISCONNECT = 3;
+ private static final int MSG_DISCONNECT = 2;
+ private static final int MSG_AUTO_FILL_ACTIVITY = 3;
+ private static final int MSG_AUTHENTICATE_FILL_RESPONSE = 4;
+ private static final int MSG_AUTHENTICATE_DATASET = 5;
private final IAutoFillService mInterface = new IAutoFillService.Stub() {
}
@Override
+ public void authenticateFillResponse(Bundle extras, int flags) {
+ final Message msg = mHandlerCaller.obtainMessage(MSG_AUTHENTICATE_FILL_RESPONSE);
+ msg.arg1 = flags;
+ msg.obj = extras;
+ mHandlerCaller.sendMessage(msg);
+ }
+
+ @Override
+ public void authenticateDataset(Bundle extras, int flags) {
+ final Message msg = mHandlerCaller.obtainMessage(MSG_AUTHENTICATE_DATASET);
+ msg.arg1 = flags;
+ msg.obj = extras;
+ mHandlerCaller.sendMessage(msg);
+ }
+
+ @Override
public void onConnected() {
mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_CONNECT));
}
final IAutoFillServerCallback callback = (IAutoFillServerCallback) args.arg3;
requestAutoFill(callback, structure, extras, flags);
break;
+ } case MSG_AUTHENTICATE_FILL_RESPONSE: {
+ final int flags = msg.arg1;
+ final Bundle extras = (Bundle) msg.obj;
+ onFillResponseAuthenticationRequest(extras, flags);
+ break;
+ } case MSG_AUTHENTICATE_DATASET: {
+ final int flags = msg.arg1;
+ final Bundle extras = (Bundle) msg.obj;
+ onDatasetAuthenticationRequest(extras, flags);
+ break;
} case MSG_DISCONNECT: {
onDisconnected();
break;
private HandlerCaller mHandlerCaller;
+ // User for debugging purposes
+ private final List<CallbackHelper.Dumpable> mPendingCallbacks =
+ DEBUG_PENDING_CALLBACKS ? new ArrayList<>() : null;
+
/**
* {@inheritDoc}
*
public abstract void onSaveRequest(AssistStructure structure,
Bundle data, CancellationSignal cancellationSignal, SaveCallback callback);
+ /**
+ * Called as result of the user action for a {@link FillResponse} that required authentication.
+ *
+ * <p>When the {@link FillResponse} required authentication through
+ * {@link android.view.autofill.FillResponse.Builder#requiresCustomAuthentication(Bundle, int)}, this
+ * call indicates the user is requesting the service to authenticate him/her (and {@code flags}
+ * contains {@link #FLAG_AUTHENTICATION_REQUESTED}), and {@code extras} contains the
+ * {@link Bundle} passed to that method.
+ *
+ * <p>When the {@link FillResponse} required authentication through
+ * {@link android.view.autofill.FillResponse.Builder#requiresFingerprintAuthentication(
+ * android.hardware.fingerprint.FingerprintManager.CryptoObject, Bundle, int)},
+ * {@code flags} this call contains the result of the fingerprint authentication (such as
+ * {@link #FLAG_AUTHENTICATION_SUCCESS}, {@link #FLAG_AUTHENTICATION_ERROR}, and
+ * {@link #FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}) and {@code extras} contains the
+ * {@link Bundle} passed to that method.
+ */
+ public void onFillResponseAuthenticationRequest(@SuppressWarnings("unused") Bundle extras,
+ int flags) {
+ if (DEBUG) Log.d(TAG, "onFillResponseAuthenticationRequest(): flags=" + flags);
+ }
+
+ /**
+ * Called as result of the user action for a {@link Dataset} that required authentication.
+ *
+ * <p>When the {@link Dataset} required authentication through
+ * {@link android.view.autofill.Dataset.Builder#requiresCustomAuthentication(Bundle, int)}, this
+ * call indicates the user is requesting the service to authenticate him/her (and {@code flags}
+ * contains {@link #FLAG_AUTHENTICATION_REQUESTED}), and {@code extras} contains the
+ * {@link Bundle} passed to that method.
+ *
+ * <p>When the {@link Dataset} required authentication through
+ * {@link android.view.autofill.Dataset.Builder#requiresFingerprintAuthentication(
+ * android.hardware.fingerprint.FingerprintManager.CryptoObject, Bundle, int)},
+ * {@code flags} this call contains the result of the fingerprint authentication (such as
+ * {@link #FLAG_AUTHENTICATION_SUCCESS}, {@link #FLAG_AUTHENTICATION_ERROR}, and
+ * {@link #FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}) and {@code extras} contains the
+ * {@link Bundle} passed to that method.
+ */
+ public void onDatasetAuthenticationRequest(@SuppressWarnings("unused") Bundle extras,
+ int flags) {
+ if (DEBUG) Log.d(TAG, "onDatasetAuthenticationRequest(): flags=" + flags);
+ }
+
+ // TODO(b/33197203): make it final and create another method classes could extend so it's
+ // guaranteed to dump the pending callbacks?
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mPendingCallbacks != null) {
+ pw.print("Number of pending callbacks: "); pw.println(mPendingCallbacks.size());
+ final String prefix = " ";
+ for (int i = 0; i < mPendingCallbacks.size(); i++) {
+ final CallbackHelper.Dumpable cb = mPendingCallbacks.get(i);
+ pw.print('#'); pw.print(i + 1); pw.println(':');
+ cb.dump(prefix, pw);
+ }
+ pw.println();
+ } else {
+ pw.println("Dumping disabled");
+ }
+ }
+
private void requestAutoFill(IAutoFillServerCallback callback, AssistStructure structure,
Bundle data, int flags) {
switch (flags) {
case AUTO_FILL_FLAG_TYPE_FILL:
final FillCallback fillCallback = new FillCallback(callback);
+ if (DEBUG_PENDING_CALLBACKS) {
+ addPendingCallback(fillCallback);
+ }
// TODO(b/33197203): hook up the cancelationSignal
onFillRequest(structure, data, new CancellationSignal(), fillCallback);
break;
case AUTO_FILL_FLAG_TYPE_SAVE:
final SaveCallback saveCallback = new SaveCallback(callback);
+ if (DEBUG_PENDING_CALLBACKS) {
+ addPendingCallback(saveCallback);
+ }
// TODO(b/33197203): hook up the cancelationSignal
onSaveRequest(structure, data, new CancellationSignal(), saveCallback);
break;
}
}
+ private void addPendingCallback(CallbackHelper.Dumpable callback) {
+ if (mPendingCallbacks == null) {
+ // Shouldn't happend since call is controlled by DEBUG_PENDING_CALLBACKS guard.
+ Log.wtf(TAG, "addPendingCallback(): mPendingCallbacks not set");
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, "Adding pending callback: " + callback);
+
+ callback.setFinalizer(() -> {
+ if (DEBUG) Log.d(TAG, "Removing pending callback: " + callback);
+ mPendingCallbacks.remove(callback);
+ });
+ mPendingCallbacks.add(callback);
+ }
+
/**
* Called when the Android system disconnects from the service.
*
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.autofill;
+
+import java.io.PrintWriter;
+
+final class CallbackHelper {
+
+ static interface Dumpable {
+ void dump(String prefix, PrintWriter pw);
+ void setFinalizer(Finalizer f);
+ }
+
+ static interface Finalizer {
+ void gone();
+ }
+}
package android.service.autofill;
import static android.service.autofill.AutoFillService.DEBUG;
+import static android.util.DebugUtils.flagsToString;
import android.annotation.Nullable;
import android.app.Activity;
import android.os.Bundle;
import android.os.RemoteException;
+import android.service.autofill.CallbackHelper.Dumpable;
+import android.service.autofill.CallbackHelper.Finalizer;
import android.util.Log;
+import android.view.autofill.Dataset;
import android.view.autofill.FillResponse;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import java.io.PrintWriter;
+
/**
* Handles auto-fill requests from the {@link AutoFillService} into the {@link Activity} being
* auto-filled.
+ *
+ * <p>This class is thread safe.
*/
-public final class FillCallback {
+public final class FillCallback implements Dumpable {
private static final String TAG = "FillCallback";
+ // NOTE: constants below are public so they can be used by flagsToString()
+ /** @hide */ public static final int STATE_INITIAL = 1 << 0;
+ /** @hide */ public static final int STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE = 1 << 1;
+ /** @hide */ public static final int STATE_WAITING_DATASET_AUTH_RESPONSE = 1 << 2;
+ /** @hide */ public static final int STATE_FINISHED_OK = 1 << 3;
+ /** @hide */ public static final int STATE_FINISHED_FAILURE = 1 << 4;
+ /** @hide */ public static final int STATE_FINISHED_ERROR = 1 << 5;
+ /** @hide */ public static final int STATE_FINISHED_AUTHENTICATED = 1 << 6;
+
private final IAutoFillServerCallback mCallback;
- private boolean mReplied = false;
+ @GuardedBy("mCallback")
+ private int mState = STATE_INITIAL;
+
+ @GuardedBy("mCallback")
+ private Finalizer mFinalizer;
/** @hide */
FillCallback(IAutoFillServerCallback callback) {
/**
* Notifies the Android System that an
- * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, FillCallback)}
- * was successfully fulfilled by the service.
+ * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure, Bundle,
+ * android.os.CancellationSignal, FillCallback)} was successfully fulfilled by the service.
*
* @param response auto-fill information for that activity, or {@code null} when the activity
- * cannot be auto-filled (for example, if it only contains read-only fields).
- *
- * @throws RuntimeException if an error occurred while calling the Android System.
+ * cannot be auto-filled (for example, if it only contains read-only fields). See
+ * {@link FillResponse} for examples.
*/
public void onSuccess(@Nullable FillResponse response) {
- if (DEBUG) Log.d(TAG, "onSuccess(): respose=" + response);
-
- checkNotRepliedYet();
-
- try {
- mCallback.showResponse(response);
- } catch (RemoteException e) {
- e.rethrowAsRuntimeException();
+ final boolean authRequired = response != null && response.isAuthRequired();
+
+ if (DEBUG) Log.d(TAG, "onSuccess(): authReq= " + authRequired + ", resp=" + response);
+
+ synchronized (mCallback) {
+ if (authRequired) {
+ assertOnStateLocked(STATE_INITIAL);
+ } else {
+ assertOnStateLocked(STATE_INITIAL | STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE
+ | STATE_WAITING_DATASET_AUTH_RESPONSE);
+ }
+
+ try {
+ mCallback.showResponse(response);
+ if (authRequired) {
+ mState = STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE;
+ } else {
+ // Check if at least one dataset requires authentication.
+ boolean waitingAuth = false;
+ if (response != null) {
+ for (Dataset dataset : response.getDatasets()) {
+ if (dataset.isAuthRequired()) {
+ waitingAuth = true;
+ break;
+ }
+ }
+ }
+ if (waitingAuth) {
+ mState = STATE_WAITING_DATASET_AUTH_RESPONSE;
+ } else {
+ setFinalStateLocked(STATE_FINISHED_OK);
+ }
+ }
+ } catch (RemoteException e) {
+ setFinalStateLocked(STATE_FINISHED_ERROR);
+ e.rethrowAsRuntimeException();
+ }
}
}
/**
* Notifies the Android System that an
- * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, FillCallback)}
+ * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure,
+ * Bundle, android.os.CancellationSignal, FillCallback)}
* could not be fulfilled by the service.
*
* @param message error message to be displayed to the user.
- *
- * @throws RuntimeException if an error occurred while calling the Android System.
*/
public void onFailure(CharSequence message) {
if (DEBUG) Log.d(TAG, "onFailure(): message=" + message);
- checkNotRepliedYet();
Preconditions.checkArgument(message != null, "message cannot be null");
- try {
- mCallback.showError(message.toString());
- } catch (RemoteException e) {
- e.rethrowAsRuntimeException();
+ synchronized (mCallback) {
+ assertOnStateLocked(STATE_INITIAL | STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE
+ | STATE_WAITING_DATASET_AUTH_RESPONSE);
+
+ try {
+ mCallback.showError(message);
+ setFinalStateLocked(STATE_FINISHED_FAILURE);
+ } catch (RemoteException e) {
+ setFinalStateLocked(STATE_FINISHED_ERROR);
+ e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Notifies the Android System when the user authenticated a {@link FillResponse} previously
+ * passed to {@link #onSuccess(FillResponse)}.
+ *
+ * @param flags must contain either
+ * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or
+ * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}.
+ */
+ public void onFillResponseAuthentication(int flags) {
+ if (DEBUG) Log.d(TAG, "onFillResponseAuthentication(): flags=" + flags);
+
+ synchronized (mCallback) {
+ assertOnStateLocked(STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE);
+
+ try {
+ mCallback.unlockFillResponse(flags);
+ setFinalStateLocked(STATE_FINISHED_AUTHENTICATED);
+ } catch (RemoteException e) {
+ setFinalStateLocked(STATE_FINISHED_ERROR);
+ e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ /**
+ * Notifies the Android System when the user authenticated a {@link Dataset} previously passed
+ * to {@link #onSuccess(FillResponse)}.
+ *
+ * @param dataset values to fill the activity with in case of successful authentication of a
+ * previously locked (and empty) dataset).
+ * @param flags must contain either
+ * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or
+ * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}.
+ */
+ public void onDatasetAuthentication(@Nullable Dataset dataset, int flags) {
+ if (DEBUG) Log.d(TAG, "onDatasetAuthentication(): dataset=" + dataset + ", flags=" + flags);
+
+ synchronized (mCallback) {
+ assertOnStateLocked(STATE_WAITING_DATASET_AUTH_RESPONSE);
+
+ try {
+ mCallback.unlockDataset(dataset, flags);
+ setFinalStateLocked(STATE_FINISHED_AUTHENTICATED);
+ } catch (RemoteException e) {
+ setFinalStateLocked(STATE_FINISHED_ERROR);
+ e.rethrowAsRuntimeException();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (!DEBUG) return super.toString();
+
+ return "FillCallback: [mState = " + mState + "]";
+ }
+
+ /** @hide */
+ @Override
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("FillCallback: mState="); pw.println(mState);
+ }
+
+ /** @hide */
+ @Override
+ public void setFinalizer(Finalizer f) {
+ synchronized (mCallback) {
+ mFinalizer = f;
}
}
- // There can be only one!!
- private void checkNotRepliedYet() {
- Preconditions.checkState(!mReplied, "already replied");
- mReplied = true;
+ /**
+ * Sets a final state (where the callback cannot be used anymore) and notifies the
+ * {@link Finalizer} (if any).
+ */
+ private void setFinalStateLocked(int state) {
+ if (DEBUG) Log.d(TAG, "setFinalState(): " + state);
+ mState = state;
+
+ if (mFinalizer != null) {
+ mFinalizer.gone();
+ }
+ }
+
+ // TODO(b/33197203): move and/or re-add state check logic on server side to avoid malicious app
+ // calling the callback on wrong state.
+
+ // Make sure callback method is called during the proper lifecycle state.
+ private void assertOnStateLocked(int flags) {
+ if (DEBUG) Log.d(TAG, "assertOnState(): current=" + mState + ", required=" + flags);
+
+ Preconditions.checkState((flags & mState) != 0,
+ "invalid state: required " + flagsToString(FillCallback.class, "STATE_", flags)
+ + ", current is " + flagsToString(FillCallback.class, "STATE_", mState));
}
}
import android.view.autofill.Dataset;
/**
+ * Object running in the application process and responsible for auto-filling it.
+ *
* @hide
*/
+// TODO(b/33197203): rename methods to make them more consistent with a callback, or rename class
+// itself
oneway interface IAutoFillAppCallback {
+ /**
+ * Auto-fills the activity with the contents of a dataset.
+ */
void autoFill(in Dataset dataset);
}
import java.util.List;
+import android.os.Bundle;
import android.view.autofill.AutoFillId;
+import android.view.autofill.Dataset;
import android.view.autofill.FillResponse;
/**
+ * Object running in the AutoFillService process and used to communicate back with system_server.
+ *
* @hide
*/
+// TODO(b/33197203): rename methods to make them more consistent with a callback, or rename class
+// itself
oneway interface IAutoFillServerCallback {
+ // TODO(b/33197203): document methods
void showResponse(in FillResponse response);
- void showError(String message);
+ void showError(CharSequence message);
void highlightSavedFields(in AutoFillId[] ids);
+ void unlockFillResponse(int flags);
+ void unlockDataset(in Dataset dataset, int flags);
}
/**
* @hide
*/
+// TODO(b/33197203): document class and methods
oneway interface IAutoFillService {
+ // TODO(b/33197203): rename method to make them more consistent
void autoFill(in AssistStructure structure, in IAutoFillServerCallback callback,
in Bundle extras, int flags);
+ void authenticateFillResponse(in Bundle extras, int flags);
+ void authenticateDataset(in Bundle extras, int flags);
void onConnected();
void onDisconnected();
}
import android.app.assist.AssistStructure.ViewNode;
import android.os.Bundle;
import android.os.RemoteException;
+import android.service.autofill.CallbackHelper.Dumpable;
+import android.service.autofill.CallbackHelper.Finalizer;
import android.util.Log;
import android.view.autofill.AutoFillId;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import java.io.PrintWriter;
+
/**
* Handles save requests from the {@link AutoFillService} into the {@link Activity} being
* auto-filled.
+ *
+ * <p>This class is thread safe.
*/
-public final class SaveCallback {
+public final class SaveCallback implements Dumpable {
private static final String TAG = "SaveCallback";
private final IAutoFillServerCallback mCallback;
+ @GuardedBy("mCallback")
private boolean mReplied = false;
+ @GuardedBy("mCallback")
+ private Finalizer mFinalizer;
+
/** @hide */
SaveCallback(IAutoFillServerCallback callback) {
mCallback = callback;
/**
* Notifies the Android System that an
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, SaveCallback)}
- * was successfully fulfilled by the service.
+ * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
+ * android.os.CancellationSignal, SaveCallback)} was successfully fulfilled by the service.
*
* @param ids ids ({@link ViewNode#getAutoFillId()}) of the fields that were saved.
*
if (DEBUG) Log.d(TAG, "onSuccess(): ids=" + ((ids == null) ? "null" : ids.length));
Preconditions.checkArgument(ids != null, "ids cannot be null");
- checkNotRepliedYet();
-
Preconditions.checkArgument(ids.length > 0, "ids cannot be empty");
- try {
- mCallback.highlightSavedFields(ids);
- } catch (RemoteException e) {
- e.rethrowAsRuntimeException();
+ synchronized (mCallback) {
+ checkNotRepliedYetLocked();
+ try {
+ mCallback.highlightSavedFields(ids);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } finally {
+ setRepliedLocked();
+ }
}
}
/**
* Notifies the Android System that an
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, SaveCallback)}
- * could not be fulfilled by the service.
+ * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
+ * android.os.CancellationSignal, SaveCallback)} could not be fulfilled by the service.
*
* @param message error message to be displayed to the user.
*
if (DEBUG) Log.d(TAG, "onFailure(): message=" + message);
Preconditions.checkArgument(message != null, "message cannot be null");
- checkNotRepliedYet();
- try {
- mCallback.showError(message.toString());
- } catch (RemoteException e) {
- e.rethrowAsRuntimeException();
+ synchronized (mCallback) {
+ checkNotRepliedYetLocked();
+
+ try {
+ mCallback.showError(message);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } finally {
+ setRepliedLocked();
+ }
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void dump(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("SaveCallback: mReplied="); pw.println(mReplied);
+ }
+
+ /** @hide */
+ @Override
+ public void setFinalizer(Finalizer f) {
+ synchronized (mCallback) {
+ mFinalizer = f;
}
}
+ @Override
+ public String toString() {
+ if (!DEBUG) return super.toString();
+
+ return "SaveCallback: [mReplied= " + mReplied + "]";
+ }
+
// There can be only one!!
- private void checkNotRepliedYet() {
+ private void checkNotRepliedYetLocked() {
Preconditions.checkState(!mReplied, "already replied");
+ }
+
+ private void setRepliedLocked() {
+ if (DEBUG) Log.d(TAG, "setReplied()");
+
mReplied = true;
+
+ if (mFinalizer != null) {
+ mFinalizer.gone();
+ }
}
}
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.SystemClock;
-import android.text.format.DateUtils;
import com.android.internal.util.XmlUtils;
import static android.view.autofill.Helper.DEBUG;
import static android.view.autofill.Helper.append;
+import android.annotation.Nullable;
import android.app.Activity;
import android.app.assist.AssistStructure.ViewNode;
+import android.hardware.fingerprint.FingerprintManager.CryptoObject;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.service.autofill.AutoFillService;
import com.android.internal.util.Preconditions;
private final CharSequence mName;
private final ArrayList<DatasetField> mFields;
private final Bundle mExtras;
+ private final int mFlags;
+ private final boolean mRequiresAuth;
+ private final boolean mHasCryptoObject;
+ private final long mCryptoOpId;
private Dataset(Dataset.Builder builder) {
mName = builder.mName;
// TODO(b/33197203): make an immutable copy of mFields?
mFields = builder.mFields;
mExtras = builder.mExtras;
+ mFlags = builder.mFlags;
+ mRequiresAuth = builder.mRequiresAuth;
+ mHasCryptoObject = builder.mHasCryptoObject;
+ mCryptoOpId = builder.mCryptoOpId;
}
/** @hide */
return mExtras;
}
+ /** @hide */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /** @hide */
+ public boolean isAuthRequired() {
+ return mRequiresAuth;
+ }
+
+ /** @hide */
+ public boolean isEmpty() {
+ return mFields.isEmpty();
+ }
+
+ /** @hide */
+ public boolean hasCryptoObject() {
+ return mHasCryptoObject;
+ }
+
+ /** @hide */
+ public long getCryptoObjectOpId() {
+ return mCryptoOpId;
+ }
+
@Override
public String toString() {
if (!DEBUG) return super.toString();
final StringBuilder builder = new StringBuilder("Dataset [name=").append(mName)
.append(", fields=").append(mFields).append(", extras=");
- append(builder, mExtras);
+ append(builder, mExtras)
+ .append(", flags=").append(mFlags)
+ .append(", requiresAuth: ").append(mRequiresAuth)
+ .append(", hasCrypto: ").append(mHasCryptoObject);
return builder.append(']').toString();
}
private CharSequence mName;
private final ArrayList<DatasetField> mFields = new ArrayList<>();
private Bundle mExtras;
+ private int mFlags;
+ private boolean mRequiresAuth;
+ private boolean mHasCryptoObject;
+ private long mCryptoOpId;
/**
* Creates a new builder.
}
/**
+ * Requires dataset authentication through the {@link
+ * android.service.autofill.AutoFillService} before auto-filling the activity with this
+ * dataset.
+ *
+ * <p>This method is typically called when the device (or the service) does not support
+ * fingerprint authentication (and hence it cannot use {@link
+ * #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs
+ * to use a custom authentication UI for the dataset. For example, when a dataset contains
+ * credit card information (such as number, expiration date, and verification code), the
+ * service displays an authentication dialog asking for the verification code to unlock the
+ * rest of the data).
+ *
+ * <p>Since the dataset is "locked" until the user authenticates it, typically this dataset
+ * name is masked (for example, "VISA....1234").
+ *
+ * <p>When the user selects this dataset, the Android System calls {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
+ * passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in
+ * the flags and the same {@code extras} passed to this method. The service can then
+ * displays its custom authentication UI, and then call the proper method on {@link
+ * android.service.autofill.FillCallback} depending on the authentication result and whether
+ * this dataset already contains the fields needed to auto-fill the activity:
+ *
+ * <ul>
+ * <li>If authentication failed, call
+ * {@link android.service.autofill.FillCallback#onDatasetAuthentication(Dataset,
+ * int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags.
+ * <li>If authentication succeeded and this datast is empty (no fields), call {@link
+ * android.service.autofill.FillCallback#onSuccess(FillResponse)} with a new dataset
+ * (with the proper fields).
+ * <li>If authentication succeeded and this response is not empty, call {@link
+ * android.service.autofill.FillCallback#onDatasetAuthentication(Dataset, int)}
+ * passing
+ * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the
+ * {@code flags} and {@code null} as the {@code dataset}.
+ * </ul>
+ *
+ * @param extras when set, will be passed back in the {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle,
+ * int)}, call so it could be used by the service to handle state.
+ * @param flags optional parameters, currently ignored.
+ */
+ public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) {
+ return requiresAuthentication(null, extras, flags);
+ }
+
+ /**
+ * Requires dataset authentication through the Fingerprint sensor before auto-filling the
+ * activity with this dataset.
+ *
+ * <p>This method is typically called when the dataset contains sensitive information (for
+ * example, credit card information) and the provider requires the user to re-authenticate
+ * before using it.
+ *
+ * <p>Since the dataset is "locked" until the user authenticates it, typically this dataset
+ * name is masked (for example, "VISA....1234").
+ *
+ * <p>When the user selects this dataset, the Android System displays an UI affordance
+ * asking the user to use the fingerprint sensor unlock the dataset, and what happens after
+ * a successful fingerprint authentication depends on whether the dataset is empty (no
+ * fields, only the masked name) or not:
+ *
+ * <ul>
+ * <li>If it's empty, the Android System will call {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle,
+ * int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the
+ * flags.
+ * <li>If it's not empty, the activity will be auto-filled with its data.
+ * </ul>
+ *
+ * <p>If the fingerprint authentication fails, the Android System will call {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
+ * passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the
+ * flags.
+ *
+ * <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use
+ * the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint
+ * authentication is available before using this method, and use other alternatives (such as
+ * {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is
+ * called when fingerprint is not available, Android System will call {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
+ * passing {@link
+ * android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}
+ * in the flags, but it would be wasting system resources (and worsening the user
+ * experience) in the process.
+ *
+ * @param crypto object that will be authenticated.
+ * @param extras when set, will be passed back in the {@link
+ * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
+ * call so it could be used by the service to handle state.
+ * @param flags optional parameters, currently ignored.
+ */
+ public Builder requiresFingerprintAuthentication(CryptoObject crypto,
+ @Nullable Bundle extras, int flags) {
+ // TODO(b/33197203): should we allow crypto to be null?
+ Preconditions.checkArgument(crypto != null, "must pass a CryptoObject");
+ return requiresAuthentication(crypto, extras, flags);
+ }
+
+ private Builder requiresAuthentication(CryptoObject cryptoObject, Bundle extras,
+ int flags) {
+ // There can be only one!
+ Preconditions.checkState(!mRequiresAuth,
+ "requires-authentication methods already called");
+ // TODO(b/33197203): make sure that either this method or setExtras() is called, but
+ // not both
+ mExtras = extras;
+ mFlags = flags;
+ mRequiresAuth = true;
+ if (cryptoObject != null) {
+ mHasCryptoObject = true;
+ mCryptoOpId = cryptoObject.getOpId();
+ }
+ return this;
+ }
+
+ /**
* Sets the value of a field.
*
* @param id id returned by {@link ViewNode#getAutoFillId()}.
}
/**
- * Sets a {@link Bundle} that will be passed to subsequent calls to {@link AutoFillService}
- * methods such as
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
- * android.os.CancellationSignal, android.service.autofill.SaveCallback)}, using
- * {@link AutoFillService#EXTRA_DATASET_EXTRAS} as the key.
+ * Sets a {@link Bundle} that will be passed to subsequent calls to
+ * {@link android.service.autofill.AutoFillService} methods such as
+ * {@link android.service.autofill.AutoFillService#onSaveRequest(android.app.assist.AssistStructure,
+ * Bundle, android.os.CancellationSignal, android.service.autofill.SaveCallback)}, using
+ * {@link android.service.autofill.AutoFillService#EXTRA_DATASET_EXTRAS} as the key.
*
* <p>It can be used to keep service state in between calls.
*/
public Builder setExtras(Bundle extras) {
+ // TODO(b/33197203): make sure that either this method or the requires-Authentication
+ // ones are called, but not both
mExtras = Objects.requireNonNull(extras, "extras cannot be null");
return this;
}
parcel.writeCharSequence(mName);
parcel.writeList(mFields);
parcel.writeBundle(mExtras);
+ parcel.writeInt(mFlags);
+ parcel.writeInt(mRequiresAuth ? 1 : 0);
+ parcel.writeInt(mHasCryptoObject ? 1 : 0);
+ if (mHasCryptoObject) {
+ parcel.writeLong(mCryptoOpId);
+ }
}
@SuppressWarnings("unchecked")
mName = parcel.readCharSequence();
mFields = parcel.readArrayList(null);
mExtras = parcel.readBundle();
+ mFlags = parcel.readInt();
+ mRequiresAuth = parcel.readInt() == 1;
+ mHasCryptoObject = parcel.readInt() == 1;
+ mCryptoOpId = mHasCryptoObject ? parcel.readLong() : 0;
}
public static final Parcelable.Creator<Dataset> CREATOR = new Parcelable.Creator<Dataset>() {
import static android.view.autofill.Helper.DEBUG;
import static android.view.autofill.Helper.append;
+import android.annotation.Nullable;
import android.app.Activity;
+import android.hardware.fingerprint.FingerprintManager.CryptoObject;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.service.autofill.AutoFillService;
+import android.service.autofill.FillCallback;
import com.android.internal.util.Preconditions;
import java.util.Set;
/**
- * Response for a
- * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure, Bundle,
- * android.os.CancellationSignal, android.service.autofill.FillCallback)}
- * request.
+ * Response for a {@link
+ * android.service.autofill.AutoFillService#onFillRequest(android.app.assist.AssistStructure,
+ * Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback)} request.
*
* <p>The response typically contains one or more {@link Dataset}s, each representing a set of
- * fields that can be auto-filled together. For example, for a login page with username/password
- * where the user only have one account in the service, the response could be:
+ * fields that can be auto-filled together, and the Android System displays a dataset picker UI
+ * affordance that the user must use before the {@link Activity} is filled with the dataset.
+ *
+ * <p>For example, for a login page with username/password where the user only has one account in
+ * the service, the response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .build();
* </pre>
*
- * <p>If the user does not have any data associated with this {@link Activity} but the service
- * wants to offer the user the option to save the data that was entered, then the service could
- * populate the response with {@code savableIds} instead of {@link Dataset}s:
+ * <p>If the user does not have any data associated with this {@link Activity} but the service wants
+ * to offer the user the option to save the data that was entered, then the service could populate
+ * the response with {@code savableIds} instead of {@link Dataset}s:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
*
* <p>Similarly, there might be cases where the user data on the service is enough to populate some
* fields but not all, and the service would still be interested on saving the other fields. In this
- * scenario, the service could populate the response with both {@link Dataset}s and
- * {@code savableIds}:
+ * scenario, the service could populate the response with both {@link Dataset}s and {@code
+ * savableIds}:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* <p>If the service has multiple {@link Dataset}s with multiple options for some fields on each
* dataset (for example, multiple accounts with both a home and work address), then it should
* "partition" the {@link Activity} in sections and populate the response with just a subset of the
- * data that would fulfill the first section; then once the user fills the first section and taps
- * a field from the next section, the Android system would issue another request for that section,
- * and so on. For example, the first response could be:
+ * data that would fulfill the first section; then once the user fills the first section and taps a
+ * field from the next section, the Android system would issue another request for that section, and
+ * so on. For example, the first response could be:
*
* <pre class="prettyprint">
* new FillResponse.Builder()
* .build();
* </pre>
*
- * <p>Finally, the service can use the {@link FillResponse.Builder#setExtras(Bundle)} and/or
- * {@link Dataset.Builder#setExtras(Bundle)} methods to pass
- * a {@link Bundle} with service-specific data use to identify this response on future calls (like
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
- * android.os.CancellationSignal, android.service.autofill.SaveCallback)}) - such bundle will be
- * available as the {@link AutoFillService#EXTRA_RESPONSE_EXTRAS} extra in
- * that method's {@code extras} argument.
+ * <p>The service could require user authentication, either at the {@link FillResponse} or {@link
+ * Dataset} levels, prior to auto-filling the activity - see {@link
+ * FillResponse.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, {@link
+ * FillResponse.Builder#requiresCustomAuthentication(Bundle, int)}, {@link
+ * Dataset.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, and {@link
+ * Dataset.Builder#requiresCustomAuthentication(Bundle, int)} for details.
+ *
+ * <p>Finally, the service can use the {@link FillResponse.Builder#setExtras(Bundle)} and/or {@link
+ * Dataset.Builder#setExtras(Bundle)} methods to pass {@link Bundle}s with service-specific data use
+ * to identify this response on future calls (like {@link
+ * android.service.autofill.AutoFillService#onSaveRequest(android.app.assist.AssistStructure,
+ * Bundle, android.os.CancellationSignal, android.service.autofill.SaveCallback)}) - such bundles
+ * will be available as the {@link android.service.autofill.AutoFillService#EXTRA_RESPONSE_EXTRAS}
+ * and {@link android.service.autofill.AutoFillService#EXTRA_DATASET_EXTRAS} extras in that method's
+ * {@code extras} argument.
*/
public final class FillResponse implements Parcelable {
private final List<Dataset> mDatasets;
private final AutoFillId[] mSavableIds;
private final Bundle mExtras;
+ private final int mFlags;
+ private final boolean mRequiresAuth;
+ private final boolean mHasCryptoObject;
+ private final long mCryptoOpId;
private FillResponse(Builder builder) {
// TODO(b/33197203): make it immutable?
mSavableIds[i++] = id;
}
mExtras = builder.mExtras;
+ mFlags = builder.mFlags;
+ mRequiresAuth = builder.mRequiresAuth;
+ mHasCryptoObject = builder.mHasCryptoObject;
+ mCryptoOpId = builder.mCryptoOpId;
}
/** @hide */
return mExtras;
}
+ /** @hide */
+ public int getFlags() {
+ return mFlags;
+ }
+
+ /** @hide */
+ public boolean isAuthRequired() {
+ return mRequiresAuth;
+ }
+
+ /** @hide */
+ public boolean hasCryptoObject() {
+ return mHasCryptoObject;
+ }
+
+ /** @hide */
+ public long getCryptoObjectOpId() {
+ return mCryptoOpId;
+ }
+
/**
* Builder for {@link FillResponse} objects.
*/
public static final class Builder {
private final List<Dataset> mDatasets = new ArrayList<>();
private final Set<AutoFillId> mSavableIds = new HashSet<>();
- private Bundle mExtras;
+ private Bundle mExtras;
+ private int mFlags;
+ private boolean mRequiresAuth;
+ private boolean mHasCryptoObject;
+ private long mCryptoOpId;
+
+ /**
+ * Requires user authentication through the {@link android.service.autofill.AutoFillService}
+ * before handling an auto-fill request.
+ *
+ * <p>This method is typically called when the device (or the service) does not support
+ * fingerprint authentication (and hence it cannot use {@link
+ * #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs
+ * to use a custom authentication UI and is used in 2 scenarios:
+ *
+ * <ol>
+ * <li>When the user data is encrypted and the service must authenticate an object that
+ * will be used to decrypt it.
+ * <li>When the service already acquired the user data but wants to confirm the user's
+ * identity before the activity is filled with it.
+ * </ol>
+ *
+ * <p>When this method is called, the Android System displays an UI affordance asking the
+ * user to tap it to auto-fill the activity; if the user taps it, the Android System calls
+ * {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in the flags and
+ * the same {@code extras} passed to this method. The service can then displays its custom
+ * authentication UI, and then call the proper method on {@link FillCallback} depending on
+ * the authentication result and whether this response already contains the {@link Dataset}s
+ * need to auto-fill the activity:
+ *
+ * <ul>
+ * <li>If authentication failed, call {@link
+ * FillCallback#onFillResponseAuthentication(int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags.
+ * <li>If authentication succeeded and this response is empty (no datasets), call {@link
+ * FillCallback#onSuccess(FillResponse)} with a new dataset (that does not require
+ * authentication).
+ * <li>If authentication succeeded and this response is not empty, call {@link
+ * FillCallback#onFillResponseAuthentication(int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the flags.
+ * </ul>
+ *
+ * @param extras when set, will be passed back in the {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} call so it could be used by the service to handle state.
+ * @param flags optional parameters, currently ignored.
+ */
+ public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) {
+ return requiresAuthentication(null, extras, flags);
+ }
+
+ /**
+ * Requires user authentication through the Fingerprint sensor before handling an auto-fill
+ * request.
+ *
+ * <p>The {@link android.service.autofill.AutoFillService} typically uses this method in 2
+ * situations:
+ *
+ * <ol>
+ * <li>When the user data is encrypted and the service must authenticate an object that
+ * will be used to decrypt it.
+ * <li>When the service already acquired the user data but wants to confirm the user's
+ * identity before the activity is filled with it.
+ * </ol>
+ *
+ * <p>When this method is called, the Android System displays an UI affordance asking the
+ * user to use the fingerprint sensor to auto-fill the activity, and what happens after a
+ * successful fingerprint authentication depends on the number of {@link Dataset}s included
+ * in this response:
+ *
+ * <ul>
+ * <li>If it's empty (scenario #1 above), the Android System will call {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the
+ * flags.
+ * <li>If it contains one dataset, the activity will be auto-filled right away.
+ * <li>If it contains many datasets, the Android System will show dataset picker UI, and
+ * then auto-fill the activity once the user select the proper datased.
+ * </ul>
+ *
+ * <p>If the fingerprint authentication fails, the Android System will call {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR}
+ * in the flags.
+ *
+ * <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use
+ * the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint
+ * authentication is available before using this method, and use other alternatives (such as
+ * {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is
+ * called when fingerprint is not available, Android System will call {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} passing {@link
+ * android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}
+ * in the flags, but it would be wasting system resources (and worsening the user
+ * experience) in the process.
+ *
+ * @param crypto object that will be authenticated.
+ * @param extras when set, will be passed back in the {@link
+ * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
+ * int)} call so it could be used by the service to handle state.
+ * @param flags optional parameters, currently ignored.
+ */
+ public Builder requiresFingerprintAuthentication(CryptoObject crypto,
+ @Nullable Bundle extras, int flags) {
+ // TODO(b/33197203): should we allow crypto to be null?
+ Preconditions.checkArgument(crypto != null, "must pass a CryptoObject");
+ return requiresAuthentication(crypto, extras, flags);
+ }
+
+ private Builder requiresAuthentication(CryptoObject cryptoObject, Bundle extras,
+ int flags) {
+ // There can be only one!
+ Preconditions.checkState(!mRequiresAuth,
+ "requires-authentication methods already called");
+ // TODO(b/33197203): make sure that either this method or setExtras() is called, but
+ // not both
+ mExtras = extras;
+ mFlags = flags;
+ mRequiresAuth = true;
+ if (cryptoObject != null) {
+ mHasCryptoObject = true;
+ mCryptoOpId = cryptoObject.getOpId();
+ }
+ return this;
+ }
/**
* Adds a new {@link Dataset} to this response.
public Builder addDataset(Dataset dataset) {
Preconditions.checkNotNull(dataset, "dataset cannot be null");
// TODO(b/33197203): check if name already exists
- // TODO(b/33197203): check if authId already exists (and update javadoc)
mDatasets.add(dataset);
for (DatasetField field : dataset.getFields()) {
mSavableIds.add(field.getId());
/**
* Adds ids of additional fields that the service would be interested to save (through
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
- * android.os.CancellationSignal, android.service.autofill.SaveCallback)}) but were not
- * indirectly set through {@link #addDataset(Dataset)}.
+ * {@link android.service.autofill.AutoFillService#onSaveRequest(
+ * android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal,
+ * android.service.autofill.SaveCallback)}) but were not indirectly set through {@link
+ * #addDataset(Dataset)}.
*
* <p>See {@link FillResponse} for examples.
*/
- public Builder addSavableFields(AutoFillId...ids) {
+ public Builder addSavableFields(AutoFillId... ids) {
for (AutoFillId id : ids) {
mSavableIds.add(id);
}
}
/**
- * Sets a {@link Bundle} that will be passed to subsequent calls to {@link AutoFillService}
- * methods such as
- * {@link AutoFillService#onSaveRequest(android.app.assist.AssistStructure, Bundle,
- * android.os.CancellationSignal, android.service.autofill.SaveCallback)}, using
- * {@link AutoFillService#EXTRA_RESPONSE_EXTRAS} as the key.
+ * Sets a {@link Bundle} that will be passed to subsequent calls to {@link
+ * android.service.autofill.AutoFillService} methods such as {@link
+ * android.service.autofill.AutoFillService#onSaveRequest(
+ * android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal,
+ * android.service.autofill.SaveCallback)}, using {@link
+ * android.service.autofill.AutoFillService#EXTRA_RESPONSE_EXTRAS} as the key.
*
* <p>It can be used when to keep service state in between calls.
*/
public Builder setExtras(Bundle extras) {
+ // TODO(b/33197203): make sure that either this method or the requires-Authentication
+ // ones are called, but not both
mExtras = Objects.requireNonNull(extras, "extras cannot be null");
return this;
}
final StringBuilder builder = new StringBuilder("FillResponse: [datasets=")
.append(mDatasets).append(", savableIds=").append(Arrays.toString(mSavableIds))
.append(", extras=");
- append(builder, mExtras);
+ append(builder, mExtras)
+ .append(", flags=").append(mFlags)
+ .append(", requiresAuth: ").append(mRequiresAuth)
+ .append(", hasCrypto: ").append(mHasCryptoObject);
return builder.append(']').toString();
}
parcel.writeList(mDatasets);
parcel.writeParcelableArray(mSavableIds, 0);
parcel.writeBundle(mExtras);
+ parcel.writeInt(mFlags);
+ parcel.writeInt(mRequiresAuth ? 1 : 0);
+ parcel.writeInt(mHasCryptoObject ? 1 : 0);
+ if (mHasCryptoObject) {
+ parcel.writeLong(mCryptoOpId);
+ }
}
private FillResponse(Parcel parcel) {
parcel.readList(mDatasets, null);
mSavableIds = parcel.readParcelableArray(null, AutoFillId.class);
mExtras = parcel.readBundle();
+ mFlags = parcel.readInt();
+ mRequiresAuth = parcel.readInt() == 1;
+ mHasCryptoObject = parcel.readInt() == 1;
+ mCryptoOpId = mHasCryptoObject ? parcel.readLong() : 0;
}
public static final Parcelable.Creator<FillResponse> CREATOR =
import android.os.Bundle;
+import java.util.Arrays;
+import java.util.Objects;
import java.util.Set;
/** @hide */
public final class Helper {
- // TODO(b/33197203): set to false when stable
- static final boolean DEBUG = true;
+ static final boolean DEBUG = true; // TODO(b/33197203): set to false when stable
static final String REDACTED = "[REDACTED]";
- static void append(StringBuilder builder, Bundle bundle) {
+ static StringBuilder append(StringBuilder builder, Bundle bundle) {
if (bundle == null) {
builder.append("N/A");
} else if (!DEBUG) {
builder.append(REDACTED);
} else {
final Set<String> keySet = bundle.keySet();
- builder.append("[bundle with ").append(keySet.size()).append(" extras:");
+ builder.append("[Bundle with ").append(keySet.size()).append(" extras:");
for (String key : keySet) {
- builder.append(' ').append(key).append('=').append(bundle.get(key)).append(',');
+ final Object value = bundle.get(key);
+ builder.append(' ').append(key).append('=');
+ builder.append((value instanceof Object[])
+ ? Arrays.toString((Objects[]) value) : value);
}
builder.append(']');
}
+ return builder;
}
private Helper() {
}
}
}
-
};
private HandlerCaller mHandlerCaller;
(new AutoFillManagerServiceShellCommand(this)).exec(
this, in, out, err, args, callback, resultReceiver);
}
-
}
private final class SettingsObserver extends ContentObserver {
package com.android.server.autofill;
-import static com.android.server.autofill.AutoFillManagerService.DEBUG;
+import static com.android.server.autofill.Helper.DEBUG;
+import static com.android.server.autofill.Helper.bundleToString;
+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 android.annotation.Nullable;
import android.app.Activity;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
-import android.icu.text.DateFormat;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.IFingerprintService;
+import android.hardware.fingerprint.IFingerprintServiceReceiver;
+import android.os.Binder;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.service.autofill.AutoFillService;
import android.service.autofill.IAutoFillServerCallback;
import android.service.autofill.IAutoFillService;
import android.service.voice.VoiceInteractionSession;
+import android.util.Log;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseArray;
import java.io.PrintWriter;
import java.util.Arrays;
-import java.util.Date;
import java.util.LinkedList;
import java.util.List;
private static final String TAG = "AutoFillManagerServiceImpl";
/** Used do assign ids to new ServerCallback instances. */
- private static int sServerCallbackCounter = 0;
+ private static int sSessionIdCounter = 0;
private final int mUserId;
private final int mUid;
};
/**
- * Cache of pending ServerCallbacks, keyed by {@link ServerCallback#id}.
+ * Cache of pending {@link Session}s, keyed by {@link Session#mId}.
*
- * <p>They're kept until the AutoFillService handles a request, or an error occurs.
+ * <p>They're kept until the {@link AutoFillService} finished handling a request, an error
+ * occurs, or the session times out.
*/
- // TODO(b/33197203): need to make sure service is bound while callback is pending
+ // TODO(b/33197203): need to make sure service is bound while callback is pending and/or
+ // use WeakReference
@GuardedBy("mLock")
- private static final SparseArray<ServerCallback> mServerCallbacks = new SparseArray<>();
+ private static final SparseArray<Session> mSessions = new SparseArray<>();
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
}
};
-
/**
* Receiver of assist data from the app's {@link Activity}, uses the {@code resultData} as
- * the {@link ServerCallback#id}.
+ * the {@link Session#mId}.
*/
private final IResultReceiver mAssistReceiver = new IResultReceiver.Stub() {
@Override
.getParcelable(VoiceInteractionSession.KEY_STRUCTURE);
final int flags = resultData.getInt(VoiceInteractionSession.KEY_FLAGS, 0);
- final ServerCallback serverCallback;
+ final Session session;
synchronized (mLock) {
- serverCallback = mServerCallbacks.get(resultCode);
- if (serverCallback == null) {
+ session = mSessions.get(resultCode);
+ if (session == null) {
Slog.w(TAG, "no server callback for id " + resultCode);
return;
}
- serverCallback.appCallback = IAutoFillAppCallback.Stub.asInterface(appBinder);
+ session.mAppCallback = IAutoFillAppCallback.Stub.asInterface(appBinder);
}
- mService.autoFill(structure, serverCallback, serverCallback.extras, flags);
+ mService.autoFill(structure, session.mServerCallback, session.mExtras, flags);
}
};
@GuardedBy("mLock")
private IAutoFillService mService;
+ @GuardedBy("mLock")
private boolean mBound;
+ @GuardedBy("mLock")
private boolean mValid;
// Estimated time when the service will be evicted from the cache.
activityToken = topActivities.get(0);
}
- final String historyItem =
- DateFormat.getDateTimeInstance().format(new Date()) + " - " + activityToken;
+ final String historyItem = TimeUtils.formatForLogging(System.currentTimeMillis())
+ + " - " + activityToken;
synchronized (mLock) {
mRequestHistory.add(historyItem);
requestAutoFillLocked(activityToken, extras, flags, true);
return;
}
- final int callbackId = ++sServerCallbackCounter;
- final ServerCallback serverCallback = new ServerCallback(callbackId, extras);
- mServerCallbacks.put(callbackId, serverCallback);
+ final int sessionId = ++sSessionIdCounter;
+ final Session session = new Session(sessionId, extras);
+ mSessions.put(sessionId, session);
/*
* TODO(b/33197203): apply security checks below:
*/
try {
// TODO(b/33197203): add MetricsLogger call
- if (!mAm.requestAutoFillData(mAssistReceiver, null, callbackId, activityToken, flags)) {
+ if (!mAm.requestAutoFillData(mAssistReceiver, null, sessionId, activityToken, flags)) {
// 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);
/**
* Called by {@link AutoFillUI} to fill an activity after the user selected a dataset.
*/
- void autoFillApp(int callbackId, Dataset 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 " + callbackId);
+ Slog.w(TAG, "autoFillApp(): no dataset for callback id " + sessionId);
return;
}
- final ServerCallback serverCallback;
+
+ final Session session;
synchronized (mLock) {
- serverCallback = mServerCallbacks.get(callbackId);
- if (serverCallback == null) {
- Slog.w(TAG, "autoFillApp(): no server callback with id " + callbackId);
+ session = mSessions.get(sessionId);
+ if (session == null) {
+ Slog.w(TAG, "autoFillApp(): no session with id " + sessionId);
return;
}
- if (serverCallback.appCallback == null) {
- Slog.w(TAG, "autoFillApp(): no app callback for server callback " + callbackId);
+ 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 {
- if (DEBUG) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
- serverCallback.appCallback.autoFill(dataset);
+ mService.authenticateFillResponse(extras, flags);
} catch (RemoteException e) {
- Slog.w(TAG, "Error auto-filling activity: " + e);
+ Slog.w(TAG, "Error sending authentication result back to service: " + e);
}
- removeServerCallbackLocked(callbackId);
}
}
- void removeServerCallbackLocked(int id) {
- if (DEBUG) Slog.d(TAG, "Removing " + id + " from server callbacks");
- mServerCallbacks.remove(id);
+ /**
+ * 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) {
pw.print(prefix); pw.print("mUserId="); pw.println(mUserId);
pw.print(prefix); pw.print("mUid="); pw.println(mUid);
pw.print(prefix); pw.print("mComponent="); pw.println(mComponent.flattenToShortString());
+ pw.print(prefix); pw.print("mService: "); pw.println(mService);
pw.print(prefix); pw.print("mBound="); pw.println(mBound);
- pw.print(prefix); pw.print("mService="); pw.println(mService);
pw.print(prefix); pw.print("mEstimateTimeOfDeath=");
TimeUtils.formatDuration(mEstimateTimeOfDeath, SystemClock.uptimeMillis(), pw);
pw.println();
if (DEBUG) {
// ServiceInfo dump is too noisy and redundant (it can be obtained through other dumps)
- pw.print(prefix); pw.println("Service info:");
+ pw.print(prefix); pw.println("ServiceInfo:");
mInfo.getServiceInfo().dump(new PrintWriterPrinter(pw), prefix + prefix);
}
}
}
- pw.print(prefix); pw.print("sServerCallbackCounter="); pw.println(sServerCallbackCounter);
- final int size = mServerCallbacks.size();
+ pw.print(prefix); pw.print("sSessionIdCounter="); pw.println(sSessionIdCounter);
+ final int size = mSessions.size();
if (size == 0) {
- pw.print(prefix); pw.println("No server callbacks");
+ pw.print(prefix); pw.println("No sessions");
} else {
- pw.print(prefix); pw.print(size); pw.println(" server callbacks:");
+ pw.print(prefix); pw.print(size); pw.println(" sessions:");
for (int i = 0; i < size; i++) {
- pw.print(prefix2); pw.print(mServerCallbacks.keyAt(i));
- final ServerCallback callback = mServerCallbacks.valueAt(i);
- if (callback.appCallback == null) {
+ pw.print(prefix2); pw.print(mSessions.keyAt(i));
+ final Session session = mSessions.valueAt(i);
+ if (session.mAppCallback == null) {
pw.println("(no appCallback)");
} else {
- pw.print(" (app callback: "); pw.print(callback.appCallback) ; pw.println(")");
+ pw.print(" (app callback: "); pw.print(session.mAppCallback) ; pw.println(")");
}
}
pw.println();
@Override
public String toString() {
- return "[AutoFillManagerServiceImpl: userId=" + mUserId + ", uid=" + mUid
+ return "AutoFillManagerServiceImpl: [userId=" + mUserId + ", uid=" + mUid
+ ", component=" + mComponent.flattenToShortString() + "]";
}
/**
* A bridge between the {@link AutoFillService} implementation and the activity being
* auto-filled (represented through the {@link IAutoFillAppCallback}).
+ *
+ * <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
+ * example, when service return a {@link FillResponse} that contains all the fields needed
+ * to fill the activity but it requires authentication first, that response need to be held
+ * until the user authenticates or it times out.
*/
- private final class ServerCallback extends IAutoFillServerCallback.Stub {
+ // TODO(b/33197203): make sure sessions are removed (and tested by CTS):
+ // - On all authentication scenarios.
+ // - When user does not interact back after a while.
+ // - When service is unbound.
+ private final class Session {
+
+ private final int mId;
+ private final Bundle mExtras;
+ private IAutoFillAppCallback mAppCallback;
+
+ // Token used on fingerprint authentication
+ private final IBinder mToken = new Binder();
+
+ private final IFingerprintService mFingerprintService;
+
+ @GuardedBy("mLock")
+ private FillResponse mResponseRequiringAuth;
+ @GuardedBy("mLock")
+ private Dataset mDatasetRequiringAuth;
+
+ // Used to auto-fill the activity directly when the FillCallback.onResponse() is called as
+ // the result of a successful user authentication on service's side.
+ @GuardedBy("mLock")
+ private boolean mAutoFillDirectly;
+
+ // TODO(b/33197203): use handler to handle results?
+ // TODO(b/33197203): handle all callback methods and/or cancelation?
+ private IFingerprintServiceReceiver mServiceReceiver =
+ new IFingerprintServiceReceiver.Stub() {
+
+ @Override
+ public void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
+ if (DEBUG) Slog.d(TAG, "onEnrollResult()");
+ }
- private final int id;
- private final Bundle extras;
- private IAutoFillAppCallback appCallback;
+ @Override
+ public void onAcquired(long deviceId, int acquiredInfo, int vendorCode) {
+ if (DEBUG) Slog.d(TAG, "onAcquired()");
+ }
- private ServerCallback(int id, Bundle extras) {
- this.id = id;
- this.extras = extras;
+ @Override
+ public void onAuthenticationSucceeded(long deviceId, Fingerprint fp, int userId) {
+ if (DEBUG) Slog.d(TAG, "onAuthenticationSucceeded(): " + fp.getGroupId());
+
+ // First, check what was authenticated, a response or a dataset.
+ // Then, decide how to handle it:
+ // - If service provided data, handle them directly.
+ // - Otherwise, notify service.
+
+ mAutoFillDirectly = true;
+
+ if (mDatasetRequiringAuth != null) {
+ if (mDatasetRequiringAuth.isEmpty()) {
+ notifyDatasetAuthenticationResult(mDatasetRequiringAuth.getExtras(),
+ FLAG_AUTHENTICATION_SUCCESS);
+ } else {
+ autoFillAppLocked(mDatasetRequiringAuth, true);
+ }
+ } else if (mResponseRequiringAuth != null) {
+ final List<Dataset> datasets = mResponseRequiringAuth.getDatasets();
+ if (datasets.isEmpty()) {
+ notifyResponseAuthenticationResult(mResponseRequiringAuth.getExtras(),
+ FLAG_AUTHENTICATION_SUCCESS);
+ } else {
+ showResponseLocked(mResponseRequiringAuth, true);
+ }
+ } else {
+ Slog.w(TAG, "onAuthenticationSucceeded(): no response or dataset");
+ }
+
+ mUi.dismissFingerprintRequest(mUserId, true);
+ }
+
+ @Override
+ public void onAuthenticationFailed(long deviceId) {
+ if (DEBUG) Slog.d(TAG, "onAuthenticationFailed()");
+ // Do nothing - onError() will be called after a few failures...
+ }
+
+ @Override
+ public void onError(long deviceId, int error, int vendorCode) {
+ if (DEBUG) Slog.d(TAG, "onError()");
+
+ // Notify service so it can fallback to its own authentication
+ if (mDatasetRequiringAuth != null) {
+ notifyDatasetAuthenticationResult(mDatasetRequiringAuth.getExtras(),
+ FLAG_AUTHENTICATION_ERROR);
+ } else if (mResponseRequiringAuth != null) {
+ notifyResponseAuthenticationResult(mResponseRequiringAuth.getExtras(),
+ FLAG_AUTHENTICATION_ERROR);
+ } else {
+ Slog.w(TAG, "onError(): no response or dataset");
+ }
+
+ mUi.dismissFingerprintRequest(mUserId, false);
+ }
+
+ @Override
+ public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) {
+ if (DEBUG) Slog.d(TAG, "onRemoved()");
+ }
+
+ @Override
+ public void onEnumerated(long deviceId, int fingerId, int groupId, int remaining) {
+ if (DEBUG) Slog.d(TAG, "onEnumerated()");
+ }
+ };
+
+ private IAutoFillServerCallback mServerCallback = new IAutoFillServerCallback.Stub() {
+ @Override
+ public void showResponse(FillResponse response) {
+ // TODO(b/33197203): add MetricsLogger call
+ if (response == null) {
+ if (DEBUG) Slog.d(TAG, "showResponse(): null response");
+
+ removeSelf();
+ return;
+ }
+
+ synchronized (mLock) {
+ showResponseLocked(response, response.isAuthRequired());
+ }
+ }
+
+ @Override
+ public void showError(CharSequence message) {
+ // TODO(b/33197203): add MetricsLogger call
+ if (DEBUG) Slog.d(TAG, "showError(): " + message);
+
+ mUi.showError(message);
+
+ removeSelf();
+ }
+
+ @Override
+ public void highlightSavedFields(AutoFillId[] ids) {
+ // TODO(b/33197203): add MetricsLogger call
+ if (DEBUG) Slog.d(TAG, "highlightSavedFields(): " + Arrays.toString(ids));
+
+ mUi.highlightSavedFields(ids);
+
+ removeSelf();
+ }
+
+ @Override
+ public void unlockFillResponse(int flags) {
+ // TODO(b/33197203): add proper MetricsLogger calls?
+ if (DEBUG) Log.d(TAG, "unlockUser(): flags=" + flags);
+
+ synchronized (mLock) {
+ if ((flags & FLAG_AUTHENTICATION_SUCCESS) != 0) {
+ if (mResponseRequiringAuth == null) {
+ Log.wtf(TAG, "unlockUser(): no mResponseRequiringAuth on flags "
+ + flags);
+ removeSelf();
+ return;
+ }
+ final List<Dataset> datasets = mResponseRequiringAuth.getDatasets();
+ if (datasets.isEmpty()) {
+ Log.w(TAG, "unlockUser(): no dataset on previous response: "
+ + mResponseRequiringAuth);
+ removeSelf();
+ return;
+ }
+ mAutoFillDirectly = true;
+ showResponseLocked(mResponseRequiringAuth, false);
+ }
+ // TODO(b/33197203): show UI error on authentication failure?
+ // Or let service handle it?
+ }
+ }
+
+ @Override
+ public void unlockDataset(Dataset dataset, int flags) {
+ // TODO(b/33197203): add proper MetricsLogger calls?
+ if (DEBUG) Log.d(TAG, "unlockDataset(): dataset=" + dataset + ", flags=" + flags);
+
+ if ((flags & FLAG_AUTHENTICATION_SUCCESS) != 0) {
+ autoFillAppLocked(dataset != null ? dataset : mDatasetRequiringAuth, true);
+ return;
+ }
+ removeSelf();
+ }
+ };
+
+ private Session(int id, Bundle extras) {
+ this.mId = id;
+ this.mExtras = extras;
+ this.mFingerprintService = IFingerprintService.Stub
+ .asInterface(ServiceManager.getService("fingerprint"));
}
- @Override
- public void showResponse(FillResponse response) {
- // TODO(b/33197203): add MetricsLogger call
- if (DEBUG) Slog.d(TAG, "showResponse(): " + response);
+ private void showResponseLocked(FillResponse response, boolean authRequired) {
+ if (DEBUG) Slog.d(TAG, "showResponse(directly=" + mAutoFillDirectly
+ + ", authRequired=" + authRequired +"):" + response);
+
+ if (mAutoFillDirectly && response != null) {
+ final List<Dataset> datasets = response.getDatasets();
+ if (datasets.size() == 1) {
+ // User authenticated and provider returned just 1 dataset - auto-fill it now!
+ final Dataset dataset = datasets.get(0);
+ if (DEBUG) Slog.d(TAG, "auto-filling directly from auth: " + dataset);
- mUi.showOptions(mUserId, id, response);
+ autoFillAppLocked(dataset, true);
+ return;
+ }
+ }
+
+ if (!authRequired) {
+ // TODO(b/33197203): add MetricsLogger call
+ mUi.showOptions(mUserId, mId, response);
+ return;
+ }
+
+ // Handles response that requires authentication.
+ // TODO(b/33197203): add MetricsLogger call, including if fingerprint requested
+
+ mResponseRequiringAuth = response;
+ final boolean requiresFingerprint = response.hasCryptoObject();
+ if (requiresFingerprint) {
+ // TODO(b/33197203): check if fingerprint is available first and call error callback
+ // with FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE if it's not.
+ // Start scanning for the fingerprint.
+ scanFingerprint(response.getCryptoObjectOpId());
+ }
+ // Displays the message asking the user to tap (or fingerprint) for AutoFill.
+ mUi.showFillResponseAuthenticationRequest(mUserId, mId, requiresFingerprint,
+ response.getExtras(), response.getFlags());
}
- @Override
- public void showError(String message) {
- // TODO(b/33197203): add MetricsLogger call
- if (DEBUG) Slog.d(TAG, "showError(): " + message);
+ void autoFill(Dataset dataset) {
+ synchronized (mLock) {
+ // Autofill it directly...
+ if (!dataset.isAuthRequired()) {
+ autoFillAppLocked(dataset, true);
+ return;
+ }
- mUi.showError(message);
+ // ...or handle authentication.
+
+ mDatasetRequiringAuth = dataset;
+ final boolean requiresFingerprint = dataset.hasCryptoObject();
+ if (requiresFingerprint) {
+ // TODO(b/33197203): check if fingerprint is available first and call error callback
+ // with FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE if it's not.
+ // Start scanning for the fingerprint.
+ scanFingerprint(dataset.getCryptoObjectOpId());
+ // Displays the message asking the user to tap (or fingerprint) for AutoFill.
+ mUi.showDatasetFingerprintAuthenticationRequest(dataset);
+ } else {
+ try {
+ mService.authenticateDataset(dataset.getExtras(),
+ FLAG_AUTHENTICATION_REQUESTED);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error authenticating dataset: " + e);
+ }
+ }
+ }
+ }
- removeSelf();
+ 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, since save is
+ // not integrated with IME yet.
+ mUi.showSaveNotification(mUserId, null, dataset);
+
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Error auto-filling activity: " + e);
+ }
+ if (removeSelf) {
+ removeSelf();
+ }
}
- @Override
- public void highlightSavedFields(AutoFillId[] ids) {
+ private void scanFingerprint(long opId) {
// TODO(b/33197203): add MetricsLogger call
- if (DEBUG) Slog.d(TAG, "showSaved(): " + Arrays.toString(ids));
-
- mUi.highlightSavedFields(ids);
+ if (DEBUG) Slog.d(TAG, "Starting fingerprint scan for op id: " + opId);
- removeSelf();
+ // TODO(b/33197203): since we're clearing the AutoFillService's identity, make sure
+ // this method is only called at the proper times, otherwise a malicious provider could
+ // keep the callback refence to bypass the check
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // TODO(b/33197203): set a timeout?
+ mFingerprintService.authenticate(mToken, opId, mUserId, mServiceReceiver, 0, null);
+ } catch (RemoteException e) {
+ // Local call, shouldn't happen.
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
private void removeSelf() {
synchronized (mLock) {
- removeServerCallbackLocked(id);
+ removeSessionLocked(mId);
}
}
}
*/
package com.android.server.autofill;
+import static android.view.View.AUTO_FILL_FLAG_TYPE_FILL;
import static android.view.View.AUTO_FILL_FLAG_TYPE_SAVE;
-import static com.android.server.autofill.AutoFillManagerService.DEBUG;
+import static com.android.server.autofill.Helper.DEBUG;
+import static com.android.server.autofill.Helper.bundleToString;
import android.app.Activity;
import android.app.Notification;
import android.app.Notification.Action;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.service.autofill.AutoFillService;
-import android.util.Log;
import android.util.Slog;
import android.view.autofill.AutoFillId;
import android.view.autofill.Dataset;
import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
-import java.util.Set;
/**
* Handles all auto-fill related UI tasks.
/**
* Displays an error message to the user.
*/
- void showError(String message) {
+ void showError(CharSequence message) {
// TODO(b/33197203): proper implementation
UiThread.getHandler().runWithScissors(() -> {
Toast.makeText(mContext, "AutoFill error: " + message, Toast.LENGTH_LONG).show();
* Shows the options from a {@link FillResponse} so the user can pick up the proper
* {@link Dataset} (when the response has one).
*/
- void showOptions(int userId, int callbackId, FillResponse response) {
+ void showOptions(int userId, int sessionId, FillResponse response) {
// TODO(b/33197203): proper implementation
// TODO(b/33197203): make sure if removes the callback from cache
- showOptionsNotification(userId, callbackId, response);
+ showOptionsNotification(userId, sessionId, response);
+ }
+
+ /**
+ * Shows an UI affordance indicating that user action is required before a {@link FillResponse}
+ * can be used.
+ *
+ * <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,
+ Bundle extras, int flags) {
+ // TODO(b/33197203): proper implementation
+ showAuthNotification(userId, sessionId, usesFingerprint, extras, flags);
+ }
+
+ /**
+ * Shows an UI affordance asking indicating that user action is required before a
+ * {@link Dataset} can be used.
+ *
+ * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint to
+ * autofill".
+ */
+ void showDatasetFingerprintAuthenticationRequest(Dataset dataset) {
+ if (DEBUG) Slog.d(TAG, "showDatasetAuthenticationRequest(): dataset=" + dataset);
+
+ // TODO(b/33197203): proper implementation (either pop up a fingerprint dialog or replace
+ // the auto-fill bar with a new message.
+ UiThread.getHandler().runWithScissors(() -> {
+ Toast.makeText(mContext, "AutoFill: press fingerprint to unlock " + dataset.getName(),
+ Toast.LENGTH_LONG).show();
+ }, 0);
+ }
+
+ /**
+ * Called by service after the user user the fingerprint sensors to authenticate.
+ */
+ void dismissFingerprintRequest(int userId, boolean success) {
+ if (DEBUG) Slog.d(TAG, "dismissFingerprintRequest(): ok=" + success);
+
+ dismissAuthNotification(userId);
+
+ if (!success) {
+ // TODO(b/33197203): proper implementation (snack bar / i18n string)
+ UiThread.getHandler().runWithScissors(() -> {
+ Toast.makeText(mContext, "AutoFill: fingerprint failed", Toast.LENGTH_LONG).show();
+ }, 0);
+ }
+ }
+
+ 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, Bundle responseExtras, Bundle datasetExtras) {
+ synchronized (mLock) {
+ final AutoFillManagerServiceImpl service = getServiceLocked(userId);
+ if (service == null) return;
+
+ 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 notificataion: " +
+ bundleToString(datasetExtras));
+ extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetExtras);
+ }
+
+ service.requestAutoFill(null, extras, AUTO_FILL_FLAG_TYPE_SAVE);
+ }
+ }
+
+ 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);
+ }
}
/////////////////////////////////////////////////////////////////////////////////
// Extras used in the notification intents
private static final String EXTRA_USER_ID = "user_id";
private static final String EXTRA_NOTIFICATION_TYPE = "notification_type";
- private static final String EXTRA_CALLBACK_ID = "callback_id";
+ private static final String EXTRA_SESSION_ID = "session_id";
private static final String EXTRA_FILL_RESPONSE = "fill_response";
private static final String EXTRA_DATASET = "dataset";
+ private static final String EXTRA_AUTH_REQUIRED_EXTRAS = "auth_required_extras";
+ private static final String EXTRA_FLAGS = "flags";
private static final String TYPE_OPTIONS = "options";
- private static final String TYPE_DELETE_CALLBACK = "delete_callback";
+ 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("mLock")
private BroadcastReceiver mNotificationReceiver;
@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);
+ return;
+ }
+ final FillResponse response = intent.getParcelableExtra(EXTRA_FILL_RESPONSE);
+ final Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET);
+ final Bundle responseExtras = response == null ? null : response.getExtras();
+ final Bundle datasetExtras = dataset == null ? null : dataset.getExtras();
+ final int flags = intent.getIntExtra(EXTRA_FLAGS, 0);
+
+ if (DEBUG) Slog.d(TAG, "Notification received: type=" + type + ", userId=" + userId
+ + ", sessionId=" + sessionId);
synchronized (mLock) {
- final AutoFillManagerServiceImpl service = mService.getServiceForUserLocked(userId);
- if (service == null) {
- Slog.w(TAG, "no auto-fill service for user " + userId);
- return;
- }
-
- final int callbackId = intent.getIntExtra(EXTRA_CALLBACK_ID, -1);
- final String type = intent.getStringExtra(EXTRA_NOTIFICATION_TYPE);
- if (type == null) {
- Slog.wtf(TAG, "No extra " + EXTRA_NOTIFICATION_TYPE + " on intent " + intent);
- return;
- }
- final FillResponse fillData = intent.getParcelableExtra(EXTRA_FILL_RESPONSE);
- final Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET);
- final Bundle datasetArgs = dataset == null ? null : dataset.getExtras();
- final Bundle fillDataArgs = fillData == null ? null : fillData.getExtras();
-
- // Bundle sent on AutoFillService methods - only set if service provided a bundle
- final Bundle extras = (datasetArgs == null && fillDataArgs == null)
- ? null : new Bundle();
-
- if (DEBUG) Slog.d(TAG, "Notification received: type=" + type + ", userId=" + userId
- + ", callbackId=" + callbackId);
switch (type) {
case TYPE_SAVE:
- if (datasetArgs != null) {
- if (DEBUG) Log.d(TAG, "filldata args on save notificataion: " +
- bundleToString(fillDataArgs));
- extras.putBundle(AutoFillService.EXTRA_RESPONSE_EXTRAS, fillDataArgs);
- }
- if (dataset != null) {
- if (DEBUG) Log.d(TAG, "dataset args on save notificataion: " +
- bundleToString(datasetArgs));
- extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetArgs);
- }
- service.requestAutoFill(null, extras, AUTO_FILL_FLAG_TYPE_SAVE);
+ onSaveRequested(userId, responseExtras, datasetExtras);
break;
- case TYPE_DELETE_CALLBACK:
- service.removeServerCallbackLocked(callbackId);
+ case TYPE_FINISH_SESSION:
+ onSessionDone(userId, sessionId);
break;
case TYPE_PICK_DATASET:
- service.autoFillApp(callbackId, dataset);
+ onDatasetPicked(userId, dataset, sessionId);
+
// Must cancel notification because it might be comming from action
- if (DEBUG) Log.d(TAG, "Cancelling notification");
+ if (DEBUG) Slog.d(TAG, "Cancelling notification");
NotificationManager.from(mContext).cancel(TYPE_OPTIONS, userId);
- if (datasetArgs != null) {
- if (DEBUG) Log.d(TAG, "adding dataset's extra_data on save intent: "
- + bundleToString(datasetArgs));
- extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetArgs);
- }
-
- // Also show notification with option to save the data
- showSaveNotification(userId, fillData, dataset);
+ break;
+ case TYPE_AUTH_RESPONSE:
+ onResponseAuthenticationRequested(userId,
+ intent.getBundleExtra(EXTRA_AUTH_REQUIRED_EXTRAS), flags);
break;
default: {
Slog.w(TAG, "Unknown notification type: " + type);
}
}
}
+ collapseStatusBar();
}
}
return intent;
}
- private PendingIntent newPickDatasetPI(int userId, int callbackId, FillResponse response,
+ private PendingIntent newPickDatasetPI(int userId, int sessionId, FillResponse response,
Dataset dataset) {
final int resultCode = ++ sResultCode;
- if (DEBUG) Log.d(TAG, "newPickDatasetPI: userId=" + userId + ", callback=" + callbackId
+ if (DEBUG) Slog.d(TAG, "newPickDatasetPI: userId=" + userId + ", sessionId=" + sessionId
+ ", resultCode=" + resultCode);
final Intent intent = newNotificationIntent(userId, TYPE_PICK_DATASET);
- intent.putExtra(EXTRA_CALLBACK_ID, callbackId);
+ 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);
}
- private static String bundleToString(Bundle bundle) {
- if (bundle == null) {
- return "null";
- }
- final Set<String> keySet = bundle.keySet();
- final StringBuilder builder = new StringBuilder("[Bundle with ").append(keySet.size())
- .append(" keys:");
- for (String key : keySet) {
- final Object value = bundle.get(key);
- builder.append(' ').append(key).append('=');
- builder.append((value instanceof Object[])
- ? Arrays.toString((Objects[]) value) : value);
- }
- return builder.append(']').toString();
- }
-
/**
* 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, FillResponse response) {
+ private void showOptionsNotification(int userId, int sessionId, FillResponse response) {
final long token = Binder.clearCallingIdentity();
try {
- showOptionsNotificationAsSystem(userId, callbackId, response);
+ showOptionsNotificationAsSystem(userId, sessionId, response);
} finally {
Binder.restoreCallingIdentity(token);
}
}
- private void showOptionsNotificationAsSystem(int userId, int callbackId,
+ private void showOptionsNotificationAsSystem(int userId, int sessionId,
FillResponse response) {
// Make sure server callback is removed from cache if user cancels the notification.
- final Intent deleteIntent = newNotificationIntent(userId, TYPE_DELETE_CALLBACK);
- deleteIntent.putExtra(EXTRA_CALLBACK_ID, callbackId);
+ 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 = new Notification.Builder(mContext)
- .setCategory(Notification.CATEGORY_SYSTEM)
+ final Notification.Builder notification = newNotificationBuilder()
.setOngoing(false)
- .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
- .setLocalOnly(true)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color))
.setDeleteIntent(deletePendingIntent)
.setContentTitle(title);
autoCancel = false;
final int size = datasets.size();
subTitle = "There are " + size + " option(s).\n"
- + "Use the notification action(s) to select the proper one.";
+ + "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 CharSequence name = dataset.getName();
- final PendingIntent pi = newPickDatasetPI(userId, callbackId, response, dataset);
+ 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());
}
}
}
}
- private void showSaveNotification(int userId, FillResponse response, Dataset dataset) {
+ void showSaveNotification(int userId, FillResponse response, Dataset dataset) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ showSaveNotificationAsSystem(userId, response, dataset);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private void showSaveNotificationAsSystem(int userId, FillResponse response, Dataset dataset) {
final Intent saveIntent = newNotificationIntent(userId, TYPE_SAVE);
- saveIntent.putExtra(EXTRA_FILL_RESPONSE, response);
+ if (response != null) {
+ saveIntent.putExtra(EXTRA_FILL_RESPONSE, response);
+ }
if (dataset != null) {
saveIntent.putExtra(EXTRA_DATASET, dataset);
}
++sResultCode, saveIntent, PendingIntent.FLAG_ONE_SHOT);
final String title = "AutoFill Save";
- final String subTitle = "Tap notification to ask provider to save fields: \n"
- + Arrays.toString(response.getSavableIds());
+ // Response is not set after fillign an authenticated dataset...
+ final String subTitle = response == null
+ ? "Tap notification to ask provider to save fields."
+ : "Tap notification to ask provider to save fields: \n"
+ + Arrays.toString(response.getSavableIds());
- final Notification notification = new Notification.Builder(mContext)
- .setCategory(Notification.CATEGORY_SYSTEM)
+ final Notification notification = newNotificationBuilder()
.setAutoCancel(true)
.setOngoing(false)
- .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
- .setLocalOnly(true)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color))
.setContentTitle(title)
.setContentIntent(savePendingIntent)
.setStyle(new Notification.BigTextStyle().bigText(subTitle))
NotificationManager.from(mContext).notify(TYPE_SAVE, userId, notification);
}
+ private void showAuthNotification(int userId, int sessionId, boolean usesFingerprint,
+ Bundle extras, int flags) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ showAuthNotificationAsSystem(userId, sessionId, usesFingerprint, extras, flags);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private void showAuthNotificationAsSystem(int userId, int sessionId,
+ 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);
+ if (extras != null) {
+ authIntent.putExtra(EXTRA_AUTH_REQUIRED_EXTRAS, extras);
+ }
+ if (flags != 0) {
+ authIntent.putExtra(EXTRA_FLAGS, flags);
+ }
+ final PendingIntent authPendingIntent = PendingIntent.getBroadcast(mContext, ++sResultCode,
+ authIntent, PendingIntent.FLAG_ONE_SHOT);
+
+ if (usesFingerprint) {
+ subTitle.append("But kindly accepts your fingerprint instead"
+ + "\n(tap fingerprint sensor to trigger it)");
+
+ } else {
+ subTitle.append("Tap notification to launch its authentication UI.");
+ }
+
+ final Notification.Builder notification = newNotificationBuilder()
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .setContentTitle(title)
+ .setStyle(new Notification.BigTextStyle().bigText(subTitle.toString()));
+ if (authPendingIntent != null) {
+ notification.setContentIntent(authPendingIntent);
+ }
+ NotificationManager.from(mContext).notify(TYPE_AUTH_RESPONSE, userId, notification.build());
+ }
+
+ private void dismissAuthNotification(int userId) {
+ NotificationManager.from(mContext).cancel(TYPE_AUTH_RESPONSE, userId);
+ }
+
+ private Notification.Builder newNotificationBuilder() {
+ return new Notification.Builder(mContext)
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+ .setLocalOnly(true)
+ .setColor(mContext.getColor(
+ com.android.internal.R.color.system_notification_accent_color));
+ }
+
+ private void collapseStatusBar() {
+ final StatusBarManager sbm = (StatusBarManager) mContext.getSystemService("statusbar");
+ sbm.collapsePanels();
+ }
/////////////////////////////////////////
// End of temporary notification code. //
/////////////////////////////////////////
--- /dev/null
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.autofill;
+
+import android.os.Bundle;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+final class Helper {
+
+ static final boolean DEBUG = true; // TODO(b/33197203): set to false when stable
+ static final String REDACTED = "[REDACTED]";
+
+ static void append(StringBuilder builder, Bundle bundle) {
+ if (bundle == null) {
+ builder.append("N/A");
+ } else if (!DEBUG) {
+ builder.append(REDACTED);
+ } else {
+ final Set<String> keySet = bundle.keySet();
+ builder.append("[Bundle with ").append(keySet.size()).append(" extras:");
+ for (String key : keySet) {
+ final Object value = bundle.get(key);
+ builder.append(' ').append(key).append('=');
+ builder.append((value instanceof Object[])
+ ? Arrays.toString((Objects[]) value) : value);
+ }
+ builder.append(']');
+ }
+ }
+
+ static String bundleToString(Bundle bundle) {
+ final StringBuilder builder = new StringBuilder();
+ append(builder, bundle);
+ return builder.toString();
+ }
+
+ private Helper() {
+ throw new UnsupportedOperationException("contains static members only");
+ }
+}