OSDN Git Service

Added support to authentication.
authorFelipe Leme <felipeal@google.com>
Thu, 15 Dec 2016 19:56:15 +0000 (11:56 -0800)
committerFelipe Leme <felipeal@google.com>
Wed, 25 Jan 2017 21:03:32 +0000 (13:03 -0800)
AutoFillService can now require user authentication, both at
FillResponse and Dataset levels;
- FillResponse authentication is typically used when the user data
  need to be unlocked before the first use.
- Dataset authentication is typically used to unlock sensitive data
  such as credit card info.

The authentication can be handled by the service itself (for example,
when it uses the credit card CVV to unlock it) or by the Android
system (when the service asks for fingerprint authentication).

Bug: 31001899

Test: manual verification
Test: CtsAutoFillServiceTestCases passes

Change-Id: If62f42f697ab5ef0d14d991ff1077d1c38808e61

19 files changed:
api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/hardware/fingerprint/FingerprintManager.java
core/java/android/service/autofill/AutoFillService.java
core/java/android/service/autofill/CallbackHelper.java [new file with mode: 0644]
core/java/android/service/autofill/FillCallback.java
core/java/android/service/autofill/IAutoFillAppCallback.aidl
core/java/android/service/autofill/IAutoFillServerCallback.aidl
core/java/android/service/autofill/IAutoFillService.aidl
core/java/android/service/autofill/SaveCallback.java
core/java/android/util/TimeUtils.java
core/java/android/view/autofill/Dataset.java
core/java/android/view/autofill/FillResponse.java
core/java/android/view/autofill/Helper.java
services/autofill/java/com/android/server/autofill/AutoFillManagerService.java
services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
services/autofill/java/com/android/server/autofill/AutoFillUI.java
services/autofill/java/com/android/server/autofill/Helper.java [new file with mode: 0644]

index 1228db8..8dbf6de 100644 (file)
@@ -35624,17 +35624,25 @@ package android.service.autofill {
     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);
   }
 
@@ -46251,6 +46259,8 @@ package android.view.autofill {
   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);
   }
@@ -46266,6 +46276,8 @@ package android.view.autofill {
     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);
   }
 
index 4698a68..68db5de 100644 (file)
@@ -38640,17 +38640,25 @@ package android.service.autofill {
     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);
   }
 
@@ -49636,6 +49644,8 @@ package android.view.autofill {
   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);
   }
@@ -49651,6 +49661,8 @@ package android.view.autofill {
     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);
   }
 
index 15844cd..b5fb440 100644 (file)
@@ -35757,17 +35757,25 @@ package android.service.autofill {
     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);
   }
 
@@ -46561,6 +46569,8 @@ package android.view.autofill {
   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);
   }
@@ -46576,6 +46586,8 @@ package android.view.autofill {
     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);
   }
 
