diff --git a/api/current.txt b/api/current.txt index 1228db83907cc..8dbf6de98e164 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); } diff --git a/api/system-current.txt b/api/system-current.txt index 4698a68100127..68db5de37fc9e 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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); } diff --git a/api/test-current.txt b/api/test-current.txt index 15844cd01cd7d..b5fb44058dd85 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -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); } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 4b5707890518e..2c9e6c720f7ba 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -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 diff --git a/core/java/android/service/autofill/AutoFillService.java b/core/java/android/service/autofill/AutoFillService.java index c7c8321a82ed3..b5cb8f88c8714 100644 --- a/core/java/android/service/autofill/AutoFillService.java +++ b/core/java/android/service/autofill/AutoFillService.java @@ -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() { @@ -108,6 +140,22 @@ public abstract class AutoFillService extends Service { .sendToTarget(); } + @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 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 index 0000000000000..ded8f97847c50 --- /dev/null +++ b/core/java/android/service/autofill/CallbackHelper.java @@ -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(); + } +} diff --git a/core/java/android/service/autofill/FillCallback.java b/core/java/android/service/autofill/FillCallback.java index 925da8b334627..7cab7ae1c479c 100644 --- a/core/java/android/service/autofill/FillCallback.java +++ b/core/java/android/service/autofill/FillCallback.java @@ -17,27 +17,49 @@ 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. + * + *

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); + final boolean authRequired = response != null && response.isAuthRequired(); - checkNotRepliedYet(); + if (DEBUG) Log.d(TAG, "onSuccess(): authReq= " + authRequired + ", resp=" + response); - try { - mCallback.showResponse(response); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); + 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(); + } } } - // There can be only one!! - private void checkNotRepliedYet() { - Preconditions.checkState(!mReplied, "already replied"); - mReplied = true; + /** + * 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; + } + } + + /** + * 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)); } } diff --git a/core/java/android/service/autofill/IAutoFillAppCallback.aidl b/core/java/android/service/autofill/IAutoFillAppCallback.aidl index 629b1f008d8f8..cc83776e2f574 100644 --- a/core/java/android/service/autofill/IAutoFillAppCallback.aidl +++ b/core/java/android/service/autofill/IAutoFillAppCallback.aidl @@ -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); } diff --git a/core/java/android/service/autofill/IAutoFillServerCallback.aidl b/core/java/android/service/autofill/IAutoFillServerCallback.aidl index 9d58c9956a259..185c8f3a4fb3f 100644 --- a/core/java/android/service/autofill/IAutoFillServerCallback.aidl +++ b/core/java/android/service/autofill/IAutoFillServerCallback.aidl @@ -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); } diff --git a/core/java/android/service/autofill/IAutoFillService.aidl b/core/java/android/service/autofill/IAutoFillService.aidl index a1f22bf114e3b..fa9786a1be534 100644 --- a/core/java/android/service/autofill/IAutoFillService.aidl +++ b/core/java/android/service/autofill/IAutoFillService.aidl @@ -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(); } diff --git a/core/java/android/service/autofill/SaveCallback.java b/core/java/android/service/autofill/SaveCallback.java index 04f8c9f5f74a8..d5022d85f1100 100644 --- a/core/java/android/service/autofill/SaveCallback.java +++ b/core/java/android/service/autofill/SaveCallback.java @@ -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. + * + *

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(); + } } } diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 37d675707a84b..0a294ababf840 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -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; diff --git a/core/java/android/view/autofill/Dataset.java b/core/java/android/view/autofill/Dataset.java index a73eb7740e806..b11eecc35e762 100644 --- a/core/java/android/view/autofill/Dataset.java +++ b/core/java/android/view/autofill/Dataset.java @@ -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 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 mFields = new ArrayList<>(); private Bundle mExtras; + private int mFlags; + private boolean mRequiresAuth; + private boolean mHasCryptoObject; + private long mCryptoOpId; /** * Creates a new builder. @@ -102,6 +143,125 @@ public final class Dataset implements Parcelable { mName = Preconditions.checkStringNotEmpty(name, "name cannot be empty or null"); } + /** + * Requires dataset authentication through the {@link + * android.service.autofill.AutoFillService} before auto-filling the activity with this + * dataset. + * + *

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). + * + *

Since the dataset is "locked" until the user authenticates it, typically this dataset + * name is masked (for example, "VISA....1234"). + * + *

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: + * + *

+ * + * @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. + * + *

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. + * + *

Since the dataset is "locked" until the user authenticates it, typically this dataset + * name is masked (for example, "VISA....1234"). + * + *

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: + * + *

+ * + *

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. + * + *

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. * @@ -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. * *

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 CREATOR = new Parcelable.Creator() { diff --git a/core/java/android/view/autofill/FillResponse.java b/core/java/android/view/autofill/FillResponse.java index 3a147674965eb..67eb85a90ab98 100644 --- a/core/java/android/view/autofill/FillResponse.java +++ b/core/java/android/view/autofill/FillResponse.java @@ -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. * *

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. + * + *

For example, for a login page with username/password where the user only has one account in + * the service, the response could be: * *

  *  new FillResponse.Builder()
@@ -67,9 +71,9 @@ import java.util.Set;
  *      .build();
  * 
* - *

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: + *

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: * *

  *  new FillResponse.Builder()
@@ -79,8 +83,8 @@ import java.util.Set;
  *
  * 

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}: * *

  *   new FillResponse.Builder()
@@ -101,9 +105,9 @@ import java.util.Set;
  * 

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: * *

  *  new FillResponse.Builder()
@@ -134,19 +138,31 @@ import java.util.Set;
  *      .build();
  * 
* - *

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. + *

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. + * + *

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 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 mDatasets = new ArrayList<>(); private final Set 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. + * + *

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: + * + *

    + *
  1. When the user data is encrypted and the service must authenticate an object that + * will be used to decrypt it. + *
  2. When the service already acquired the user data but wants to confirm the user's + * identity before the activity is filled with it. + *
+ * + *

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: + * + *

    + *
  • If authentication failed, call {@link + * FillCallback#onFillResponseAuthentication(int)} passing {@link + * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags. + *
  • If authentication succeeded and this response is empty (no datasets), call {@link + * FillCallback#onSuccess(FillResponse)} with a new dataset (that does not require + * authentication). + *
  • 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. + *
+ * + * @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. + * + *

The {@link android.service.autofill.AutoFillService} typically uses this method in 2 + * situations: + * + *

    + *
  1. When the user data is encrypted and the service must authenticate an object that + * will be used to decrypt it. + *
  2. When the service already acquired the user data but wants to confirm the user's + * identity before the activity is filled with it. + *
+ * + *

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: + * + *

    + *
  • 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. + *
  • If it contains one dataset, the activity will be auto-filled right away. + *
  • 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. + *
+ * + *

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. + * + *

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)}. * *

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. * *

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 CREATOR = diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java index 772710e31b882..14cf9e8838627 100644 --- a/core/java/android/view/autofill/Helper.java +++ b/core/java/android/view/autofill/Helper.java @@ -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 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() { diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java index 6779657452bae..47ac1ced4755e 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java @@ -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 { diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java index 83faf1b42423f..77e7b317cba46 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -16,7 +16,11 @@ 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}. * - *

They're kept until the AutoFillService handles a request, or an error occurs. + *

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 mServerCallbacks = new SparseArray<>(); + private static final SparseArray 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? - try { - if (DEBUG) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); - serverCallback.appCallback.autoFill(dataset); - } catch (RemoteException e) { - Slog.w(TAG, "Error auto-filling activity: " + e); - } - removeServerCallbackLocked(callbackId); + session.autoFill(dataset); } } - void removeServerCallbackLocked(int id) { - if (DEBUG) Slog.d(TAG, "Removing " + id + " from server callbacks"); - mServerCallbacks.remove(id); + 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. + * + *

Typically called by the UI after user taps the "Tap to autofill" affordance, or after user + * used the fingerprint sensors to authenticate. + */ + void notifyResponseAuthenticationResult(Bundle extras, int flags) { + if (DEBUG) Slog.d(TAG, "notifyResponseAuthenticationResult(): flags=" + flags + + ", extras=" + bundleToString(extras)); + + synchronized (mLock) { + try { + mService.authenticateFillResponse(extras, flags); + } catch (RemoteException e) { + Slog.w(TAG, "Error sending authentication result back to service: " + e); + } + } + } + + /** + * Notifies the result of a {@link Dataset} authentication request to the service. + * + *

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}). + * + *

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 id; - private final Bundle extras; - private IAutoFillAppCallback appCallback; + private final int mId; + private final Bundle mExtras; + private IAutoFillAppCallback mAppCallback; - private ServerCallback(int id, Bundle extras) { - this.id = id; - this.extras = extras; + // 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()"); + } + + @Override + public void onAcquired(long deviceId, int acquiredInfo, int vendorCode) { + if (DEBUG) Slog.d(TAG, "onAcquired()"); + } + + @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 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 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); - mUi.showOptions(mUserId, id, response); + if (mAutoFillDirectly && response != null) { + final List 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); + + 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. - removeSelf(); + 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); + } + } + } } - @Override - public void highlightSavedFields(AutoFillId[] ids) { + 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(); + } + } + + private void scanFingerprint(long opId) { // TODO(b/33197203): add MetricsLogger call - if (DEBUG) Slog.d(TAG, "showSaved(): " + Arrays.toString(ids)); + if (DEBUG) Slog.d(TAG, "Starting fingerprint scan for op id: " + opId); - mUi.highlightSavedFields(ids); - - 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); } } } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index 6fafd5d7167ff..ad525d45b2355 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -15,15 +15,18 @@ */ 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. + * + *

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. + * + *

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 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 index 0000000000000..79095a197c17d --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -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 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"); + } +}