index 4b57078..2c9e6c7 100644 (file)
@@ -1074,7 +1074,7 @@ public class FingerprintManager {
 
         @Override // binder call
         public void onAuthenticationFailed(long deviceId) {
-            mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();;
+            mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
         }
 
         @Override // binder call
index c7c8321..b5cb8f8 100644 (file)
@@ -30,13 +30,19 @@ import android.os.Looper;
 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.
@@ -48,6 +54,9 @@ public abstract class AutoFillService extends Service {
     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
@@ -73,6 +82,7 @@ public abstract class AutoFillService extends Service {
     // 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.";
@@ -93,10 +103,32 @@ public abstract class AutoFillService extends Service {
      */
     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() {
 
@@ -109,6 +141,22 @@ public abstract class AutoFillService extends Service {
         }
 
         @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));
         }
@@ -135,6 +183,16 @@ public abstract class AutoFillService extends Service {
                     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;
@@ -147,6 +205,10 @@ public abstract class AutoFillService extends Service {
 
     private HandlerCaller mHandlerCaller;
 
+    // User for debugging purposes
+    private final List<CallbackHelper.Dumpable> mPendingCallbacks =
+            DEBUG_PENDING_CALLBACKS ? new ArrayList<>() : null;
+
     /**
      * {@inheritDoc}
      *
@@ -211,16 +273,84 @@ public abstract class AutoFillService extends Service {
     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;
@@ -229,6 +359,22 @@ public abstract class AutoFillService extends Service {
         }
     }
 
+    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.
      *
diff --git a/core/java/android/service/autofill/CallbackHelper.java b/core/java/android/service/autofill/CallbackHelper.java
new file mode 100644 (file)
index 0000000..ded8f97
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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();
+    }
+}
index 925da8b..7cab7ae 100644 (file)
 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) {
@@ -46,51 +68,174 @@ public final class FillCallback {
 
     /**
      * 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));
     }
 }
index 629b1f0..cc83776 100644 (file)
@@ -21,8 +21,15 @@ import java.util.List;
 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);
 }
index 9d58c99..185c8f3 100644 (file)
@@ -18,14 +18,23 @@ package android.service.autofill;
 
 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);
 }
index a1f22bf..fa9786a 100644 (file)
@@ -24,9 +24,13 @@ import com.android.internal.os.IResultReceiver;
 /**
  * @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();
 }
index 04f8c9f..d5022d8 100644 (file)
@@ -22,23 +22,34 @@ import android.app.Activity;
 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;
@@ -46,8 +57,8 @@ public final class SaveCallback {
 
     /**
      * 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.
      *
@@ -57,21 +68,24 @@ public final class SaveCallback {
         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.
      *
@@ -81,18 +95,53 @@ public final class SaveCallback {
         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();
+        }
     }
 }
index 37d6757..0a294ab 100644 (file)
@@ -19,7 +19,6 @@ package android.util;
 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;
 
index a73eb77..b11eecc 100644 (file)
@@ -19,12 +19,13 @@ package android.view.autofill;
 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;
 
@@ -50,12 +51,20 @@ public final class Dataset implements Parcelable {
     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 */
@@ -73,13 +82,41 @@ public final class Dataset implements Parcelable {
         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();
     }
 
@@ -90,6 +127,10 @@ public final class Dataset implements Parcelable {
         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.
@@ -103,6 +144,125 @@ public final class Dataset implements Parcelable {
         }
 
         /**
+         * 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()}.
@@ -121,15 +281,17 @@ public final class Dataset implements Parcelable {
         }
 
         /**
-         * 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;
         }
@@ -158,6 +320,12 @@ public final class Dataset implements Parcelable {
         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")
@@ -165,6 +333,10 @@ public final class Dataset implements Parcelable {
         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>() {
index 3a14767..67eb85a 100644 (file)
@@ -18,11 +18,13 @@ package android.view.autofill;
 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;
 
@@ -34,14 +36,16 @@ import java.util.Objects;
 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()
@@ -67,9 +71,9 @@ import java.util.Set;
  *      .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()
@@ -79,8 +83,8 @@ import java.util.Set;
  *
  * <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()
@@ -101,9 +105,9 @@ import java.util.Set;
  * <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()
@@ -134,19 +138,31 @@ import java.util.Set;
  *      .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?
@@ -158,6 +174,10 @@ public final class FillResponse implements Parcelable {
             mSavableIds[i++] = id;
         }
         mExtras = builder.mExtras;
+        mFlags = builder.mFlags;
+        mRequiresAuth = builder.mRequiresAuth;
+        mHasCryptoObject = builder.mHasCryptoObject;
+        mCryptoOpId = builder.mCryptoOpId;
     }
 
     /** @hide */
@@ -175,13 +195,161 @@ public final class FillResponse implements Parcelable {
         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.
@@ -191,7 +359,6 @@ public final class FillResponse implements Parcelable {
         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());
@@ -201,13 +368,14 @@ public final class FillResponse implements Parcelable {
 
         /**
          * 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);
             }
@@ -215,15 +383,18 @@ public final class FillResponse implements Parcelable {
         }
 
         /**
-         * 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;
         }
@@ -246,7 +417,10 @@ public final class FillResponse implements Parcelable {
         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();
     }
 
@@ -264,6 +438,12 @@ public final class FillResponse implements Parcelable {
         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) {
@@ -271,6 +451,10 @@ public final class FillResponse implements Parcelable {
         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 =
index 772710e..14cf9e8 100644 (file)
@@ -18,28 +18,33 @@ package android.view.autofill;
 
 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() {
index 6779657..47ac1ce 100644 (file)
@@ -102,7 +102,6 @@ public final class AutoFillManagerService extends SystemService {
                 }
             }
         }
-
     };
 
     private HandlerCaller mHandlerCaller;
@@ -313,7 +312,6 @@ public final class AutoFillManagerService extends SystemService {
             (new AutoFillManagerServiceShellCommand(this)).exec(
                     this, in, out, err, args, callback, resultReceiver);
         }
-
     }
 
     private final class SettingsObserver extends ContentObserver {
index 83faf1b..77e7b31 100644 (file)
 
 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;
@@ -31,12 +35,16 @@ import android.content.Intent;
 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;
@@ -45,6 +53,7 @@ import android.service.autofill.IAutoFillAppCallback;
 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;
@@ -59,7 +68,6 @@ import com.android.server.LocalServices;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -73,7 +81,7 @@ final class AutoFillManagerServiceImpl {
     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;
@@ -106,13 +114,15 @@ final class AutoFillManagerServiceImpl {
     };
 
     /**
-     * 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
@@ -146,10 +156,9 @@ final class AutoFillManagerServiceImpl {
         }
     };
 
-
     /**
      * 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
@@ -165,22 +174,24 @@ final class AutoFillManagerServiceImpl {
                     .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.
@@ -274,8 +285,8 @@ final class AutoFillManagerServiceImpl {
             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);
@@ -294,9 +305,9 @@ final class AutoFillManagerServiceImpl {
             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:
@@ -307,7 +318,7 @@ final class AutoFillManagerServiceImpl {
          */
         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);
@@ -347,39 +358,74 @@ final class AutoFillManagerServiceImpl {
     /**
      * 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) {
@@ -398,15 +444,15 @@ final class AutoFillManagerServiceImpl {
         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);
         }
 
@@ -427,19 +473,19 @@ final class AutoFillManagerServiceImpl {
             }
         }
 
-        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();
@@ -448,7 +494,7 @@ final class AutoFillManagerServiceImpl {
 
     @Override
     public String toString() {
-        return "[AutoFillManagerServiceImpl: userId=" + mUserId + ", uid=" + mUid
+        return "AutoFillManagerServiceImpl: [userId=" + mUserId + ", uid=" + mUid
                 + ", component=" + mComponent.flattenToShortString() + "]";
     }
 
@@ -472,49 +518,311 @@ final class AutoFillManagerServiceImpl {
     /**
      * 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);
             }
         }
     }
index 6fafd5d..ad525d4 100644 (file)
  */
 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;
@@ -31,7 +34,6 @@ import android.content.IntentFilter;
 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;
@@ -43,8 +45,6 @@ import com.android.server.UiThread;
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
-import java.util.Set;
 
 /**
  * Handles all auto-fill related UI tasks.
@@ -67,7 +67,7 @@ final class AutoFillUI {
     /**
      * 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();
@@ -89,10 +89,115 @@ final class AutoFillUI {
      * Shows the options from a {@link FillResponse} so the user can pick up the proper
      * {@link Dataset} (when the response has one).
      */
-    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);
+        }
     }
 
     /////////////////////////////////////////////////////////////////////////////////
@@ -107,14 +212,17 @@ final class AutoFillUI {
     // 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;
@@ -139,68 +247,46 @@ final class AutoFillUI {
         @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();
         }
     }
 
@@ -211,66 +297,45 @@ final class AutoFillUI {
         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);
 
@@ -305,10 +370,19 @@ final class AutoFillUI {
                 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());
                 }
             }
@@ -324,9 +398,20 @@ final class AutoFillUI {
         }
     }
 
-    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);
         }
@@ -334,17 +419,15 @@ final class AutoFillUI {
                 ++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))
@@ -352,6 +435,68 @@ final class AutoFillUI {
         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. //
     /////////////////////////////////////////
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
new file mode 100644 (file)
index 0000000..79095a1
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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");
+    }
+}