diff options
| author | 2017-02-02 20:02:51 -0800 | |
|---|---|---|
| committer | 2017-02-06 04:02:35 +0000 | |
| commit | 0f4928f1f73407485d6d94beda1dba1a2360ebbf (patch) | |
| tree | 423b3d3f2a1a51981a7b7f0fdc7575482b706f36 | |
| parent | 21c8595d764bf6cbf2a1b59715c85ca211240ed4 (diff) | |
Refactoring of auto fill - lifecycle, auth, improvements
1. Move management of the remote fill service in a dedicated
class that abstracts away the async and ephemeral nature
of the binding.
2. Update auth to move fingerprint out of the platform and
allow response and dataset auth.
3. Cleaned up the fill and save callback classes.
4. The UI is now shared among all sessions and cleaned up.
5. Reshuffled the remote callbacks to have cleaner separation.
6. Cleaned up and tightened the reponse and dataset classes.
7. Added API to support communicationn with intent based auth.
Test: CTS + manually
bug:31001899
Change-Id: Idc924a01d1aea82807e0397ff7293d2b8470d4d6
25 files changed, 1824 insertions, 2059 deletions
diff --git a/Android.mk b/Android.mk index 22323c5e6772..1da4783bcac5 100644 --- a/Android.mk +++ b/Android.mk @@ -265,8 +265,9 @@ LOCAL_SRC_FILES += \ core/java/android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl \ core/java/android/service/autofill/IAutoFillAppCallback.aidl \ core/java/android/service/autofill/IAutoFillManagerService.aidl \ - core/java/android/service/autofill/IAutoFillServerCallback.aidl \ core/java/android/service/autofill/IAutoFillService.aidl \ + core/java/android/service/autofill/IFillCallback.aidl \ + core/java/android/service/autofill/ISaveCallback.aidl \ core/java/android/service/carrier/ICarrierService.aidl \ core/java/android/service/carrier/ICarrierMessagingCallback.aidl \ core/java/android/service/carrier/ICarrierMessagingService.aidl \ diff --git a/api/current.txt b/api/current.txt index eca3c27d56b6..adc32ee95247 100644 --- a/api/current.txt +++ b/api/current.txt @@ -9092,6 +9092,10 @@ package android.content { field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD"; field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE"; field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID"; + field public static final java.lang.String EXTRA_AUTO_FILL_ASSIST_STRUCTURE = "android.intent.extra.AUTO_FILL_ASSIST_STRUCTURE"; + field public static final java.lang.String EXTRA_AUTO_FILL_CALLBACK = "android.intent.extra.AUTO_FILL_CALLBACK"; + field public static final java.lang.String EXTRA_AUTO_FILL_EXTRAS = "android.intent.extra.AUTO_FILL_EXTRAS"; + field public static final java.lang.String EXTRA_AUTO_FILL_ITEM_ID = "android.intent.extra.AUTO_FILL_ITEM_ID"; field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC"; field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT"; field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC"; @@ -36096,26 +36100,19 @@ 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.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); + public final class FillCallback implements android.os.Parcelable { + method public int describeContents(); method public void onFailure(java.lang.CharSequence); - method public void onFillResponseAuthentication(int); method public void onSuccess(android.view.autofill.FillResponse); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.FillCallback> CREATOR; } public final class SaveCallback { @@ -46755,10 +46752,9 @@ package android.view.autofill { } public static final class Dataset.Builder { - ctor public Dataset.Builder(java.lang.CharSequence); + ctor public Dataset.Builder(java.lang.String, 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 setAuthentication(android.content.IntentSender); 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); } @@ -46770,12 +46766,11 @@ package android.view.autofill { } public static final class FillResponse.Builder { - ctor public FillResponse.Builder(); + ctor public FillResponse.Builder(java.lang.String); 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 setAuthentication(android.content.IntentSender); method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle); } @@ -47201,7 +47196,7 @@ package android.view.textclassifier { public final class TextClassificationManager { method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence); - method public android.view.textclassifier.TextClassifier getDefaultTextClassifier(); + method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier(); } public final class TextClassificationResult { diff --git a/api/system-current.txt b/api/system-current.txt index 55b32f6c3b93..a8000dcf7173 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9504,6 +9504,10 @@ package android.content { field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD"; field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE"; field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID"; + field public static final java.lang.String EXTRA_AUTO_FILL_ASSIST_STRUCTURE = "android.intent.extra.AUTO_FILL_ASSIST_STRUCTURE"; + field public static final java.lang.String EXTRA_AUTO_FILL_CALLBACK = "android.intent.extra.AUTO_FILL_CALLBACK"; + field public static final java.lang.String EXTRA_AUTO_FILL_EXTRAS = "android.intent.extra.AUTO_FILL_EXTRAS"; + field public static final java.lang.String EXTRA_AUTO_FILL_ITEM_ID = "android.intent.extra.AUTO_FILL_ITEM_ID"; field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC"; field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT"; field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC"; @@ -39128,26 +39132,19 @@ 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.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); + public final class FillCallback implements android.os.Parcelable { + method public int describeContents(); method public void onFailure(java.lang.CharSequence); - method public void onFillResponseAuthentication(int); method public void onSuccess(android.view.autofill.FillResponse); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.FillCallback> CREATOR; } public final class SaveCallback { @@ -50160,10 +50157,9 @@ package android.view.autofill { } public static final class Dataset.Builder { - ctor public Dataset.Builder(java.lang.CharSequence); + ctor public Dataset.Builder(java.lang.String, 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 setAuthentication(android.content.IntentSender); 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); } @@ -50175,12 +50171,11 @@ package android.view.autofill { } public static final class FillResponse.Builder { - ctor public FillResponse.Builder(); + ctor public FillResponse.Builder(java.lang.String); 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 setAuthentication(android.content.IntentSender); method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle); } @@ -50606,7 +50601,7 @@ package android.view.textclassifier { public final class TextClassificationManager { method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence); - method public android.view.textclassifier.TextClassifier getDefaultTextClassifier(); + method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier(); } public final class TextClassificationResult { diff --git a/api/test-current.txt b/api/test-current.txt index bc7a0015485b..d36b8de463dc 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -9118,6 +9118,10 @@ package android.content { field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD"; field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE"; field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID"; + field public static final java.lang.String EXTRA_AUTO_FILL_ASSIST_STRUCTURE = "android.intent.extra.AUTO_FILL_ASSIST_STRUCTURE"; + field public static final java.lang.String EXTRA_AUTO_FILL_CALLBACK = "android.intent.extra.AUTO_FILL_CALLBACK"; + field public static final java.lang.String EXTRA_AUTO_FILL_EXTRAS = "android.intent.extra.AUTO_FILL_EXTRAS"; + field public static final java.lang.String EXTRA_AUTO_FILL_ITEM_ID = "android.intent.extra.AUTO_FILL_ITEM_ID"; field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC"; field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT"; field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC"; @@ -36231,26 +36235,19 @@ 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.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); + public final class FillCallback implements android.os.Parcelable { + method public int describeContents(); method public void onFailure(java.lang.CharSequence); - method public void onFillResponseAuthentication(int); method public void onSuccess(android.view.autofill.FillResponse); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.FillCallback> CREATOR; } public final class SaveCallback { @@ -47068,10 +47065,9 @@ package android.view.autofill { } public static final class Dataset.Builder { - ctor public Dataset.Builder(java.lang.CharSequence); + ctor public Dataset.Builder(java.lang.String, 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 setAuthentication(android.content.IntentSender); 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); } @@ -47083,12 +47079,11 @@ package android.view.autofill { } public static final class FillResponse.Builder { - ctor public FillResponse.Builder(); + ctor public FillResponse.Builder(java.lang.String); 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 setAuthentication(android.content.IntentSender); method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle); } @@ -47514,7 +47509,7 @@ package android.view.textclassifier { public final class TextClassificationManager { method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence); - method public android.view.textclassifier.TextClassifier getDefaultTextClassifier(); + method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier(); } public final class TextClassificationResult { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 11303274e9bf..6a8141f865d9 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1806,6 +1806,41 @@ public class Intent implements Parcelable, Cloneable { @SystemApi public static final String EXTRA_PERMISSION_NAME = "android.intent.extra.PERMISSION_NAME"; + /** + * Intent extra: An id if an autofill item ({@link + * android.view.autofill.Dataset} or {@link android.view.autofill.FillResponse}). + * <p> + * Type: String + * </p> + */ + public static final String EXTRA_AUTO_FILL_ITEM_ID = "android.intent.extra.AUTO_FILL_ITEM_ID"; + + /** + * Intent extra: The assist structure which captures the filled screen. + * <p> + * Type: {@link android.app.assist.AssistStructure} + * </p> + */ + public static final String EXTRA_AUTO_FILL_ASSIST_STRUCTURE = + "android.intent.extra.AUTO_FILL_ASSIST_STRUCTURE"; + + /** + * Intent extra: The metadata associated with the authenticated entity ({@link + * android.view.autofill.Dataset} or {@link android.view.autofill.FillResponse}). + * <p> + * Type: {@link android.os.Bundle} + * </p> + */ + public static final String EXTRA_AUTO_FILL_EXTRAS = "android.intent.extra.AUTO_FILL_EXTRAS"; + + /** + * Intent extra: A callback to report an authentication result. + * <p> + * Type: {@link android.view.autofill.FillResponse} + * </p> + */ + public static final String EXTRA_AUTO_FILL_CALLBACK = "android.intent.extra.AUTO_FILL_CALLBACK"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent broadcast actions (see action variable). diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index e99d303642ba..f94e89a2785d 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -17,6 +17,7 @@ package android.os; import android.annotation.IntegerRes; +import android.annotation.NonNull; import android.annotation.Nullable; import android.text.TextUtils; import android.util.ArrayMap; @@ -1339,7 +1340,7 @@ public final class Parcel { } /** - * Flatten a heterogeneous array containing a particular object type into + * Flatten a homogeneous array containing a particular object type into * the parcel, at * the current dataPosition() and growing dataCapacity() if needed. The * type of the objects in the array must be one that implements Parcelable. @@ -1361,7 +1362,7 @@ public final class Parcel { if (val != null) { int N = val.length; writeInt(N); - for (int i=0; i<N; i++) { + for (int i = 0; i < N; i++) { T item = val[i]; if (item != null) { writeInt(1); @@ -1376,6 +1377,146 @@ public final class Parcel { } /** + * Write a uniform (all items are null or the same class) array list of + * parcelables. + * + * @param list The list to write. + * + * @hide + */ + public final <T extends Parcelable> void writeTypedArrayList(@Nullable ArrayList<T> list, + int parcelableFlags) { + if (list != null) { + int N = list.size(); + writeInt(N); + boolean wroteCreator = false; + for (int i = 0; i < N; i++) { + T item = list.get(i); + if (item != null) { + writeInt(1); + if (!wroteCreator) { + writeParcelableCreator(item); + wroteCreator = true; + } + item.writeToParcel(this, parcelableFlags); + } else { + writeInt(0); + } + } + } else { + writeInt(-1); + } + } + + /** + * Reads a uniform (all items are null or the same class) array list of + * parcelables. + * + * @return The list or null. + * + * @hide + */ + public final @Nullable <T> ArrayList<T> readTypedArrayList(@Nullable ClassLoader loader) { + int N = readInt(); + if (N <= 0) { + return null; + } + Parcelable.Creator<?> creator = null; + ArrayList<T> result = new ArrayList<T>(N); + for (int i = 0; i < N; i++) { + if (readInt() != 0) { + if (creator == null) { + creator = readParcelableCreator(loader); + if (creator == null) { + return null; + } + } + final T parcelable; + if (creator instanceof Parcelable.ClassLoaderCreator<?>) { + Parcelable.ClassLoaderCreator<?> classLoaderCreator = + (Parcelable.ClassLoaderCreator<?>) creator; + parcelable = (T) classLoaderCreator.createFromParcel(this, loader); + } else { + parcelable = (T) creator.createFromParcel(this); + } + result.add(parcelable); + } else { + result.add(null); + } + } + return result; + } + + /** + * Write a uniform (all items are null or the same class) array set of + * parcelables. + * + * @param set The set to write. + * + * @hide + */ + public final <T extends Parcelable> void writeTypedArraySet(@Nullable ArraySet<T> set, + int parcelableFlags) { + if (set != null) { + int N = set.size(); + writeInt(N); + boolean wroteCreator = false; + for (int i = 0; i < N; i++) { + T item = set.valueAt(i); + if (item != null) { + writeInt(1); + if (!wroteCreator) { + writeParcelableCreator(item); + wroteCreator = true; + } + item.writeToParcel(this, parcelableFlags); + } else { + writeInt(0); + } + } + } else { + writeInt(-1); + } + } + + /** + * Reads a uniform (all items are null or the same class) array set of + * parcelables. + * + * @return The set or null. + * + * @hide + */ + public final @Nullable <T> ArraySet<T> readTypedArraySet(@Nullable ClassLoader loader) { + int N = readInt(); + if (N <= 0) { + return null; + } + Parcelable.Creator<?> creator = null; + ArraySet<T> result = new ArraySet<T>(N); + for (int i = 0; i < N; i++) { + T parcelable = null; + if (readInt() != 0) { + if (creator == null) { + creator = readParcelableCreator(loader); + if (creator == null) { + return null; + } + } + if (creator instanceof Parcelable.ClassLoaderCreator<?>) { + Parcelable.ClassLoaderCreator<?> classLoaderCreator = + (Parcelable.ClassLoaderCreator<?>) creator; + parcelable = (T) classLoaderCreator.createFromParcel(this, loader); + } else { + parcelable = (T) creator.createFromParcel(this); + } + } + result.append(parcelable); + } + return result; + } + + /** * Flatten the Parcelable object into the parcel. * * @param val The Parcelable object to be written. diff --git a/core/java/android/service/autofill/AutoFillService.java b/core/java/android/service/autofill/AutoFillService.java index bfaf23cd5fae..aa4d26c18d7c 100644 --- a/core/java/android/service/autofill/AutoFillService.java +++ b/core/java/android/service/autofill/AutoFillService.java @@ -15,7 +15,10 @@ */ package android.service.autofill; -import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.RemoteException; +import com.android.internal.os.HandlerCaller; import android.annotation.SdkConstant; import android.app.Activity; import android.app.Service; @@ -24,21 +27,13 @@ import android.content.Intent; import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; +import android.os.ICancellationSignal; import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; import android.util.Log; -import android.view.autofill.Dataset; import android.view.autofill.FillResponse; -import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; -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. @@ -48,12 +43,7 @@ import java.util.List; * <p>Apps providing auto-fill capabilities must extend this service. */ 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. @@ -79,85 +69,33 @@ 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."; - - /** - * Key of the {@link Bundle} passed to methods such as - * {@link #onSaveRequest(AssistStructure, Bundle, SaveCallback)} containing the extras set by - * {@link android.view.autofill.FillResponse.Builder#setExtras(Bundle)}. - */ - public static final String EXTRA_RESPONSE_EXTRAS = KEY_PREFIX + "RESPONSE_EXTRAS"; - - /** - * Key of the {@link Bundle} passed to methods such as - * {@link #onSaveRequest(AssistStructure, Bundle, SaveCallback)} containing the extras set by - * {@link android.view.autofill.Dataset.Builder#setExtras(Bundle)}. - */ - 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_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 static final int MSG_SAVE = 6; + private static final int MSG_ON_FILL_REQUEST = 3; + private static final int MSG_ON_SAVE_REQUEST = 4; private final IAutoFillService mInterface = new IAutoFillService.Stub() { - @Override - public void autoFill(AssistStructure structure, IAutoFillServerCallback callback) { - mHandlerCaller - .obtainMessageOO(MSG_AUTO_FILL_ACTIVITY, structure, callback) - .sendToTarget(); - } - - @Override - public void save(AssistStructure structure, IAutoFillServerCallback callback, - Bundle extras) throws RemoteException { - mHandlerCaller - .obtainMessageOOO(MSG_SAVE, structure, callback, extras) + public void onFillRequest(AssistStructure structure, Bundle extras, + IFillCallback callback) { + ICancellationSignal transport = CancellationSignal.createTransport(); + try { + callback.onCancellable(transport); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + mHandlerCaller.obtainMessageOOOO(MSG_ON_FILL_REQUEST, structure, + CancellationSignal.fromTransport(transport), extras, callback) .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); + public void onSaveRequest(AssistStructure structure, Bundle extras, + ISaveCallback callback) { + mHandlerCaller.obtainMessageOOO(MSG_ON_SAVE_REQUEST, structure, + extras, callback).sendToTarget(); } @Override @@ -171,63 +109,41 @@ public abstract class AutoFillService extends Service { } }; - private final HandlerCaller.Callback mHandlerCallback = new HandlerCaller.Callback() { - - @Override - public void executeMessage(Message msg) { - switch (msg.what) { - case MSG_CONNECT: { - onConnected(); - break; - } case MSG_AUTO_FILL_ACTIVITY: { - final SomeArgs args = (SomeArgs) msg.obj; - try { - final AssistStructure structure = (AssistStructure) args.arg1; - final IAutoFillServerCallback callback = - (IAutoFillServerCallback) args.arg2; - handleAutoFill(structure, callback); - } finally { - args.recycle(); - } - break; - } case MSG_SAVE: { - final SomeArgs args = (SomeArgs) msg.obj; - try { - final AssistStructure structure = (AssistStructure) args.arg1; - final IAutoFillServerCallback callback = - (IAutoFillServerCallback) args.arg2; - final Bundle extras = (Bundle) args.arg3; - handleSave(structure, callback, extras); - } finally { - args.recycle(); - } - 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; - } default: { - Log.w(TAG, "MyCallbacks received invalid message type: " + msg); - } + private final HandlerCaller.Callback mHandlerCallback = (msg) -> { + switch (msg.what) { + case MSG_CONNECT: { + onConnected(); + break; + } case MSG_ON_FILL_REQUEST: { + final SomeArgs args = (SomeArgs) msg.obj; + final AssistStructure structure = (AssistStructure) args.arg1; + final CancellationSignal cancellation = (CancellationSignal) args.arg2; + final Bundle extras = (Bundle) args.arg3; + final IFillCallback callback = (IFillCallback) args.arg4; + final FillCallback fillCallback = new FillCallback(callback); + args.recycle(); + onFillRequest(structure, extras, cancellation, fillCallback); + break; + } case MSG_ON_SAVE_REQUEST: { + final SomeArgs args = (SomeArgs) msg.obj; + final AssistStructure structure = (AssistStructure) args.arg1; + final Bundle extras = (Bundle) args.arg2; + final ISaveCallback callback = (ISaveCallback) args.arg3; + final SaveCallback saveCallback = new SaveCallback(callback); + args.recycle(); + onSaveRequest(structure, extras, saveCallback); + break; + } case MSG_DISCONNECT: { + onDisconnected(); + break; + } default: { + Log.w(TAG, "MyCallbacks received invalid message type: " + msg); } } }; private HandlerCaller mHandlerCaller; - // User for debugging purposes - private final List<CallbackHelper.Dumpable> mPendingCallbacks = - DEBUG_PENDING_CALLBACKS ? new ArrayList<>() : null; - /** * {@inheritDoc} * @@ -236,7 +152,6 @@ public abstract class AutoFillService extends Service { @Override public void onCreate() { super.onCreate(); - mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(), mHandlerCallback, true); } @@ -255,7 +170,7 @@ public abstract class AutoFillService extends Service { * <p>You should generally do initialization here rather than in {@link #onCreate}. */ public void onConnected() { - if (DEBUG) Log.d(TAG, "onConnected()"); + } /** @@ -267,14 +182,18 @@ public abstract class AutoFillService extends Service { * to notify the result of the request. * * @param structure {@link Activity}'s view structure. - * @param data bundle containing additional arguments set by the Android system (currently none) - * or data passed by the service on previous calls to fullfill other sections of this activity - * (see {@link FillResponse} Javadoc for examples of multiple-sections requests). - * @param cancellationSignal signal for observing cancel requests. + * @param data bundle containing data passed by the service on previous calls to fill. + * This bundle allows your service to keep state between fill and save requests + * as well as when filling different sections of the UI as the system will try to + * aggressively unbind from the service to conserve resources. See {@link FillResponse} + * Javadoc for examples of multiple-sections requests. + * @param cancellationSignal signal for observing cancellation requests. The system will use + * this to notify you that the fill result is no longer needed and you should stop + * handling this fill request in order to save resources. * @param callback object used to notify the result of the request. */ - public abstract void onFillRequest(AssistStructure structure, Bundle data, - CancellationSignal cancellationSignal, FillCallback callback); + public abstract void onFillRequest(@NonNull AssistStructure structure, @Nullable Bundle data, + @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback); /** * Called when user requests service to save the fields of an {@link Activity}. @@ -284,108 +203,15 @@ public abstract class AutoFillService extends Service { * to notify the result of the request. * * @param structure {@link Activity}'s view structure. - * @param data bundle containing additional arguments set by the Android system (currently none) - * or data passed by the service in the {@link FillResponse} that originated this call. + * @param data bundle containing data passed by the service on previous calls to fill. + * This bundle allows your service to keep state between fill and save requests + * as well as when filling different sections of the UI as the system will try to + * aggressively unbind from the service to conserve resources. See {@link FillResponse} + * Javadoc for examples of multiple-sections requests. * @param callback object used to notify the result of the request. */ - public abstract void onSaveRequest(AssistStructure structure, Bundle data, - SaveCallback callback); - - /** - * Called as result of the user action for a {@link FillResponse} that required authentication. - * - * <p>When the {@link FillResponse} required authentication through - * {@link android.view.autofill.FillResponse.Builder#requiresCustomAuthentication(Bundle, int)}, - * this call indicates the user is requesting the service to authenticate him/her (and - * {@code flags} contains {@link #FLAG_AUTHENTICATION_REQUESTED}), and {@code extras} contains - * the {@link Bundle} passed to that method. - * - * <p>When the {@link FillResponse} required authentication through - * {@link android.view.autofill.FillResponse.Builder#requiresFingerprintAuthentication( - * android.hardware.fingerprint.FingerprintManager.CryptoObject, Bundle, int)}, - * {@code flags} this call contains the result of the fingerprint authentication (such as - * {@link #FLAG_AUTHENTICATION_SUCCESS}, {@link #FLAG_AUTHENTICATION_ERROR}, and - * {@link #FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}) and {@code extras} contains the - * {@link Bundle} passed to that method. - */ - public void onFillResponseAuthenticationRequest(@SuppressWarnings("unused") Bundle extras, - int flags) { - if (DEBUG) Log.d(TAG, "onFillResponseAuthenticationRequest(): flags=" + flags); - } - - /** - * Called as result of the user action for a {@link Dataset} that required authentication. - * - * <p>When the {@link Dataset} required authentication through - * {@link android.view.autofill.Dataset.Builder#requiresCustomAuthentication(Bundle, int)}, this - * call indicates the user is requesting the service to authenticate him/her (and {@code flags} - * contains {@link #FLAG_AUTHENTICATION_REQUESTED}), and {@code extras} contains the - * {@link Bundle} passed to that method. - * - * <p>When the {@link Dataset} required authentication through - * {@link android.view.autofill.Dataset.Builder#requiresFingerprintAuthentication( - * android.hardware.fingerprint.FingerprintManager.CryptoObject, Bundle, int)}, - * {@code flags} this call contains the result of the fingerprint authentication (such as - * {@link #FLAG_AUTHENTICATION_SUCCESS}, {@link #FLAG_AUTHENTICATION_ERROR}, and - * {@link #FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}) and {@code extras} contains the - * {@link Bundle} passed to that method. - */ - public void onDatasetAuthenticationRequest(@SuppressWarnings("unused") Bundle extras, - int flags) { - if (DEBUG) Log.d(TAG, "onDatasetAuthenticationRequest(): flags=" + flags); - } - - @Override - @CallSuper - 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 handleAutoFill(AssistStructure structure, IAutoFillServerCallback callback) { - final FillCallback fillCallback = new FillCallback(callback); - if (DEBUG_PENDING_CALLBACKS) { - addPendingCallback(fillCallback); - } - // TODO(b/33197203): hook up the cancelationSignal - onFillRequest(structure, null, new CancellationSignal(), fillCallback); - return; - } - - private void handleSave(AssistStructure structure, IAutoFillServerCallback callback, - Bundle extras) { - final SaveCallback saveCallback = new SaveCallback(callback); - if (DEBUG_PENDING_CALLBACKS) { - addPendingCallback(saveCallback); - } - onSaveRequest(structure, extras, saveCallback); - } - - 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); - } + public abstract void onSaveRequest(@NonNull AssistStructure structure, @Nullable Bundle data, + @NonNull SaveCallback callback); /** * Called when the Android system disconnects from the service. @@ -393,6 +219,6 @@ public abstract class AutoFillService extends Service { * <p> At this point this service may no longer be an active {@link AutoFillService}. */ public void onDisconnected() { - if (DEBUG) Log.d(TAG, "onDisconnected()"); + } } diff --git a/core/java/android/service/autofill/AutoFillServiceInfo.java b/core/java/android/service/autofill/AutoFillServiceInfo.java index fd957f179443..985e32fcd966 100644 --- a/core/java/android/service/autofill/AutoFillServiceInfo.java +++ b/core/java/android/service/autofill/AutoFillServiceInfo.java @@ -16,6 +16,7 @@ package android.service.autofill; import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppGlobals; import android.content.ComponentName; @@ -25,7 +26,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.RemoteException; -import android.util.AndroidException; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; @@ -40,13 +40,10 @@ import java.io.IOException; /** * {@link ServiceInfo} and meta-data about an {@link AutoFillService}. * - * <p>Upon construction, if {@link #getParseError()} is {@code null}, then the service is configured - * correctly. Otherwise, {@link #getParseError()} indicates the parsing error. - * * @hide */ public final class AutoFillServiceInfo { - static final String TAG = "AutoFillServiceInfo"; + private static final String TAG = "AutoFillServiceInfo"; private static ServiceInfo getServiceInfoOrThrow(ComponentName comp, int userHandle) throws PackageManager.NameNotFoundException { @@ -63,10 +60,9 @@ public final class AutoFillServiceInfo { throw new PackageManager.NameNotFoundException(comp.toString()); } - @Nullable - private final String mParseError; - + @NonNull private final ServiceInfo mServiceInfo; + @Nullable private final String mSettingsActivity; @@ -77,17 +73,7 @@ public final class AutoFillServiceInfo { public AutoFillServiceInfo(PackageManager pm, ServiceInfo si) { mServiceInfo = si; - TypedArray metaDataArray; - try { - metaDataArray = getMetaDataArray(pm, si); - } catch (AndroidException e) { - mParseError = e.getMessage(); - mSettingsActivity = null; - Log.w(TAG, mParseError, e); - return; - } - - mParseError = null; + final TypedArray metaDataArray = getMetaDataArray(pm, si); if (metaDataArray != null) { mSettingsActivity = metaDataArray.getString(R.styleable.AutoFillService_settingsActivity); @@ -101,12 +87,11 @@ public final class AutoFillServiceInfo { * Gets the meta-data as a TypedArray, or null if not provided, or throws if invalid. */ @Nullable - private static TypedArray getMetaDataArray(PackageManager pm, ServiceInfo si) - throws AndroidException { + private static TypedArray getMetaDataArray(PackageManager pm, ServiceInfo si) { // Check for permissions. if (!Manifest.permission.BIND_AUTO_FILL.equals(si.permission)) { - throw new AndroidException( - "Service does not require permission " + Manifest.permission.BIND_AUTO_FILL); + Log.e(TAG, "Service does not require permission " + Manifest.permission.BIND_AUTO_FILL); + return null; } // Get the AutoFill metadata, if declared. @@ -125,11 +110,13 @@ public final class AutoFillServiceInfo { && type != XmlPullParser.START_TAG) { } } catch (XmlPullParserException | IOException e) { - throw new AndroidException("Error parsing auto fill service meta-data: " + e, e); + Log.e(TAG, "Error parsing auto fill service meta-data", e); + return null; } if (!"autofill-service".equals(parser.getName())) { - throw new AndroidException("Meta-data does not start with autofill-service tag"); + Log.e(TAG, "Meta-data does not start with autofill-service tag"); + return null; } attrs = Xml.asAttributeSet(parser); @@ -138,7 +125,8 @@ public final class AutoFillServiceInfo { try { res = pm.getResourcesForApplication(si.applicationInfo); } catch (PackageManager.NameNotFoundException e) { - throw new AndroidException("Error getting application resources: " + e, e); + Log.e(TAG, "Error getting application resources", e); + return null; } return res.obtainAttributes(attrs, R.styleable.AutoFillService); @@ -147,11 +135,6 @@ public final class AutoFillServiceInfo { } } - @Nullable - public String getParseError() { - return mParseError; - } - public ServiceInfo getServiceInfo() { return mServiceInfo; } diff --git a/core/java/android/service/autofill/FillCallback.java b/core/java/android/service/autofill/FillCallback.java index 7cab7ae1c479..a306809886a7 100644 --- a/core/java/android/service/autofill/FillCallback.java +++ b/core/java/android/service/autofill/FillCallback.java @@ -16,56 +16,32 @@ 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.Parcel; +import android.os.Parcelable; import android.os.RemoteException; -import android.service.autofill.CallbackHelper.Dumpable; -import android.service.autofill.CallbackHelper.Finalizer; -import android.util.Log; -import android.view.autofill.Dataset; import android.view.autofill.FillResponse; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.Preconditions; - -import java.io.PrintWriter; - /** * Handles auto-fill requests from the {@link AutoFillService} into the {@link Activity} being * auto-filled. - * - * <p>This class is thread safe. */ -public final class FillCallback 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; - - @GuardedBy("mCallback") - private int mState = STATE_INITIAL; - - @GuardedBy("mCallback") - private Finalizer mFinalizer; +public final class FillCallback implements Parcelable { + private final IFillCallback mCallback; + private boolean mCalled; /** @hide */ - FillCallback(IAutoFillServerCallback callback) { + public FillCallback(IFillCallback callback) { mCallback = callback; } + /** @hide */ + private FillCallback(Parcel parcel) { + mCallback = IFillCallback.Stub.asInterface(parcel.readStrongBinder()); + } + /** * Notifies the Android System that an * {@link AutoFillService#onFillRequest(android.app.assist.AssistStructure, Bundle, @@ -76,43 +52,12 @@ public final class FillCallback implements Dumpable { * {@link FillResponse} for examples. */ public void onSuccess(@Nullable FillResponse response) { - final boolean authRequired = response != null && response.isAuthRequired(); - - if (DEBUG) Log.d(TAG, "onSuccess(): authReq= " + authRequired + ", resp=" + response); - - synchronized (mCallback) { - if (authRequired) { - assertOnStateLocked(STATE_INITIAL); - } else { - assertOnStateLocked(STATE_INITIAL | STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE - | STATE_WAITING_DATASET_AUTH_RESPONSE); - } - - try { - mCallback.showResponse(response); - if (authRequired) { - mState = STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE; - } else { - // Check if at least one dataset requires authentication. - boolean waitingAuth = false; - if (response != null) { - for (Dataset dataset : response.getDatasets()) { - if (dataset.isAuthRequired()) { - waitingAuth = true; - break; - } - } - } - if (waitingAuth) { - mState = STATE_WAITING_DATASET_AUTH_RESPONSE; - } else { - setFinalStateLocked(STATE_FINISHED_OK); - } - } - } catch (RemoteException e) { - setFinalStateLocked(STATE_FINISHED_ERROR); - e.rethrowAsRuntimeException(); - } + assertNotCalled(); + mCalled = true; + try { + mCallback.onSuccess(response); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); } } @@ -124,118 +69,43 @@ public final class FillCallback implements Dumpable { * * @param message error message to be displayed to the user. */ - public void onFailure(CharSequence message) { - if (DEBUG) Log.d(TAG, "onFailure(): message=" + message); - - Preconditions.checkArgument(message != null, "message cannot be null"); - - synchronized (mCallback) { - assertOnStateLocked(STATE_INITIAL | STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE - | STATE_WAITING_DATASET_AUTH_RESPONSE); - - try { - mCallback.showError(message); - setFinalStateLocked(STATE_FINISHED_FAILURE); - } catch (RemoteException e) { - setFinalStateLocked(STATE_FINISHED_ERROR); - e.rethrowAsRuntimeException(); - } - } - } - - /** - * Notifies the Android System when the user authenticated a {@link FillResponse} previously - * passed to {@link #onSuccess(FillResponse)}. - * - * @param flags must contain either - * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or - * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}. - */ - public void onFillResponseAuthentication(int flags) { - if (DEBUG) Log.d(TAG, "onFillResponseAuthentication(): flags=" + flags); - - synchronized (mCallback) { - assertOnStateLocked(STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE); - - try { - mCallback.unlockFillResponse(flags); - setFinalStateLocked(STATE_FINISHED_AUTHENTICATED); - } catch (RemoteException e) { - setFinalStateLocked(STATE_FINISHED_ERROR); - e.rethrowAsRuntimeException(); - } - } - } - - /** - * Notifies the Android System when the user authenticated a {@link Dataset} previously passed - * to {@link #onSuccess(FillResponse)}. - * - * @param dataset values to fill the activity with in case of successful authentication of a - * previously locked (and empty) dataset). - * @param flags must contain either - * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or - * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}. - */ - public void onDatasetAuthentication(@Nullable Dataset dataset, int flags) { - if (DEBUG) Log.d(TAG, "onDatasetAuthentication(): dataset=" + dataset + ", flags=" + flags); - - synchronized (mCallback) { - assertOnStateLocked(STATE_WAITING_DATASET_AUTH_RESPONSE); - - try { - mCallback.unlockDataset(dataset, flags); - setFinalStateLocked(STATE_FINISHED_AUTHENTICATED); - } catch (RemoteException e) { - setFinalStateLocked(STATE_FINISHED_ERROR); - e.rethrowAsRuntimeException(); - } + public void onFailure(@Nullable CharSequence message) { + assertNotCalled(); + mCalled = true; + try { + mCallback.onFailure(message); + } catch (RemoteException e) { + 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); + public int describeContents() { + return 0; } /** @hide */ @Override - public void setFinalizer(Finalizer f) { - synchronized (mCallback) { - mFinalizer = f; - } + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeStrongBinder(mCallback.asBinder()); } - /** - * 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(); + private void assertNotCalled() { + if (mCalled) { + throw new IllegalStateException("Already called"); } } - // 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); + public static final Creator<FillCallback> CREATOR = new Creator<FillCallback>() { + @Override + public FillCallback createFromParcel(Parcel parcel) { + return new FillCallback(parcel); + } - Preconditions.checkState((flags & mState) != 0, - "invalid state: required " + flagsToString(FillCallback.class, "STATE_", flags) - + ", current is " + flagsToString(FillCallback.class, "STATE_", mState)); - } + @Override + public FillCallback[] newArray(int size) { + return new FillCallback[size]; + } + }; } diff --git a/core/java/android/service/autofill/IAutoFillAppCallback.aidl b/core/java/android/service/autofill/IAutoFillAppCallback.aidl index 8c3898ab2b15..d9c161c1994b 100644 --- a/core/java/android/service/autofill/IAutoFillAppCallback.aidl +++ b/core/java/android/service/autofill/IAutoFillAppCallback.aidl @@ -18,6 +18,8 @@ package android.service.autofill; import java.util.List; +import android.content.Intent; +import android.content.IntentSender; import android.view.autofill.Dataset; /** @@ -31,4 +33,9 @@ oneway interface IAutoFillAppCallback { * Auto-fills the activity with the contents of a dataset. */ void autoFill(in Dataset dataset); + + /** + * Start an intent sender from the context of the filled app + */ + void startIntentSender(in IntentSender intent, in Intent fillInIntent); } diff --git a/core/java/android/service/autofill/IAutoFillService.aidl b/core/java/android/service/autofill/IAutoFillService.aidl index a4e6ebc996d3..fa1ea65e30b0 100644 --- a/core/java/android/service/autofill/IAutoFillService.aidl +++ b/core/java/android/service/autofill/IAutoFillService.aidl @@ -18,19 +18,20 @@ package android.service.autofill; import android.app.assist.AssistStructure; import android.os.Bundle; -import android.service.autofill.IAutoFillServerCallback; +import android.service.autofill.IFillCallback; +import android.service.autofill.ISaveCallback; import com.android.internal.os.IResultReceiver; /** + * Interface from the system to an auto fill service. + * * @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); - void save(in AssistStructure structure, in IAutoFillServerCallback callback, in Bundle extras); - void authenticateFillResponse(in Bundle extras, int flags); - void authenticateDataset(in Bundle extras, int flags); + void onFillRequest(in AssistStructure structure, in Bundle extras, + in IFillCallback callback); + void onSaveRequest(in AssistStructure structure, in Bundle extras, + in ISaveCallback callback); void onConnected(); void onDisconnected(); } diff --git a/core/java/android/service/autofill/IAutoFillServerCallback.aidl b/core/java/android/service/autofill/IFillCallback.aidl index 480438ac44ea..537403e72fc5 100644 --- a/core/java/android/service/autofill/IAutoFillServerCallback.aidl +++ b/core/java/android/service/autofill/IFillCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,24 +16,17 @@ package android.service.autofill; -import java.util.List; +import android.os.ICancellationSignal; -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. + * Interface to receive the result of a save request. * * @hide */ -// TODO(b/33197203): rename to IAutoFillServerSession -oneway interface IAutoFillServerCallback { - // TODO(b/33197203): document methods - void showResponse(in FillResponse response); - void showError(CharSequence message); - void onSaved(); - void unlockFillResponse(int flags); - void unlockDataset(in Dataset dataset, int flags); +interface IFillCallback { + void onCancellable(in ICancellationSignal cancellation); + void onSuccess(in FillResponse response); + void onFailure(CharSequence message); } diff --git a/core/java/android/service/autofill/CallbackHelper.java b/core/java/android/service/autofill/ISaveCallback.aidl index ded8f97847c5..e260c7375cc5 100644 --- a/core/java/android/service/autofill/CallbackHelper.java +++ b/core/java/android/service/autofill/ISaveCallback.aidl @@ -13,18 +13,15 @@ * 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); - } +package android.service.autofill; - static interface Finalizer { - void gone(); - } +/** + * Interface to receive the result of a save request. + * + * @hide + */ +interface ISaveCallback { + void onSuccess(); + void onFailure(CharSequence message); } diff --git a/core/java/android/service/autofill/SaveCallback.java b/core/java/android/service/autofill/SaveCallback.java index 9dd979599f5d..46b307233e88 100644 --- a/core/java/android/service/autofill/SaveCallback.java +++ b/core/java/android/service/autofill/SaveCallback.java @@ -16,21 +16,9 @@ package android.service.autofill; -import static android.service.autofill.AutoFillService.DEBUG; - 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 @@ -38,20 +26,12 @@ import java.io.PrintWriter; * * <p>This class is thread safe. */ -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; +public final class SaveCallback { + private final ISaveCallback mCallback; + private boolean mCalled; /** @hide */ - SaveCallback(IAutoFillServerCallback callback) { + SaveCallback(ISaveCallback callback) { mCallback = callback; } @@ -63,17 +43,12 @@ public final class SaveCallback implements Dumpable { * @throws RuntimeException if an error occurred while calling the Android System. */ public void onSuccess() { - if (DEBUG) Log.d(TAG, "onSuccess()"); - - synchronized (mCallback) { - checkNotRepliedYetLocked(); - try { - mCallback.onSaved(); - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } finally { - setRepliedLocked(); - } + assertNotCalled(); + mCalled = true; + try { + mCallback.onSuccess(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); } } @@ -87,54 +62,18 @@ public final class SaveCallback implements Dumpable { * @throws RuntimeException if an error occurred while calling the Android System. */ public void onFailure(CharSequence message) { - if (DEBUG) Log.d(TAG, "onFailure(): message=" + message); - - 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; + assertNotCalled(); + mCalled = true; + try { + mCallback.onFailure(message); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); } } - @Override - public String toString() { - if (!DEBUG) return super.toString(); - - return "SaveCallback: [mReplied= " + mReplied + "]"; - } - - // There can be only one!! - private void checkNotRepliedYetLocked() { - Preconditions.checkState(!mReplied, "already replied"); - } - - private void setRepliedLocked() { - if (DEBUG) Log.d(TAG, "setReplied()"); - - mReplied = true; - - if (mFinalizer != null) { - mFinalizer.gone(); + private void assertNotCalled() { + if (mCalled) { + throw new IllegalStateException("Already called"); } } } diff --git a/core/java/android/view/autofill/AutoFillManager.java b/core/java/android/view/autofill/AutoFillManager.java index f2f522daa707..147d72a09f14 100644 --- a/core/java/android/view/autofill/AutoFillManager.java +++ b/core/java/android/view/autofill/AutoFillManager.java @@ -16,6 +16,8 @@ package android.view.autofill; +import static android.view.autofill.Helper.DEBUG; + import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; @@ -31,7 +33,6 @@ import android.view.View; public final class AutoFillManager { private static final String TAG = "AutoFillManager"; - private static final boolean DEBUG = true; // TODO(b/33197203): change to false once stable /** * Flag used to show the auto-fill UI affordance for a view. diff --git a/core/java/android/view/autofill/AutoFillSession.java b/core/java/android/view/autofill/AutoFillSession.java index eec7a823a841..efc1df6c6e84 100644 --- a/core/java/android/view/autofill/AutoFillSession.java +++ b/core/java/android/view/autofill/AutoFillSession.java @@ -19,7 +19,8 @@ package android.view.autofill; import static android.view.autofill.Helper.DEBUG; import android.app.Activity; -import android.os.RemoteException; +import android.content.Intent; +import android.content.IntentSender; import android.service.autofill.IAutoFillAppCallback; import android.util.Log; import android.view.View; @@ -39,7 +40,7 @@ public final class AutoFillSession { private final IAutoFillAppCallback mCallback = new IAutoFillAppCallback.Stub() { @Override - public void autoFill(Dataset dataset) throws RemoteException { + public void autoFill(Dataset dataset) { final Activity activity = mActivity.get(); if (activity == null) { if (DEBUG) Log.d(TAG, "autoFill(): activity already GCed"); @@ -49,12 +50,10 @@ public final class AutoFillSession { // dataset.extras to service activity.runOnUiThread(() -> { final View root = activity.getWindow().getDecorView().getRootView(); - for (DatasetField field : dataset.getFields()) { - final AutoFillId id = field.getId(); - if (id == null) { - Log.w(TAG, "autoFill(): null id on " + field); - continue; - } + final int itemCount = dataset.getFieldIds().size(); + for (int i = 0; i < itemCount; i++) { + final AutoFillId id = dataset.getFieldIds().get(i); + final AutoFillValue value = dataset.getFieldValues().get(i); final int viewId = id.getViewId(); final View view = root.findViewByAccessibilityIdTraversal(viewId); if (view == null) { @@ -79,14 +78,28 @@ public final class AutoFillSession { Log.d(TAG, "autoFill(): delegating " + id + " to VirtualViewDelegate " + delegate); } - delegate.autoFill(id.getVirtualChildId(), field.getValue()); + delegate.autoFill(id.getVirtualChildId(), value); } else { // Handle non-virtual fields itself. - view.autoFill(field.getValue()); + view.autoFill(value); } } }); } + + @Override + public void startIntentSender(IntentSender intent, Intent fillInIntent) { + final Activity activity = mActivity.get(); + if (activity != null) { + activity.runOnUiThread(() -> { + try { + activity.startIntentSender(intent, fillInIntent, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "startIntentSender() failed for intent:" + intent, e); + } + }); + } + } }; private final WeakReference<Activity> mActivity; @@ -114,5 +127,4 @@ public final class AutoFillSession { } } } - } diff --git a/core/java/android/view/autofill/Dataset.java b/core/java/android/view/autofill/Dataset.java index 18a08f930703..2708358c3143 100644 --- a/core/java/android/view/autofill/Dataset.java +++ b/core/java/android/view/autofill/Dataset.java @@ -17,12 +17,12 @@ package android.view.autofill; import static android.view.autofill.Helper.DEBUG; -import static android.view.autofill.Helper.append; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; import android.app.assist.AssistStructure.ViewNode; -import android.hardware.fingerprint.FingerprintManager.CryptoObject; +import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -30,8 +30,6 @@ import android.os.Parcelable; import com.android.internal.util.Preconditions; import java.util.ArrayList; -import java.util.List; -import java.util.Objects; /** * A set of data that can be used to auto-fill an {@link Activity}. @@ -44,221 +42,184 @@ import java.util.Objects; * <li>An optional {@link Bundle} with extras (used only by the service creating it). * </ol> * - * See {@link FillResponse} for examples. + * @see FillResponse for examples. */ public final class Dataset implements Parcelable { - + private final String mId; private final CharSequence mName; - private final ArrayList<DatasetField> mFields; + private final ArrayList<AutoFillId> mFieldIds; + private final ArrayList<AutoFillValue> mFieldValues; private final Bundle mExtras; - private final int mFlags; - private final boolean mRequiresAuth; - private final boolean mHasCryptoObject; - private final long mCryptoOpId; + private final IntentSender mAuthentication; - private Dataset(Dataset.Builder builder) { + private Dataset(Builder builder) { + mId = builder.mId; mName = builder.mName; - // TODO(b/33197203): make an immutable copy of mFields? - mFields = builder.mFields; + mFieldIds = builder.mFieldIds; + mFieldValues = builder.mFieldValues; mExtras = builder.mExtras; - mFlags = builder.mFlags; - mRequiresAuth = builder.mRequiresAuth; - mHasCryptoObject = builder.mHasCryptoObject; - mCryptoOpId = builder.mCryptoOpId; + mAuthentication = builder.mAuthentication; } /** @hide */ - public CharSequence getName() { - return mName; + public @NonNull String getId() { + return mId; } /** @hide */ - public List<DatasetField> getFields() { - return mFields; - } - - /** @hide */ - public Bundle getExtras() { - return mExtras; + public @NonNull CharSequence getName() { + return mName; } /** @hide */ - public int getFlags() { - return mFlags; + public @Nullable ArrayList<AutoFillId> getFieldIds() { + return mFieldIds; } /** @hide */ - public boolean isAuthRequired() { - return mRequiresAuth; + public @Nullable ArrayList<AutoFillValue> getFieldValues() { + return mFieldValues; } /** @hide */ - public boolean isEmpty() { - return mFields.isEmpty(); + public @Nullable Bundle getExtras() { + return mExtras; } /** @hide */ - public boolean hasCryptoObject() { - return mHasCryptoObject; + public @Nullable IntentSender getAuthentication() { + return mAuthentication; } /** @hide */ - public long getCryptoObjectOpId() { - return mCryptoOpId; + public boolean isEmpty() { + return mFieldIds == null || mFieldIds.isEmpty(); } @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(", flags=").append(mFlags) - .append(", requiresAuth: ").append(mRequiresAuth) - .append(", hasCrypto: ").append(mHasCryptoObject); + final StringBuilder builder = new StringBuilder("Dataset [id=").append(mId) + .append(", name=").append(mName) + .append(", fieldIds=").append(mFieldIds) + .append(", fieldValues=").append(mFieldValues) + .append(", hasAuthentication=").append(mAuthentication != null) + .append(", hasExtras=").append(mExtras != null); return builder.append(']').toString(); } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Dataset other = (Dataset) obj; + if (mId == null) { + if (other.mId != null) { + return false; + } + } else if (!mId.equals(other.mId)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return mId != null ? mId.hashCode() : 0; + } + /** - * A builder for {@link Dataset} objects. + * A builder for {@link Dataset} objects. You must to provide at least + * one value for a field or set an authentication intent. */ public static final class Builder { + private String mId; private CharSequence mName; - private final ArrayList<DatasetField> mFields = new ArrayList<>(); + private ArrayList<AutoFillId> mFieldIds; + private ArrayList<AutoFillValue> mFieldValues; private Bundle mExtras; - private int mFlags; - private boolean mRequiresAuth; - private boolean mHasCryptoObject; - private long mCryptoOpId; + private IntentSender mAuthentication; + private boolean mDestroyed; + + /** @hide */ + // TODO(b/33197203): Remove once GCore migrates + public Builder(@NonNull CharSequence name) { + this(String.valueOf(System.currentTimeMillis()), name); + } /** * Creates a new builder. * + * @param id A required id to identify this dataset for future interactions related to it. * @param name Name used to identify the dataset in the UI. Typically it's the same value as - * the first field in the dataset (like username or email address) or an user-provided name + * the first field in the dataset (like username or email address) or a user-provided name * (like "My Work Address"). */ - public Builder(CharSequence name) { + public Builder(@NonNull String id, @NonNull CharSequence name) { + mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty or null"); 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. + * Requires a dataset authentication before auto-filling the activity with this dataset. * - * <p>This method is typically called when the device (or the service) does not support - * fingerprint authentication (and hence it cannot use {@link - * #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs - * to use a custom authentication UI for the dataset. For example, when a dataset contains - * credit card information (such as number, expiration date, and verification code), the - * service displays an authentication dialog asking for the verification code to unlock the - * rest of the data). + * <p>This method is called when you need to provide an authentication + * UI for the dataset. For example, when a dataset contains credit card information + * (such as number, expiration date, and verification code), you can display UI + * asking for the verification code to before filing in the data). Even if the + * dataset is completely populated the system will launch the specified authentication + * intent and will need your approval to fill it in. Since the dataset is "locked" + * until the user authenticates it, typically this dataset name is masked + * (for example, "VISA....1234"). Typically you would want to store the dataset + * labels non-encypted and the actual sensitive data encrypted and not in memory. + * This allows showing the labels in the UI while involving the user if one of + * the items with these labels is chosen. Note that if you use sensitive data as + * a label, for example an email address, then it should also be encrypted. + *</p> * - * <p>Since the dataset is "locked" until the user authenticates it, typically this dataset - * name is masked (for example, "VISA....1234"). + * <p>When a user selects this dataset, the system triggers the provided intent + * whose extras will have the {@link android.content.Intent#EXTRA_AUTO_FILL_ITEM_ID id} + * of the {@link android.view.autofill.Dataset dataset} to authenticate, the {@link + * android.content.Intent#EXTRA_AUTO_FILL_EXTRAS extras} associated with this + * dataset, and a {@link android.content.Intent#EXTRA_AUTO_FILL_CALLBACK callback} + * to dispatch the authentication result.</p> * - * <p>When the user selects this dataset, the Android System calls {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)} - * passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in - * the flags and the same {@code extras} passed to this method. The service can then - * displays its custom authentication UI, and then call the proper method on {@link - * android.service.autofill.FillCallback} depending on the authentication result and whether - * this dataset already contains the fields needed to auto-fill the activity: + * <p>Once you complete your authentication flow you should use the provided callback + * to notify for a failure or a success. In case of a success you need to provide + * only the fully populated dataset that is being authenticated. For example, if you + * provided a {@link FillResponse} with two {@link Dataset}s and marked that + * only the first dataset needs an authentication then in the provided response + * you need to provide only the fully populated dataset being authenticated instead + * of both of them. + * </p> * - * <ul> - * <li>If authentication failed, call - * {@link android.service.autofill.FillCallback#onDatasetAuthentication(Dataset, - * int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags. - * <li>If authentication succeeded and this datast is empty (no fields), call {@link - * android.service.autofill.FillCallback#onSuccess(FillResponse)} with a new dataset - * (with the proper fields). - * <li>If authentication succeeded and this response is not empty, call {@link - * android.service.autofill.FillCallback#onDatasetAuthentication(Dataset, int)} - * passing - * {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the - * {@code flags} and {@code null} as the {@code dataset}. - * </ul> + * <p>The indent sender mechanism allows you to have your authentication UI + * implemented as an activity or a service or a receiver. However, the recommended + * way is to do this is with an activity which the system will start in the + * filled activity's task meaning it will properly work with back, recent apps, and + * free-form multi-window, while avoiding the need for the "draw on top of other" + * apps special permission. You can still theme your authentication activity's + * UI to look like a dialog if desired.</p> * - * @param extras when set, will be passed back in the {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, - * int)}, call so it could be used by the service to handle state. - * @param flags optional parameters, currently ignored. - */ - public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) { - return requiresAuthentication(null, extras, flags); - } - - /** - * Requires dataset authentication through the Fingerprint sensor before auto-filling the - * activity with this dataset. - * - * <p>This method is typically called when the dataset contains sensitive information (for - * example, credit card information) and the provider requires the user to re-authenticate - * before using it. - * - * <p>Since the dataset is "locked" until the user authenticates it, typically this dataset - * name is masked (for example, "VISA....1234"). - * - * <p>When the user selects this dataset, the Android System displays an UI affordance - * asking the user to use the fingerprint sensor unlock the dataset, and what happens after - * a successful fingerprint authentication depends on whether the dataset is empty (no - * fields, only the masked name) or not: + * <p></><strong>Note:</strong> Do not make the provided intent sender + * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the + * platform needs to fill in the authentication arguments.</p> * - * <ul> - * <li>If it's empty, the Android System will call {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, - * int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the - * flags. - * <li>If it's not empty, the activity will be auto-filled with its data. - * </ul> + * @param authentication Intent to trigger your authentication flow. * - * <p>If the fingerprint authentication fails, the Android System will call {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)} - * passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the - * flags. - * - * <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use - * the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint - * authentication is available before using this method, and use other alternatives (such as - * {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is - * called when fingerprint is not available, Android System will call {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)} - * passing {@link - * android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE} - * in the flags, but it would be wasting system resources (and worsening the user - * experience) in the process. - * - * @param crypto object that will be authenticated. - * @param extras when set, will be passed back in the {@link - * android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)} - * call so it could be used by the service to handle state. - * @param flags optional parameters, currently ignored. + * @see android.app.PendingIntent#getIntentSender() */ - 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(); - } + public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) { + throwIfDestroyed(); + mAuthentication = authentication; return this; } @@ -268,41 +229,55 @@ public final class Dataset implements Parcelable { * @param id id returned by {@link ViewNode#getAutoFillId()}. * @param value value to be auto filled. */ - public Dataset.Builder setValue(AutoFillId id, AutoFillValue value) { - putField(new DatasetField(id, value)); + public @NonNull Builder setValue(@NonNull AutoFillId id, @NonNull AutoFillValue value) { + throwIfDestroyed(); + Preconditions.checkNotNull(id, "id cannot be null"); + Preconditions.checkNotNull(value, "value cannot be null"); + if (mFieldIds != null) { + final int existingIdx = mFieldIds.indexOf(id); + if (existingIdx >= 0) { + mFieldValues.set(existingIdx, value); + return this; + } + } else { + mFieldIds = new ArrayList<>(); + mFieldValues = new ArrayList<>(); + } + mFieldIds.add(id); + mFieldValues.add(value); return this; } /** - * Creates a new {@link Dataset} instance. + * Sets a {@link Bundle} that will be passed to subsequent APIs that + * manipulate this dataset. For example, they are passed in as {@link + * android.content.Intent#EXTRA_AUTO_FILL_EXTRAS extras} to your + * authentication flow. */ - public Dataset build() { - return new Dataset(this); + public @NonNull Builder setExtras(@Nullable Bundle extras) { + throwIfDestroyed(); + mExtras = extras; + return this; } /** - * 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.service.autofill.SaveCallback)}, using - * {@link android.service.autofill.AutoFillService#EXTRA_DATASET_EXTRAS} as the key. - * - * <p>It can be used to keep service state in between calls. + * Creates a new {@link Dataset} instance. You should not interact + * with this builder once this method is called. */ - 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; + public @NonNull Dataset build() { + throwIfDestroyed(); + mDestroyed = true; + if (mFieldIds == null && mAuthentication == null) { + throw new IllegalArgumentException( + "at least one value or an authentication must be set"); + } + return new Dataset(this); } - /** - * Emulates {@code Map.put()} by adding a new field to the list if its id is not the yet, - * or replacing the existing one. - */ - private void putField(DatasetField field) { - // TODO(b/33197203): check if already exists and replaces it if so - mFields.add(field); + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } } } @@ -317,32 +292,33 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mId); parcel.writeCharSequence(mName); - parcel.writeList(mFields); + parcel.writeTypedArrayList(mFieldIds, 0); + parcel.writeTypedArrayList(mFieldValues, 0); parcel.writeBundle(mExtras); - parcel.writeInt(mFlags); - parcel.writeInt(mRequiresAuth ? 1 : 0); - parcel.writeInt(mHasCryptoObject ? 1 : 0); - if (mHasCryptoObject) { - parcel.writeLong(mCryptoOpId); - } - } - - @SuppressWarnings("unchecked") - private Dataset(Parcel parcel) { - 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; + parcel.writeParcelable(mAuthentication, flags); } public static final Parcelable.Creator<Dataset> CREATOR = new Parcelable.Creator<Dataset>() { @Override - public Dataset createFromParcel(Parcel source) { - return new Dataset(source); + public Dataset createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final Builder builder = new Builder(parcel.readString(), parcel.readCharSequence()); + final ArrayList<AutoFillId> ids = parcel.readTypedArrayList(null); + final ArrayList<AutoFillValue> values = parcel.readTypedArrayList(null); + final int idCount = (ids != null) ? ids.size() : 0; + final int valueCount = (values != null) ? values.size() : 0; + for (int i = 0; i < idCount; i++) { + AutoFillId id = ids.get(i); + AutoFillValue value = (valueCount > i) ? values.get(i) : null; + builder.setValue(id, value); + } + builder.setExtras(parcel.readBundle()); + builder.setAuthentication(parcel.readParcelable(null)); + return builder.build(); } @Override diff --git a/core/java/android/view/autofill/DatasetField.java b/core/java/android/view/autofill/DatasetField.java deleted file mode 100644 index c6b92acd5759..000000000000 --- a/core/java/android/view/autofill/DatasetField.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2016 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.view.autofill; - -import static android.view.autofill.Helper.DEBUG; - -import android.os.Parcel; -import android.os.Parcelable; - -/** @hide */ -public final class DatasetField implements Parcelable { - - private final AutoFillId mId; - private final AutoFillValue mValue; - - DatasetField(AutoFillId id, AutoFillValue value) { - mId = id; - mValue = value; - } - - public AutoFillId getId() { - return mId; - } - - public AutoFillValue getValue() { - return mValue; - } - - ///////////////////////////////// - // Object "contract" methods. // - ///////////////////////////////// - - @Override - public String toString() { - if (!DEBUG) return super.toString(); - - return "DatasetField [id=" + mId + ", value=" + mValue + "]"; - } - - ///////////////////////////////////// - // Parcelable "contract" methods. // - ///////////////////////////////////// - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeParcelable(mId, 0); - parcel.writeParcelable(mValue, 0); - } - - private DatasetField(Parcel parcel) { - mId = parcel.readParcelable(null); - mValue = parcel.readParcelable(null); - } - - public static final Parcelable.Creator<DatasetField> CREATOR = - new Parcelable.Creator<DatasetField>() { - @Override - public DatasetField createFromParcel(Parcel source) { - return new DatasetField(source); - } - - @Override - public DatasetField[] newArray(int size) { - return new DatasetField[size]; - } - }; -} diff --git a/core/java/android/view/autofill/FillResponse.java b/core/java/android/view/autofill/FillResponse.java index 48dbb8415ec4..596a06c28d57 100644 --- a/core/java/android/view/autofill/FillResponse.java +++ b/core/java/android/view/autofill/FillResponse.java @@ -16,36 +16,30 @@ package android.view.autofill; import static android.view.autofill.Helper.DEBUG; -import static android.view.autofill.Helper.append; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; -import android.hardware.fingerprint.FingerprintManager.CryptoObject; +import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.service.autofill.FillCallback; +import android.util.ArraySet; import com.android.internal.util.Preconditions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - /** * Response for a {@link * android.service.autofill.AutoFillService#onFillRequest(android.app.assist.AssistStructure, - * Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback)} request. + * Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback)} and + * authentication requests. * * <p>The response typically contains one or more {@link Dataset}s, each representing a set of - * fields that can be auto-filled together, and the Android System displays a dataset picker UI + * fields that can be auto-filled together, and the Android system displays a dataset picker UI * affordance that the user must use before the {@link Activity} is filled with the dataset. * * <p>For example, for a login page with username/password where the user only has one account in - * the service, the response could be: + * the response could be: * * <pre class="prettyprint"> * new FillResponse.Builder() @@ -102,12 +96,17 @@ import java.util.Set; * <p>Notice that the ids that are part of a dataset (ids 1 to 4, in this example) are automatically * added to the {@code savableIds} list. * - * <p>If the service has multiple {@link Dataset}s with multiple options for some fields on each - * dataset (for example, multiple accounts with both a home and work address), then it should - * "partition" the {@link Activity} in sections and populate the response with just a subset of the - * data that would fulfill the first section; then once the user fills the first section and taps a - * field from the next section, the Android system would issue another request for that section, and - * so on. For example, the first response could be: + * <p>If the service has multiple {@link Dataset}s for different sections of the activity, + * for example, a user section for which there are two datasets followed by an address + * section for which there are two datasets for each user user, then it should "partition" + * the activity in sections and populate the response with just a subset of the data that would + * fulfill the first section (the name in our example); then once the user fills the first + * section and taps a field from the next section (the address in our example), the Android + * system would issue another request for that section, and so on. Note that if the user + * chooses to populate the first section with a service provided dataset, the subsequent request + * would contain the populated values so you don't try to provide suggestions for the first + * section but ony for the second one based on the context of what was already filled. For + * example, the first response could be: * * <pre class="prettyprint"> * new FillResponse.Builder() @@ -132,236 +131,182 @@ import java.util.Set; * .setTextFieldValue(id4, "Springfield") * .build()) * .add(new Dataset.Builder("Work") - * .setTextFieldValue(id3, "Springfield Nuclear Power Plant") + * .setTextFieldValue(id3, "Springfield Power Plant") * .setTextFieldValue(id4, "Springfield") * .build()) * .build(); * </pre> * - * <p>The service could require user authentication, either at the {@link FillResponse} or {@link - * Dataset} levels, prior to auto-filling the activity - see {@link - * FillResponse.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, {@link - * FillResponse.Builder#requiresCustomAuthentication(Bundle, int)}, {@link - * Dataset.Builder#requiresFingerprintAuthentication(CryptoObject, Bundle, int)}, and {@link - * Dataset.Builder#requiresCustomAuthentication(Bundle, int)} for details. + * <p>The service could require user authentication at the {@link FillResponse} or the + * {@link Dataset} level, prior to auto-filling an activity - see {@link FillResponse.Builder + * #setAuthentication(IntentSender)} and {@link Dataset.Builder#setAuthentication(IntentSender)}. + * It is recommended that you encrypt only the sensitive data but leave the labels unencrypted + * which would allow you to provide the dataset names to the user and if they choose one + * them challenge the user to authenticate. For example, if the user has a home and a work + * address the Home and Work labels could be stored unencrypted as they don't have any sensitive + * data while the address data is in an encrypted storage. If the user chooses Home, then the + * platform will start your authentication flow. If you encrypt all data and require auth + * at the response level the user will have to interact with the fill UI to trigger a request + * for the datasets as they don't see Home and Work options which will trigger your auth + * flow and after successfully authenticating the user will be presented with the Home and + * Work options where they can pick one. Hence, you have flexibility how to implement your + * auth while storing labels non-encrypted and data encrypted provides a better user + * experience.</p> * - * <p>Finally, the service can use the {@link FillResponse.Builder#setExtras(Bundle)} and/or {@link - * Dataset.Builder#setExtras(Bundle)} methods to pass {@link Bundle}s with service-specific data use - * to identify this response on future calls (like {@link - * android.service.autofill.AutoFillService#onSaveRequest(android.app.assist.AssistStructure, - * Bundle, android.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. + * <p>Finally, the service can use {@link Dataset.Builder#setExtras(Bundle)} methods + * to pass {@link Bundle extras} provided to all future calls related to a dataset, + * for example during authentication and saving.</p> */ public final class FillResponse implements Parcelable { - - private final List<Dataset> mDatasets; - private final AutoFillId[] mSavableIds; + private final String mId; + private final ArraySet<Dataset> mDatasets; + private final ArraySet<AutoFillId> mSavableIds; private final Bundle mExtras; - private final int mFlags; - private final boolean mRequiresAuth; - private final boolean mHasCryptoObject; - private final long mCryptoOpId; + private final IntentSender mAuthentication; - private FillResponse(Builder builder) { - // TODO(b/33197203): make it immutable? + private FillResponse(@NonNull Builder builder) { + mId = builder.mId; mDatasets = builder.mDatasets; - final int size = builder.mSavableIds.size(); - mSavableIds = new AutoFillId[size]; - int i = 0; - for (AutoFillId id : builder.mSavableIds) { - mSavableIds[i++] = id; - } + mSavableIds = builder.mSavableIds; mExtras = builder.mExtras; - mFlags = builder.mFlags; - mRequiresAuth = builder.mRequiresAuth; - mHasCryptoObject = builder.mHasCryptoObject; - mCryptoOpId = builder.mCryptoOpId; - } - - /** @hide */ - public List<Dataset> getDatasets() { - return mDatasets; + mAuthentication = builder.mAuthentication; } /** @hide */ - public AutoFillId[] getSavableIds() { - return mSavableIds; + public @NonNull String getId() { + return mId; } /** @hide */ - public Bundle getExtras() { + public @Nullable Bundle getExtras() { return mExtras; } /** @hide */ - public int getFlags() { - return mFlags; - } - - /** @hide */ - public boolean isAuthRequired() { - return mRequiresAuth; + public @Nullable ArraySet<Dataset> getDatasets() { + return mDatasets; } /** @hide */ - public boolean hasCryptoObject() { - return mHasCryptoObject; + public @Nullable ArraySet<AutoFillId> getSavableIds() { + return mSavableIds; } /** @hide */ - public long getCryptoObjectOpId() { - return mCryptoOpId; + public @Nullable IntentSender getAuthentication() { + return mAuthentication; } /** - * Builder for {@link FillResponse} objects. + * Builder for {@link FillResponse} objects. You must to provide at least + * one dataset or set an authentication intent. */ public static final class Builder { - private final List<Dataset> mDatasets = new ArrayList<>(); - private final Set<AutoFillId> mSavableIds = new HashSet<>(); + private final String mId; + private ArraySet<Dataset> mDatasets; + private ArraySet<AutoFillId> mSavableIds; private Bundle mExtras; - private int mFlags; - private boolean mRequiresAuth; - private boolean mHasCryptoObject; - private long mCryptoOpId; + private IntentSender mAuthentication; + private boolean mDestroyed; + + /** @hide */ + // TODO(b/33197203): Remove once GCore migrates + public Builder() { + this(String.valueOf(System.currentTimeMillis())); + } /** - * Requires user authentication through the {@link android.service.autofill.AutoFillService} - * before handling an auto-fill request. + * Creates a new {@link FillResponse} builder. * - * <p>This method is typically called when the device (or the service) does not support - * fingerprint authentication (and hence it cannot use {@link - * #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs - * to use a custom authentication UI and is used in 2 scenarios: - * - * <ol> - * <li>When the user data is encrypted and the service must authenticate an object that - * will be used to decrypt it. - * <li>When the service already acquired the user data but wants to confirm the user's - * identity before the activity is filled with it. - * </ol> - * - * <p>When this method is called, the Android System displays an UI affordance asking the - * user to tap it to auto-fill the activity; if the user taps it, the Android System calls - * {@link - * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle, - * int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in the flags and - * the same {@code extras} passed to this method. The service can then displays its custom - * authentication UI, and then call the proper method on {@link FillCallback} depending on - * the authentication result and whether this response already contains the {@link Dataset}s - * need to auto-fill the activity: - * - * <ul> - * <li>If authentication failed, call {@link - * FillCallback#onFillResponseAuthentication(int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags. - * <li>If authentication succeeded and this response is empty (no datasets), call {@link - * FillCallback#onSuccess(FillResponse)} with a new dataset (that does not require - * authentication). - * <li>If authentication succeeded and this response is not empty, call {@link - * FillCallback#onFillResponseAuthentication(int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the flags. - * </ul> - * - * @param extras when set, will be passed back in the {@link - * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle, - * int)} call so it could be used by the service to handle state. - * @param flags optional parameters, currently ignored. + * @param id A required id to identify this dataset for future interactions related to it. */ - public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) { - return requiresAuthentication(null, extras, flags); + public Builder(@NonNull String id) { + mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty or null"); } /** - * Requires user authentication through the Fingerprint sensor before handling an auto-fill - * request. + * Requires a fill response authentication before auto-filling the activity with + * any dataset in this response. This is typically useful when a user interaction + * is required to unlock their data vault if you encrypt the dataset labels and + * dataset data. It is recommended to encrypt only the sensitive data and not the + * dataset labels which would allow auth on the dataset level leading to a better + * user experience. Note that if you use sensitive data as a label, for example an + * email address, then it should also be encrypted. * - * <p>The {@link android.service.autofill.AutoFillService} typically uses this method in 2 - * situations: + * <p>This method is called when you need to provide an authentication + * UI for the fill response. For example, when the user's data is stored + * encrypted and needs a user interaction to decrypt before offering fill + * suggestions.</p> * - * <ol> - * <li>When the user data is encrypted and the service must authenticate an object that - * will be used to decrypt it. - * <li>When the service already acquired the user data but wants to confirm the user's - * identity before the activity is filled with it. - * </ol> + * <p>When a user initiates an auto fill, the system triggers the provided + * intent whose extras will have the {@link android.content.Intent + * #EXTRA_AUTO_FILL_ITEM_ID id} of the {@link android.view.autofill.FillResponse}) + * to authenticate, the {@link android.content.Intent#EXTRA_AUTO_FILL_EXTRAS extras} + * associated with this response, and a {@link android.content.Intent + * #EXTRA_AUTO_FILL_CALLBACK callback} to dispatch the authentication result.</p> * - * <p>When this method is called, the Android System displays an UI affordance asking the - * user to use the fingerprint sensor to auto-fill the activity, and what happens after a - * successful fingerprint authentication depends on the number of {@link Dataset}s included - * in this response: + * <p>Once you complete your authentication flow you should use the provided callback + * to notify for a failure or a success. In case of a success you need to provide + * the fully populated response that is being authenticated. For example, if you + * provided an empty {@link FillResponse} because the user's data was locked and + * marked that the response needs an authentication then in the response returned + * if authentication succeeds you need to provide all available datasets some of + * which may need to be further authenticated, for example a credit card whose + * CVV needs to be entered.</p> * - * <ul> - * <li>If it's empty (scenario #1 above), the Android System will call {@link - * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle, - * int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the - * flags. - * <li>If it contains one dataset, the activity will be auto-filled right away. - * <li>If it contains many datasets, the Android System will show dataset picker UI, and - * then auto-fill the activity once the user select the proper datased. - * </ul> + * <p>The indent sender mechanism allows you to have your authentication UI + * implemented as an activity or a service or a receiver. However, the recommended + * way is to do this is with an activity which the system will start in the + * filled activity's task meaning it will properly work with back, recent apps, and + * free-form multi-window, while avoiding the need for the "draw on top of other" + * apps special permission. You can still theme your authentication activity's + * UI to look like a dialog if desired.</p> * - * <p>If the fingerprint authentication fails, the Android System will call {@link - * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle, - * int)} passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} - * in the flags. + * <p></><strong>Note:</strong> Do not make the provided intent sender + * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the + * platform needs to fill in the authentication arguments.</p> * - * <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use - * the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint - * authentication is available before using this method, and use other alternatives (such as - * {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is - * called when fingerprint is not available, Android System will call {@link - * android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle, - * int)} passing {@link - * android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE} - * in the flags, but it would be wasting system resources (and worsening the user - * experience) in the process. + * @param authentication Intent to trigger your authentication flow. * - * @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. + * @see android.app.PendingIntent#getIntentSender() */ - 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(); - } + public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) { + throwIfDestroyed(); + mAuthentication = authentication; return this; } /** - * Adds a new {@link Dataset} to this response. + * Adds a new {@link Dataset} to this response. Adding a dataset with the + * same id updates the existing one. * * @throws IllegalArgumentException if a dataset with same {@code name} already exists. */ - public Builder addDataset(Dataset dataset) { - Preconditions.checkNotNull(dataset, "dataset cannot be null"); - // TODO(b/33197203): check if name already exists - mDatasets.add(dataset); - for (DatasetField field : dataset.getFields()) { - mSavableIds.add(field.getId()); + public@NonNull Builder addDataset(@Nullable Dataset dataset) { + throwIfDestroyed(); + if (dataset == null) { + return this; + } + if (mDatasets == null) { + mDatasets = new ArraySet<>(); + } + final int datasetCount = mDatasets.size(); + for (int i = 0; i < datasetCount; i++) { + if (mDatasets.valueAt(i).getName().equals(dataset.getName())) { + throw new IllegalArgumentException("Duplicate dataset name: " + + dataset.getName()); + } + } + if (!mDatasets.add(dataset)) { + return this; + } + final int fieldCount = dataset.getFieldIds().size(); + for (int i = 0; i < fieldCount; i++) { + final AutoFillId id = dataset.getFieldIds().get(i); + if (mSavableIds == null) { + mSavableIds = new ArraySet<>(); + } + mSavableIds.add(id); } return this; } @@ -374,27 +319,35 @@ public final class FillResponse implements Parcelable { * * <p>See {@link FillResponse} for examples. */ - public Builder addSavableFields(AutoFillId... ids) { + public @NonNull Builder addSavableFields(@Nullable AutoFillId... ids) { + throwIfDestroyed(); + if (ids == null) { + return this; + } for (AutoFillId id : ids) { + if (mSavableIds == null) { + mSavableIds = new ArraySet<>(); + } mSavableIds.add(id); } return this; } /** - * Sets a {@link Bundle} that will be passed to subsequent calls to {@link - * android.service.autofill.AutoFillService} methods such as {@link + * Sets a {@link Bundle} that will be passed to subsequent APIs that + * manipulate this response. For example, they are passed in as {@link + * android.content.Intent#EXTRA_AUTO_FILL_EXTRAS extras} to your + * authentication flow and to subsequent calls to {@link + * android.service.autofill.AutoFillService#onFillRequest( + * android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal, + * android.service.autofill.FillCallback)} and {@link * android.service.autofill.AutoFillService#onSaveRequest( - * android.app.assist.AssistStructure, Bundle, android.service.autofill.SaveCallback)}, - * using {@link - * android.service.autofill.AutoFillService#EXTRA_RESPONSE_EXTRAS} as the key. - * - * <p>It can be used when to keep service state in between calls. + * android.app.assist.AssistStructure, Bundle, + * android.service.autofill.SaveCallback)}. */ 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"); + throwIfDestroyed(); + mExtras = extras; return this; } @@ -402,8 +355,16 @@ public final class FillResponse implements Parcelable { * Builds a new {@link FillResponse} instance. */ public FillResponse build() { + throwIfDestroyed(); + mDestroyed = true; return new FillResponse(this); } + + private void throwIfDestroyed() { + if (mDestroyed) { + throw new IllegalStateException("Already called #build()"); + } + } } ///////////////////////////////////// @@ -412,14 +373,12 @@ public final class FillResponse implements Parcelable { @Override public String toString() { if (!DEBUG) return super.toString(); - - final StringBuilder builder = new StringBuilder("FillResponse: [datasets=") - .append(mDatasets).append(", savableIds=").append(Arrays.toString(mSavableIds)) - .append(", extras="); - append(builder, mExtras) - .append(", flags=").append(mFlags) - .append(", requiresAuth: ").append(mRequiresAuth) - .append(", hasCrypto: ").append(mHasCryptoObject); + final StringBuilder builder = new StringBuilder( + "FillResponse: [id=").append(mId) + .append(", datasets=").append(mDatasets) + .append(", savableIds=").append(mSavableIds) + .append(", hasExtras=").append(mExtras != null) + .append(", hasAuthentication=").append(mAuthentication != null); return builder.append(']').toString(); } @@ -434,33 +393,34 @@ public final class FillResponse implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - 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) { - mDatasets = new ArrayList<>(); - 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; + parcel.writeString(mId); + parcel.writeTypedArraySet(mDatasets, 0); + parcel.writeTypedArraySet(mSavableIds, 0); + parcel.writeParcelable(mExtras, 0); + parcel.writeParcelable(mAuthentication, 0); } public static final Parcelable.Creator<FillResponse> CREATOR = new Parcelable.Creator<FillResponse>() { @Override - public FillResponse createFromParcel(Parcel source) { - return new FillResponse(source); + public FillResponse createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + final Builder builder = new Builder(parcel.readString()); + final ArraySet<Dataset> datasets = parcel.readTypedArraySet(null); + final int datasetCount = (datasets != null) ? datasets.size() : 0; + for (int i = 0; i < datasetCount; i++) { + builder.addDataset(datasets.valueAt(i)); + } + final ArraySet<AutoFillId> fillIds = parcel.readTypedArraySet(null); + final int fillIdCount = (fillIds != null) ? fillIds.size() : 0; + for (int i = 0; i < fillIdCount; i++) { + builder.addSavableFields(fillIds.valueAt(i)); + } + builder.setExtras(parcel.readParcelable(null)); + builder.setAuthentication(parcel.readParcelable(null)); + return builder.build(); } @Override diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java index 78436f796bf6..178a6973a7b1 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerService.java @@ -34,7 +34,6 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; @@ -43,7 +42,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.autofill.IAutoFillManagerService; import android.text.TextUtils; -import android.text.format.DateUtils; import android.util.LocalLog; import android.util.Log; import android.util.Slog; @@ -72,66 +70,47 @@ import java.util.List; public final class AutoFillManagerService extends SystemService { private static final String TAG = "AutoFillManagerService"; - static final boolean DEBUG = true; // TODO(b/33197203): change to false once stable + static final boolean DEBUG = false; - private static final long SERVICE_BINDING_LIFETIME_MS = 5 * DateUtils.MINUTE_IN_MILLIS; - - private static final int MSG_UNBIND = 1; - private static final int MSG_REQUEST_AUTO_FILL_FOR_USER = 2; - private static final int MSG_REQUEST_AUTO_FILL = 3; + protected static final int MSG_REQUEST_AUTO_FILL_FOR_USER = 1; + protected static final int MSG_REQUEST_AUTO_FILL = 2; + private static final int MSG_REQUEST_SAVE_FOR_USER = 3; private static final int MSG_ON_VALUE_CHANGED = 4; - private static final int MSG_REQUEST_SAVE_FOR_USER = 5; - private final AutoFillManagerServiceStub mServiceStub; private final Context mContext; - private final ContentResolver mResolver; + private final AutoFillUI mUi; private final Object mLock = new Object(); - private final HandlerCaller.Callback mHandlerCallback = new HandlerCaller.Callback() { - - @Override - public void executeMessage(Message msg) { - switch (msg.what) { - case MSG_UNBIND: { - synchronized (mLock) { - removeCachedServiceLocked(msg.arg1); - } - return; - } case MSG_REQUEST_AUTO_FILL_FOR_USER: { - handleAutoFillForUser(msg.arg1); - return; - } case MSG_REQUEST_SAVE_FOR_USER: { - handleSaveForUser(msg.arg1); - return; - } case MSG_REQUEST_AUTO_FILL: { - final SomeArgs args = (SomeArgs) msg.obj; - try { - final int userId = msg.arg1; - final int flags = msg.arg2; - final IBinder activityToken = (IBinder) args.arg1; - final AutoFillId autoFillId = (AutoFillId) args.arg2; - final Rect bounds = (Rect) args.arg3; - handleAutoFill(activityToken, userId, autoFillId, bounds, flags); - } finally { - args.recycle(); - } - return; - } case MSG_ON_VALUE_CHANGED: { - final SomeArgs args = (SomeArgs) msg.obj; - try { - final int userId = msg.arg1; - final IBinder activityToken = (IBinder) args.arg1; - final AutoFillId autoFillId = (AutoFillId) args.arg2; - final AutoFillValue newValue = (AutoFillValue) args.arg3; - handleValueChanged(activityToken, userId, autoFillId, newValue); - } finally { - args.recycle(); - } - return; - } default: { - Slog.w(TAG, "Invalid message: " + msg); - } + private final HandlerCaller.Callback mHandlerCallback = (msg) -> { + switch (msg.what) { + case MSG_REQUEST_AUTO_FILL_FOR_USER: { + handleAutoFillForUser(msg.arg1); + return; + } case MSG_REQUEST_SAVE_FOR_USER: { + handleSaveForUser(msg.arg1); + return; + } case MSG_REQUEST_AUTO_FILL: { + final SomeArgs args = (SomeArgs) msg.obj; + final int userId = msg.arg1; + final int flags = msg.arg2; + final IBinder activityToken = (IBinder) args.arg1; + final AutoFillId autoFillId = (AutoFillId) args.arg2; + final Rect bounds = (Rect) args.arg3; + args.recycle(); + handleAutoFill(activityToken, userId, autoFillId, bounds, flags); + return; + } case MSG_ON_VALUE_CHANGED: { + final SomeArgs args = (SomeArgs) msg.obj; + final int userId = msg.arg1; + final IBinder activityToken = (IBinder) args.arg1; + final AutoFillId autoFillId = (AutoFillId) args.arg2; + final AutoFillValue newValue = (AutoFillValue) args.arg3; + args.recycle(); + handleValueChanged(activityToken, userId, autoFillId, newValue); + return; + } default: { + Slog.w(TAG, "Invalid message: " + msg); } } }; @@ -148,10 +127,10 @@ public final class AutoFillManagerService extends SystemService { * Entries on this cache are added on demand and removed when: * <ol> * <li>An auto-fill service app is removed. - * <li>The {@link android.provider.Settings.Secure#AUTO_FILL_SERVICE} for an user change. - * <li>It has not been interacted with for {@link #SERVICE_BINDING_LIFETIME_MS} ms. + * <li>The {@link android.provider.Settings.Secure#AUTO_FILL_SERVICE} for an user change.\ * </ol> */ + // TODO(b/33197203): Update the above comment @GuardedBy("mLock") private SparseArray<AutoFillManagerServiceImpl> mServicesCache = new SparseArray<>(); @@ -160,19 +139,14 @@ public final class AutoFillManagerService extends SystemService { public AutoFillManagerService(Context context) { super(context); - mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(), mHandlerCallback, true); - mContext = context; - - mResolver = context.getContentResolver(); - mServiceStub = new AutoFillManagerServiceStub(); + mUi = new AutoFillUI(mContext); } @Override public void onStart() { - if (DEBUG) Slog.d(TAG, "onStart(): binding as " + AUTO_FILL_MANAGER_SERVICE); - publishBinderService(AUTO_FILL_MANAGER_SERVICE, mServiceStub); + publishBinderService(AUTO_FILL_MANAGER_SERVICE, new AutoFillManagerServiceStub()); } @Override @@ -186,61 +160,41 @@ public final class AutoFillManagerService extends SystemService { ComponentName serviceComponent = null; ServiceInfo serviceInfo = null; final String componentName = Settings.Secure.getStringForUser( - mResolver, Settings.Secure.AUTO_FILL_SERVICE, userId); + mContext.getContentResolver(), Settings.Secure.AUTO_FILL_SERVICE, userId); if (!TextUtils.isEmpty(componentName)) { try { serviceComponent = ComponentName.unflattenFromString(componentName); serviceInfo = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, 0, userId); } catch (RuntimeException | RemoteException e) { - Slog.wtf(TAG, "Bad auto-fill service name " + componentName, e); + Slog.e(TAG, "Bad auto-fill service name " + componentName, e); return null; } } - if (DEBUG) { - Slog.d(TAG, "getServiceComponentForUser(" + userId + "): component=" - + serviceComponent + ", info: " + serviceInfo); - } if (serviceInfo == null) { - if (DEBUG) Slog.d(TAG, "no service info for " + serviceComponent); return null; } - return new AutoFillManagerServiceImpl(this, mContext, mLock, mRequestsHistory, - userId, serviceInfo.applicationInfo.uid, serviceComponent, - SERVICE_BINDING_LIFETIME_MS); + + try { + return new AutoFillManagerServiceImpl(mContext, mLock, mRequestsHistory, + userId, serviceComponent, mUi); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Auto-fill service not found: " + serviceComponent, e); + } + + return null; } /** * Gets the service instance for an user. - * <p> - * First it tries to return the existing instance from the cache; if it's not cached, it creates - * a new instance and caches it. */ - // TODO(b/33197203): make private once AutoFillUi does not uses notifications - AutoFillManagerServiceImpl getServiceForUserLocked(int userId) { + AutoFillManagerServiceImpl getOrCreateServiceForUserLocked(int userId) { AutoFillManagerServiceImpl service = mServicesCache.get(userId); - if (service != null) { - if (DEBUG) - Log.d(TAG, "reusing cached service for userId " + userId); - service.setLifeExpectancy(SERVICE_BINDING_LIFETIME_MS); - } else { + if (service == null) { service = newServiceForUser(userId); - if (service == null) { - // Already logged - return null; - } - if (DEBUG) Log.d(TAG, "creating new cached service for userId " + userId); - service.startLocked(); mServicesCache.put(userId, service); } - // Keep service connection alive for a while, in case user needs to interact with it - // (for example, to save the data that was inputted in) - if (mHandlerCaller.hasMessages(MSG_UNBIND)) { - mHandlerCaller.removeMessages(MSG_UNBIND); - } - mHandlerCaller.sendMessageDelayed(mHandlerCaller.obtainMessageI(MSG_UNBIND, userId), - SERVICE_BINDING_LIFETIME_MS); return service; } @@ -248,22 +202,17 @@ public final class AutoFillManagerService extends SystemService { * Removes a cached service for a given user. */ void removeCachedServiceLocked(int userId) { - if (DEBUG) Log.d(TAG, "removing cached service for userId " + userId); final AutoFillManagerServiceImpl service = mServicesCache.get(userId); - if (service == null) { - if (DEBUG) { - Log.d(TAG, "removeCachedServiceForUser(): no cached service for userId " + userId); - } - return; + if (service != null) { + mServicesCache.delete(userId); + service.destroyLocked(); } - mServicesCache.delete(userId); - service.stopLocked(); } private void handleAutoFill(IBinder activityToken, int userId, AutoFillId autoFillId, Rect bounds, int flags) { synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId); + final AutoFillManagerServiceImpl service = getOrCreateServiceForUserLocked(userId); if (service != null) { service.requestAutoFillLocked(activityToken, autoFillId, bounds, flags); } @@ -273,7 +222,7 @@ public final class AutoFillManagerService extends SystemService { private void handleValueChanged(IBinder activityToken, int userId, AutoFillId autoFillId, AutoFillValue newValue) { synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId); + final AutoFillManagerServiceImpl service = getOrCreateServiceForUserLocked(userId); if (service != null) { service.onValueChangeLocked(activityToken, autoFillId, newValue); } @@ -283,45 +232,32 @@ public final class AutoFillManagerService extends SystemService { private IBinder getTopActivityForUser() { final List<IBinder> topActivities = LocalServices .getService(ActivityManagerInternal.class).getTopVisibleActivities(); - if (DEBUG) Slog.d(TAG, "Top activities (" + topActivities.size() + "): " + topActivities); if (topActivities.isEmpty()) { - Slog.w(TAG, "Could not get top activity"); return null; } return topActivities.get(0); } private void handleAutoFillForUser(int userId) { - if (DEBUG) Slog.d(TAG, "handler.requestAutoFillForUser(): id=" + userId); final IBinder activityToken = getTopActivityForUser(); - if (activityToken == null) { - return; - } - - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId); - if (service == null) { - Slog.w(TAG, "no service for user " + userId); - return; + if (activityToken != null) { + synchronized (mLock) { + final AutoFillManagerServiceImpl service = + getOrCreateServiceForUserLocked(userId); + service.requestAutoFillLocked(activityToken, null, null, 0); } - service.requestAutoFillLocked(activityToken, null, null, 0); } + } private void handleSaveForUser(int userId) { - if (DEBUG) Slog.d(TAG, "handler.handleSaveForUser(): id=" + userId); final IBinder activityToken = getTopActivityForUser(); - if (activityToken == null) { - return; - } - - synchronized (mLock) { - final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId); - if (service == null) { - Slog.w(TAG, "no service for user " + userId); - return; + if (activityToken != null) { + synchronized (mLock) { + final AutoFillManagerServiceImpl service = + getOrCreateServiceForUserLocked(userId); + service.requestSaveForUserLocked(activityToken); } - service.requestSaveForUserLocked(activityToken); } } @@ -347,9 +283,6 @@ public final class AutoFillManagerService extends SystemService { @Override public void requestAutoFill(AutoFillId id, Rect bounds, int flags) { - if (DEBUG) Slog.d(TAG, "requestAutoFill: flags=" + flags + ", autoFillId=" + id - + ", bounds=" + bounds); - final IBinder activityToken = getTopActivity(); if (activityToken != null) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIIOOO(MSG_REQUEST_AUTO_FILL, @@ -360,25 +293,19 @@ public final class AutoFillManagerService extends SystemService { @Override public void requestAutoFillForUser(int userId) { mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG); - - mHandlerCaller.sendMessage( - mHandlerCaller.obtainMessageI(MSG_REQUEST_AUTO_FILL_FOR_USER, userId)); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageI( + MSG_REQUEST_AUTO_FILL_FOR_USER, userId)); } @Override public void requestSaveForUser(int userId) { mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG); - - mHandlerCaller.sendMessage( - mHandlerCaller.obtainMessageI(MSG_REQUEST_SAVE_FOR_USER, userId)); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageI(MSG_REQUEST_SAVE_FOR_USER, userId)); } @Override public void onValueChanged(AutoFillId id, AutoFillValue value) { - if (DEBUG) Slog.d(TAG, "onValueChanged(): id=" + id + ", value=" + value); - final IBinder activityToken = getTopActivity(); - if (activityToken != null) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO(MSG_ON_VALUE_CHANGED, UserHandle.getCallingUserId(), activityToken, id, value)); @@ -407,6 +334,7 @@ public final class AutoFillManagerService extends SystemService { impl.dumpLocked(" ", pw); } } + mUi.dump(pw); } pw.println("Requests history:"); mRequestsHistory.reverseDump(fd, pw, args); @@ -430,7 +358,6 @@ public final class AutoFillManagerService extends SystemService { @Override public void onChange(boolean selfChange, Uri uri, int userId) { - if (DEBUG) Slog.d(TAG, "settings (" + uri + " changed for " + userId); synchronized (mLock) { removeCachedServiceLocked(userId); } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java index 42e4fd3b2b7f..e32e21d54b56 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java @@ -16,15 +16,11 @@ package com.android.server.autofill; -import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_ERROR; -import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_REQUESTED; -import static android.service.autofill.AutoFillService.FLAG_AUTHENTICATION_SUCCESS; import static android.view.autofill.AutoFillManager.FLAG_UPDATE_UI_SHOW; import static android.view.autofill.AutoFillManager.FLAG_UPDATE_UI_HIDE; import static com.android.server.autofill.Helper.DEBUG; import static com.android.server.autofill.Helper.VERBOSE; -import static com.android.server.autofill.Helper.bundleToString; import android.annotation.Nullable; import android.app.Activity; @@ -38,33 +34,26 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; +import android.content.IntentSender; import android.content.pm.PackageManager; import android.graphics.Rect; -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.IBinder; +import android.os.ICancellationSignal; import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; import android.service.autofill.AutoFillService; import android.service.autofill.AutoFillServiceInfo; +import android.service.autofill.FillCallback; import android.service.autofill.IAutoFillAppCallback; -import android.service.autofill.IAutoFillServerCallback; import android.service.autofill.IAutoFillService; +import android.service.autofill.IFillCallback; import android.service.voice.VoiceInteractionSession; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.LocalLog; -import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.SparseArray; -import android.util.TimeUtils; import android.view.autofill.AutoFillId; import android.view.autofill.AutoFillValue; import android.view.autofill.Dataset; @@ -77,8 +66,6 @@ import com.android.server.FgThread; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -95,24 +82,13 @@ final class AutoFillManagerServiceImpl { private static int sSessionIdCounter = 0; private final int mUserId; - private final int mUid; private final ComponentName mComponent; private final String mComponentName; private final Context mContext; private final IActivityManager mAm; private final Object mLock; private final AutoFillServiceInfo mInfo; - private final AutoFillManagerService mManagerService; - - // Token used for fingerprint authentication - // TODO(b/33197203): create on demand? - private final IBinder mAuthToken = new Binder(); - - private final IFingerprintService mFingerprintService = - IFingerprintService.Stub.asInterface(ServiceManager.getService("fingerprint")); - - @GuardedBy("mLock") - private final List<QueuedRequest> mQueuedRequests = new LinkedList<>(); + private final AutoFillUI mUi; private final LocalLog mRequestsHistory; @@ -122,15 +98,7 @@ final class AutoFillManagerServiceImpl { if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { final String reason = intent.getStringExtra("reason"); if (DEBUG) Slog.d(TAG, "close system dialogs: " + reason); - - synchronized (mLock) { - final int size = mSessions.size(); - for (int i = 0; i < size; i++) { - final Session session = mSessions.valueAt(i); - // TODO(b/33197203): invalidate the sessions instead? - session.mUi.closeAll(); - } - } + mUi.hideAll(); } } }; @@ -144,40 +112,7 @@ final class AutoFillManagerServiceImpl { // TODO(b/33197203): need to make sure service is bound while callback is pending and/or // use WeakReference @GuardedBy("mLock") - private static final SparseArray<Session> mSessions = new SparseArray<>(); - - private final ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - if (DEBUG) Slog.d(TAG, "onServiceConnected():" + name); - synchronized (mLock) { - mService = IAutoFillService.Stub.asInterface(service); - try { - mService.onConnected(); - } catch (RemoteException e) { - Slog.w(TAG, "Exception on service.onConnected(): " + e); - return; - } - if (!mQueuedRequests.isEmpty()) { - if (DEBUG) Slog.d(TAG, "queued requests:" + mQueuedRequests.size()); - } - for (final QueuedRequest request: mQueuedRequests) { - requestAutoFillLocked(request.activityToken, request.autoFillId, - request.bounds, request.flags, false); - } - mQueuedRequests.clear(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - if (DEBUG) Slog.d(TAG, name + " disconnected"); - synchronized (mLock) { - mService = null; - mManagerService.removeCachedServiceLocked(mUserId); - } - } - }; + private final SparseArray<Session> mSessions = new SparseArray<>(); /** * Receiver of assist data from the app's {@link Activity}, uses the {@code resultData} as @@ -193,8 +128,13 @@ final class AutoFillManagerServiceImpl { Slog.w(TAG, "no app callback on mAssistReceiver's resultData"); return; } + final AssistStructure structure = resultData .getParcelable(VoiceInteractionSession.KEY_STRUCTURE); + if (structure == null) { + Slog.w(TAG, "no assist structure for id " + resultCode); + return; + } final Session session; synchronized (mLock) { @@ -203,91 +143,55 @@ final class AutoFillManagerServiceImpl { Slog.w(TAG, "no server callback for id " + resultCode); return; } - session.setAppCallbackLocked(appBinder); - // TODO(b/33197203): since service is fetching the data (to use for save later), - // we should optimize what's sent (for example, remove layout containers, - // color / font info, etc...) - session.mStructure = structure; - - // TODO(b/33197203, b/33269702): Must fetch the data so it's available later on - // handleSave(), even if if the activity is gone by then, but structure.ensureData() - // gives a ONE_WAY warning because system_service could block on app calls. - // We need to change AssistStructure so it provides a "one-way" writeToParcel() - // method that sends all the data - structure.ensureData(); - - structure.sanitizeForParceling(true); - if (VERBOSE) { - Slog.v(TAG, "Dumping " + structure + " before calling service.autoFill()"); - structure.dump(); - } - mService.autoFill(structure, session.mServerCallback); } - } - }; - @GuardedBy("mLock") - private IAutoFillService mService; - @GuardedBy("mLock") - private boolean mBound; - @GuardedBy("mLock") - private boolean mValid; + // TODO(b/33197203): since service is fetching the data (to use for save later), + // we should optimize what's sent (for example, remove layout containers, + // color / font info, etc...) - // Estimated time when the service will be evicted from the cache. - long mEstimateTimeOfDeath; + // TODO(b/33197203, b/33269702): Must fetch the data so it's available later on + // handleSave(), even if if the activity is gone by then, but structure.ensureData() + // gives a ONE_WAY warning because system_service could block on app calls. + // We need to change AssistStructure so it provides a "one-way" writeToParcel() + // method that sends all the data + structure.ensureData(); - AutoFillManagerServiceImpl(AutoFillManagerService managerService, Context context, Object lock, - LocalLog requestsHistory, int userId, int uid, ComponentName component, long ttl) { - mManagerService = managerService; + structure.sanitizeForParceling(true); + + if (VERBOSE) { + Slog.v(TAG, "Dumping " + structure + " before calling service.autoFill()"); + structure.dump(); + } + + session.onApplicationDataAvailable(structure, appBinder); + } + }; + + AutoFillManagerServiceImpl(Context context, Object lock, LocalLog requestsHistory, + int userId, ComponentName component, AutoFillUI ui) + throws PackageManager.NameNotFoundException { mContext = context; mLock = lock; mRequestsHistory = requestsHistory; mUserId = userId; - mUid = uid; mComponent = component; mComponentName = mComponent.flattenToShortString(); mAm = ActivityManager.getService(); - setLifeExpectancy(ttl); - - final AutoFillServiceInfo info; - try { - info = new AutoFillServiceInfo(context.getPackageManager(), component, mUserId); - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Auto-fill service not found: " + component, e); - mInfo = null; - mValid = false; - return; - } - mInfo = info; - if (mInfo.getParseError() != null) { - Slog.w(TAG, "Bad auto-fill service: " + mInfo.getParseError()); - mValid = false; - return; - } + mUi = ui; + mInfo = new AutoFillServiceInfo(context.getPackageManager(), component, mUserId); - mValid = true; IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); mContext.registerReceiver(mBroadcastReceiver, filter, null, FgThread.getHandler()); } - void setLifeExpectancy(long ttl) { - mEstimateTimeOfDeath = SystemClock.uptimeMillis() + ttl; - } - - void startLocked() { - if (DEBUG) Slog.d(TAG, "startLocked()"); - final Intent intent = new Intent(AutoFillService.SERVICE_INTERFACE); - intent.setComponent(mComponent); - mBound = mContext.bindServiceAsUser(intent, mConnection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, new UserHandle(mUserId)); - - if (!mBound) { - Slog.w(TAG, "Failed binding to auto-fill service " + mComponent); - return; - } - if (DEBUG) Slog.d(TAG, "Bound to " + mComponent); + /** + * Used by {@link AutoFillManagerServiceShellCommand} to request save for the current top app. + */ + void requestSaveForUserLocked(IBinder activityToken) { + final Session session = getOrCreateSessionByTokenLocked(activityToken); + session.onSaveLocked(); } /** @@ -299,66 +203,17 @@ final class AutoFillManagerServiceImpl { * @param flags optional flags. */ void requestAutoFillLocked(IBinder activityToken, @Nullable AutoFillId autoFillId, - @Nullable Rect bounds, int flags) { - if (!mBound) { - Slog.w(TAG, "requestAutoFillLocked() failed because it's not bound to service"); - return; - } - - requestAutoFillLocked(activityToken, autoFillId, bounds, flags, true); - } - - /** - * Used by {@link AutoFillManagerServiceShellCommand} to request save for the current top app. - */ - void requestSaveForUserLocked(IBinder activityToken) { - if (!mBound) { - Slog.w(TAG, "requestSaveForUserLocked() failed because it's not bound to service"); - return; - } - if (mService == null) { - Slog.w(TAG, "requestSaveForUserLocked: service not set"); - return; - } - - final Session session = getSessionByTokenLocked(activityToken); - if (session == null) { - Slog.w(TAG, "requestSaveForUserLocked(): no session for " + activityToken); - return; - } - - session.onSaveLocked(); - } - - private void requestAutoFillLocked(IBinder activityToken, @Nullable AutoFillId autoFillId, - @Nullable Rect bounds, int flags, boolean queueIfNecessary) { - if (mService == null) { - if (!queueIfNecessary) { - Slog.w(TAG, "requestAutoFillLocked(): service is null"); - return; - } - if (DEBUG) Slog.d(TAG, "requestAutoFillLocked(): service not set yet, queuing it"); - mQueuedRequests.add(new QueuedRequest(activityToken, autoFillId, bounds, flags)); - return; - } - + @Nullable Rect bounds, int flags) { final String historyItem = "s=" + mComponentName + " u=" + mUserId + " f=" + flags + " a=" + activityToken + " i=" + autoFillId + " b=" + bounds; mRequestsHistory.log(historyItem); // TODO(b/33197203): Handle partitioning - Session session = getSessionByTokenLocked(activityToken); - - if (session == null) { - session = createSessionByTokenLocked(activityToken); - } else { - if (DEBUG) Slog.d(TAG, "reusing session for " + activityToken + ": " + session.mId); - } - + Session session = getOrCreateSessionByTokenLocked(activityToken); session.updateAutoFillInput(flags, autoFillId, null, bounds); } - private Session getSessionByTokenLocked(IBinder activityToken) { + private Session getOrCreateSessionByTokenLocked(IBinder activityToken) { final int size = mSessions.size(); for (int i = 0; i < size; i++) { final Session session = mSessions.valueAt(i); @@ -366,14 +221,14 @@ final class AutoFillManagerServiceImpl { return session; } } - return null; + return createSessionByTokenLocked(activityToken); } private Session createSessionByTokenLocked(IBinder activityToken) { final int sessionId = ++sSessionIdCounter; if (DEBUG) Slog.d(TAG, "creating session for " + activityToken + ": " + sessionId); - final Session newSession = new Session(sessionId, activityToken); + final Session newSession = new Session(mContext, activityToken, sessionId); mSessions.put(sessionId, newSession); /* @@ -401,88 +256,33 @@ final class AutoFillManagerServiceImpl { */ void onValueChangeLocked(IBinder activityToken, AutoFillId autoFillId, AutoFillValue newValue) { // TODO(b/33197203): add MetricsLogger call - final Session session = getSessionByTokenLocked(activityToken); - if (session == null) { - Slog.w(TAG, "onValueChangeLocked(): session gone for " + activityToken); - return; - } - + final Session session = getOrCreateSessionByTokenLocked(activityToken); session.updateValueLocked(autoFillId, newValue); } - void stopLocked() { - if (DEBUG) Slog.d(TAG, "stopLocked()"); - - // Sanity check. - if (mService == null) { - Slog.w(TAG, "service already null on shutdown"); - return; - } - try { - mService.onDisconnected(); - } catch (RemoteException e) { - if (! (e instanceof DeadObjectException)) { - Slog.w(TAG, "Exception calling service.onDisconnected(): " + e); - } - } finally { - mService = null; - } - - if (mBound) { - mContext.unbindService(mConnection); - mBound = false; - } - if (mValid) { - mContext.unregisterReceiver(mBroadcastReceiver); - } - } - 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 - // and cached AssistStructures + mSessions.get(id); } - void dumpLocked(String prefix, PrintWriter pw) { - if (!mValid) { - pw.print(" NOT VALID: "); - if (mInfo == null) { - pw.println("no info"); - } else { - pw.println(mInfo.getParseError()); - } - return; + void destroyLocked() { + mContext.unregisterReceiver(mBroadcastReceiver); + final int sessionCount = mSessions.size(); + for (int i = sessionCount - 1; i >= 0; i--) { + Session session = mSessions.valueAt(i); + session.destroy(); + mSessions.removeAt(i); } + } + void dumpLocked(String prefix, PrintWriter pw) { final String prefix2 = prefix + " "; - - 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(mComponentName); - pw.print(prefix); pw.print("mService: "); pw.println(mService); - pw.print(prefix); pw.print("mBound="); pw.println(mBound); - pw.print(prefix); pw.print("mEstimateTimeOfDeath="); - TimeUtils.formatDuration(mEstimateTimeOfDeath, SystemClock.uptimeMillis(), pw); - pw.println(); - pw.print(prefix); pw.print("mAuthToken: "); pw.println(mAuthToken); - if (DEBUG) { // ServiceInfo dump is too noisy and redundant (it can be obtained through other dumps) pw.print(prefix); pw.println("ServiceInfo:"); mInfo.getServiceInfo().dump(new PrintWriterPrinter(pw), prefix + prefix); } - if (mQueuedRequests.isEmpty()) { - pw.print(prefix); pw.println("No queued requests"); - } else { - pw.print(prefix); pw.println("Queued requests:"); - for (int i = 0; i < mQueuedRequests.size(); i++) { - pw.print(prefix2); pw.print(i); pw.print(": "); pw.println(mQueuedRequests.get(i)); - } - } - pw.print(prefix); pw.print("sSessionIdCounter="); pw.println(sSessionIdCounter); final int size = mSessions.size(); if (size == 0) { @@ -498,32 +298,10 @@ final class AutoFillManagerServiceImpl { @Override public String toString() { - return "AutoFillManagerServiceImpl: [userId=" + mUserId + ", uid=" + mUid + return "AutoFillManagerServiceImpl: [userId=" + mUserId + ", component=" + mComponentName + "]"; } - private static final class QueuedRequest { - final IBinder activityToken; - final AutoFillId autoFillId; - final Rect bounds; - final int flags; - - QueuedRequest(IBinder activityToken, AutoFillId autoFillId, Rect bounds, int flags) { - this.activityToken = activityToken; - this.autoFillId = autoFillId; - this.bounds = bounds; - this.flags = flags; - } - - @Override - public String toString() { - if (!DEBUG) return super.toString(); - - return "QueuedRequest: [flags=" + flags + ", token=" + activityToken - + ", id=" + autoFillId + ", bounds=" + bounds; - } - } - /** * State for a given view with a AutoFillId. * @@ -553,12 +331,7 @@ final class AutoFillManagerServiceImpl { * Response should only be set once. */ void setResponse(FillResponse response) { - if (mResponse != null) { - Slog.e(TAG, "ViewState response set more than once"); - return; - } mResponse = response; - maybeCallOnFillReady(); } @@ -608,39 +381,27 @@ final class AutoFillManagerServiceImpl { // - On all authentication scenarios. // - When user does not interact back after a while. // - When service is unbound. - final class Session implements ViewState.Listener { + final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, + AutoFillUI.AutoFillUiCallback { + private final int mId; - private final AutoFillUI mUi; private final WeakReference<IBinder> mActivityToken; @GuardedBy("mLock") private final Map<AutoFillId, ViewState> mViewStates = new ArrayMap<>(); + @GuardedBy("mLock") @Nullable private ViewState mCurrentViewState; private IAutoFillAppCallback mAppCallback; - // TODO(b/33197203): Get a response per view instead of per activity. - @GuardedBy("mLock") - private FillResponse mCurrentResponse; - @GuardedBy("mLock") - private FillResponse mResponseRequiringAuth; - @GuardedBy("mLock") - 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; + RemoteFillService mRemoteFillService; - /** - * Used to remember which {@link Dataset} filled the session. - */ + // TODO(b/33197203): Get a response per view instead of per activity. @GuardedBy("mLock") - private Dataset mAutoFilledDataset; + private FillResponse mCurrentResponse; /** * Map of ids that must be updated so they're send to {@link #onSaveLocked()}. @@ -652,168 +413,77 @@ final class AutoFillManagerServiceImpl { * Assist structure sent by the app; it will be updated (sanitized, change values for save) * before sent to {@link AutoFillService}. */ + @GuardedBy("mLock") private AssistStructure mStructure; - // 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 { - autoFillApp(mDatasetRequiringAuth); - } - } else if (mResponseRequiringAuth != null) { - final List<Dataset> datasets = mResponseRequiringAuth.getDatasets(); - if (datasets.isEmpty()) { - notifyResponseAuthenticationResult(mResponseRequiringAuth.getExtras(), - FLAG_AUTHENTICATION_SUCCESS); - } else { - showResponseLocked(mResponseRequiringAuth, true); - } - } else { - Slog.w(TAG, "onAuthenticationSucceeded(): no response or dataset"); - } - - mUi.dismissFingerprintRequest(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(false); - } - - @Override - public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) { - if (DEBUG) Slog.d(TAG, "onRemoved()"); - } + private Session(Context context, IBinder activityToken, int id) { + mActivityToken = new WeakReference<>(activityToken); + mRemoteFillService = new RemoteFillService(context, mComponent, mUserId, this); + mId = id; + } - @Override - public void onEnumerated(long deviceId, int fingerId, int groupId, int remaining) { - if (DEBUG) Slog.d(TAG, "onEnumerated()"); + // FillServiceCallbacks + @Override + public void onFillRequestSuccess(FillResponse response) { + // TODO(b/33197203): add MetricsLogger call + if (response == null) { + destroy(); + return; } - }; - - 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()); - } + synchronized (mLock) { + processResponseLocked(response); } + } - @Override - public void showError(CharSequence message) { - // TODO(b/33197203): add MetricsLogger call - if (DEBUG) Slog.d(TAG, "showError(): " + message); + // FillServiceCallbacks + @Override + public void onFillRequestFailure(CharSequence message) { + // TODO(b/33197203): add MetricsLogger call + getUiForShowing().showError(message); + destroy(); + } - mUi.showError(message); - removeSelf(); - } + // FillServiceCallbacks + @Override + public void onSaveRequestSuccess() { + // TODO: Implement + } - @Override - public void onSaved() { - // TODO(b/33197203): add MetricsLogger call - if (DEBUG) Slog.d(TAG, "onSaved()"); + // FillServiceCallbacks + @Override + public void onSaveRequestFailure(CharSequence message) { + // TODO(b/33197203): add MetricsLogger call + getUiForShowing().showError(message); + destroy(); + } - removeSelf(); - } + // FillServiceCallbacks + @Override + public void authenticate(IntentSender intent, Intent fillInIntent) { + startAuthIntent(intent, fillInIntent); + } - @Override - public void unlockFillResponse(int flags) { - // TODO(b/33197203): add proper MetricsLogger calls? - if (DEBUG) Log.d(TAG, "unlockUser(): flags=" + flags); - - synchronized (mLock) { - if ((flags & FLAG_AUTHENTICATION_SUCCESS) != 0) { - if (mResponseRequiringAuth == null) { - Log.wtf(TAG, "unlockUser(): no mResponseRequiringAuth on flags " - + flags); - removeSelf(); - return; - } - final List<Dataset> datasets = mResponseRequiringAuth.getDatasets(); - if (datasets.isEmpty()) { - Log.w(TAG, "unlockUser(): no dataset on previous response: " - + mResponseRequiringAuth); - removeSelf(); - return; - } - mAutoFillDirectly = true; - showResponseLocked(mResponseRequiringAuth, false); - } - // TODO(b/33197203): show UI error on authentication failure? - // Or let service handle it? - } - } + // FillServiceCallbacks + @Override + public void onServiceDied(RemoteFillService service) { + // TODO: Implement + } - @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); + // AutoFillUiCallback + @Override + public void fill(Dataset dataset) { + autoFill(dataset); + } - if ((flags & FLAG_AUTHENTICATION_SUCCESS) != 0) { - autoFillApp(dataset != null ? dataset : mDatasetRequiringAuth); - return; - } + // AutoFillUiCallback + @Override + public void save() { + synchronized (mLock) { + onSaveLocked(); } - }; - - final int mId; + } private Session(int id, IBinder activityToken) { - mUi = new AutoFillUI(mContext, this); mId = id; mActivityToken = new WeakReference<>(activityToken); } @@ -826,7 +496,7 @@ final class AutoFillManagerServiceImpl { // TODO(b/33197203): ignore if not part of the savable ids. if (mUpdatedValues == null) { - // Lazy intializes it + // Lazy initializes it mUpdatedValues = new HashMap<>(); } mUpdatedValues.put(id, newValue); @@ -847,32 +517,10 @@ final class AutoFillManagerServiceImpl { if (mUpdatedValues == null || mUpdatedValues.isEmpty()) { // Nothing changed if (DEBUG) Slog.d(TAG, "onSave(): when no changes, comes no responsibilities"); - return; } // TODO(b/33197203): make sure the extras are tested by CTS - final Bundle responseExtras = mCurrentResponse == null ? null - : mCurrentResponse.getExtras(); - final Bundle datasetExtras = mAutoFilledDataset == null ? null - : mAutoFilledDataset.getExtras(); - final Bundle extras = (responseExtras == null && datasetExtras == null) - ? null : new Bundle(); - if (responseExtras != null) { - if (DEBUG) { - Slog.d(TAG, "response extras on save extras: " - + bundleToString(responseExtras)); - } - extras.putBundle(AutoFillService.EXTRA_RESPONSE_EXTRAS, responseExtras); - } - if (datasetExtras != null) { - if (DEBUG) { - Slog.d(TAG, "dataset extras on save extras: " + bundleToString(datasetExtras)); - } - extras.putBundle(AutoFillService.EXTRA_DATASET_EXTRAS, datasetExtras); - } - - for (Entry<AutoFillId, AutoFillValue> entry : mUpdatedValues.entrySet()) { final AutoFillId id = entry.getKey(); final ViewNode node = findViewNodeByIdLocked(id); @@ -891,20 +539,24 @@ final class AutoFillManagerServiceImpl { Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()"); mStructure.dump(); } - try { - mService.save(mStructure, mServerCallback, extras); - } catch (RemoteException e) { - Slog.w(TAG, "Error calling save on service: " + e); - // TODO(b/33197203): invalidate session? - } + + mRemoteFillService.onSaveRequest(mStructure, mCurrentResponse.getExtras()); + } + + void onApplicationDataAvailable(AssistStructure structure, IBinder appCallback) { + setAppCallback(appCallback); + mStructure = structure; + // TODO(b/33197203): Need to pipe the bundle + mRemoteFillService.onFillRequest(structure, null); } - void setAppCallbackLocked(IBinder appBinder) { + private void setAppCallback(IBinder appBinder) { try { appBinder.linkToDeath(() -> { if (DEBUG) Slog.d(TAG, "app callback died"); // TODO(b/33197203): more cleanup here? mAppCallback = null; + destroy(); }, 0); } catch (RemoteException e) { Slog.w(TAG, "linkToDeath() failed: " + e); @@ -957,78 +609,105 @@ final class AutoFillManagerServiceImpl { filterText = text.toString(); } } - mUi.showFillUi(viewState, response.getDatasets(), bounds, filterText); + getUiForShowing().showFillUi(viewState, response.getDatasets(), bounds, filterText); } - private void showResponseLocked(FillResponse response, boolean authRequired) { - if (DEBUG) Slog.d(TAG, "showResponse(directly=" + mAutoFillDirectly - + ", authRequired=" + authRequired +"):" + response); + private void processResponseLocked(FillResponse response) { + if (DEBUG) Slog.d(TAG, "showResponse(authRequired=" + + response.getAuthentication() +"):" + response); - if (mAutoFillDirectly && response != null) { - final List<Dataset> datasets = response.getDatasets(); - if (datasets.size() == 1) { - // User authenticated and provider returned just 1 dataset - auto-fill it now! - final Dataset dataset = datasets.get(0); - if (DEBUG) Slog.d(TAG, "auto-filling directly from auth: " + dataset); + // TODO(b/33197203): add MetricsLogger calls - autoFillApp(dataset); - return; - } - } + mCurrentResponse = response; - if (!authRequired) { - // TODO(b/33197203): add MetricsLogger call - mCurrentResponse = response; + if (mCurrentResponse.getAuthentication() != null) { + // ...or handle authentication. + Intent fillInIntent = createAuthFillInIntent(response.getId(), mStructure, + new Bundle(), new FillCallback(new IFillCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + // TODO(b/33197203): Handle cancellation + } + + @Override + public void onSuccess(FillResponse response) { + mCurrentResponse = createAuthenticatedResponse( + mCurrentResponse, response); + processResponseLocked(mCurrentResponse); + } + + @Override + public void onFailure(CharSequence message) { + getUiForShowing().showError(message); + destroy(); + } + })); + + getUiForShowing().showFillResponseAuthRequest( + mCurrentResponse.getAuthentication(), fillInIntent); + } else { // TODO(b/33197203): Consider using mCurrentResponse, depends on partitioning design if (mCurrentViewState != null) { mCurrentViewState.setResponse(mCurrentResponse); } - 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(requiresFingerprint, - response.getExtras(), response.getFlags()); } void autoFill(Dataset dataset) { synchronized (mLock) { // Autofill it directly... - if (!dataset.isAuthRequired()) { + if (dataset.getAuthentication() == null) { autoFillApp(dataset); + // For now just show this on every fill + getUiForShowing().showSaveUi(); return; } // ...or handle authentication. + Intent fillInIntent = createAuthFillInIntent(dataset.getId(), mStructure, + new Bundle(), new FillCallback(new IFillCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + // TODO(b/33197203): Handle cancellation + } - 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 onSuccess(FillResponse response) { + mCurrentResponse = createAuthenticatedResponse( + mCurrentResponse, response); + Dataset augmentedDataset = Helper.findDatasetById(dataset.getId(), + mCurrentResponse); + if (augmentedDataset != null) { + autoFill(augmentedDataset); + } } - } + + @Override + public void onFailure(CharSequence message) { + getUiForShowing().showError(message); + destroy(); + } + })); + + startAuthIntent(dataset.getAuthentication(), fillInIntent); + } + } + + private Intent createAuthFillInIntent(String itemId, AssistStructure structure, + Bundle extras, FillCallback fillCallback) { + Intent fillInIntent = new Intent(); + fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_ITEM_ID, itemId); + fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_ASSIST_STRUCTURE, structure); + fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_EXTRAS, extras); + fillInIntent.putExtra(Intent.EXTRA_AUTO_FILL_CALLBACK, fillCallback); + return fillInIntent; + } + + private void startAuthIntent(IntentSender intent, Intent fillInIntent) { + try { + mAppCallback.startIntentSender(intent, fillInIntent); + } catch (RemoteException e) { + Slog.e(TAG, "Error launching auth intent", e); } } @@ -1036,11 +715,6 @@ final class AutoFillManagerServiceImpl { pw.print(prefix); pw.print("mId: "); pw.println(mId); pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken.get()); pw.print(prefix); pw.print("mCurrentResponse: "); pw.println(mCurrentResponse); - pw.print(prefix); - pw.print("mResponseRequiringAuth: "); pw.println(mResponseRequiringAuth); - pw.print(prefix); - pw.print("mDatasetRequiringAuth: "); pw.println(mDatasetRequiringAuth); - pw.print(prefix); pw.print("mAutoFillDirectly: "); pw.println(mAutoFillDirectly); pw.print(prefix); pw.print("mCurrentViewStates: "); pw.println(mCurrentViewState); pw.print(prefix); pw.print("mViewStates: "); pw.println(mViewStates.size()); final String prefix2 = prefix + " "; @@ -1057,42 +731,8 @@ final class AutoFillManagerServiceImpl { } else { pw.println("null"); } - } - /** - * Notifies the result of a {@link FillResponse} authentication request to the service. - * - * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after user - * used the fingerprint sensors to authenticate. - */ - void notifyResponseAuthenticationResult(Bundle extras, int flags) { - if (DEBUG) Slog.d(TAG, "notifyResponseAuthenticationResult(): flags=" + flags - + ", extras=" + bundleToString(extras)); - synchronized (mLock) { - try { - mService.authenticateFillResponse(extras, flags); - } catch (RemoteException e) { - Slog.w(TAG, "Error sending authentication result back to service: " + e); - } - } - } - - /** - * Notifies the result of a {@link Dataset} authentication request to the service. - * - * <p>Typically called by the UI after user taps the "Tap to autofill" affordance, or after - * it gets the results from a fingerprint authentication. - */ - void notifyDatasetAuthenticationResult(Bundle extras, int flags) { - if (DEBUG) Slog.d(TAG, "notifyDatasetAuthenticationResult(): flags=" + flags - + ", extras=" + bundleToString(extras)); - synchronized (mLock) { - try { - mService.authenticateDataset(extras, flags); - } catch (RemoteException e) { - Slog.w(TAG, "Error sending authentication result back to service: " + e); - } - } + mRemoteFillService.dump(prefix, pw); } void autoFillApp(Dataset dataset) { @@ -1106,32 +746,9 @@ final class AutoFillManagerServiceImpl { } } - /** - * Called by UI to trigger a save request to the service. - */ - void requestSave() { - synchronized (mLock) { - onSaveLocked(); - } - } - - private void scanFingerprint(long opId) { - // TODO(b/33197203): add MetricsLogger call - if (DEBUG) Slog.d(TAG, "Starting fingerprint scan for op id: " + opId); - - // 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(mAuthToken, opId, mUserId, mServiceReceiver, 0, - null); - } catch (RemoteException e) { - // Local call, shouldn't happen. - } finally { - Binder.restoreCallingIdentity(token); - } + private AutoFillUI getUiForShowing() { + mUi.setCallback(this, mId); + return mUi; } private ViewNode findViewNodeByIdLocked(AutoFillId id) { @@ -1167,10 +784,89 @@ final class AutoFillManagerServiceImpl { return null; } - private void removeSelf() { + private void destroy() { synchronized (mLock) { + mRemoteFillService.destroy(); + mUi.hideAll(); + mUi.setCallback(null, 0); removeSessionLocked(mId); } } + + /** + * Creates a response from the {@code original} and an {@code update} by + * replacing all items that needed authentication (response or datasets) + * with their updated version if the latter does not need authentication. + * New datasets that don't require auth are appended. + * + * @param original The original response requiring auth at some level. + * @param update An updated response with auth not needed anymore at some level. + * @return A new response with updated items where auth is not needed anymore. + */ + // TODO(b/33197203) Unit test + FillResponse createAuthenticatedResponse(FillResponse original, FillResponse update) { + // Can update only if ids match + if (!original.getId().equals(update.getId())) { + return original; + } + + // If the original required auth and the update doesn't, the update wins + // but only if none of the update's datasets requires authentication. + if (original.getAuthentication() != null && update.getAuthentication() == null) { + ArraySet<Dataset> updateDatasets = update.getDatasets(); + final int udpateDatasetCount = updateDatasets.size(); + for (int i = 0; i < udpateDatasetCount; i++) { + Dataset updateDataset = updateDatasets.valueAt(i); + if (updateDataset.getAuthentication() != null) { + return original; + } + } + return update; + } + + // If no auth on response level we create a response that has all + // datasets from the original with the ones that required auth but + // not anymore updated and new ones not requiring auth appended. + + // The update shouldn't require auth + if (update.getAuthentication() != null) { + return original; + } + + final FillResponse.Builder builder = new FillResponse.Builder(original.getId()); + + // Update existing datasets + final ArraySet<Dataset> origDatasets = original.getDatasets(); + final int origDatasetCount = origDatasets.size(); + for (int i = 0; i < origDatasetCount; i++) { + Dataset origDataset = origDatasets.valueAt(i); + ArraySet<Dataset> updateDatasets = update.getDatasets(); + final int updateDatasetCount = updateDatasets.size(); + for (int j = 0; j < updateDatasetCount; j++) { + Dataset updateDataset = updateDatasets.valueAt(j); + if (origDataset.getId().equals(updateDataset.getId())) { + // The update shouldn't require auth + if (updateDataset.getAuthentication() == null) { + origDataset = updateDataset; + updateDatasets.removeAt(j); + } + break; + } + } + builder.addDataset(origDataset); + } + + // Add new datasets + final ArraySet<Dataset> updateDatasets = update.getDatasets(); + final int updateDatasetCount = updateDatasets.size(); + for (int i = 0; i < updateDatasetCount; i++) { + final Dataset updateDataset = updateDatasets.valueAt(i); + builder.addDataset(updateDataset); + } + + // For now no extras and savable id updates. + + return builder.build(); + } } } diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java index 76c291687ec3..da54d853135b 100644 --- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java @@ -18,10 +18,7 @@ package com.android.server.autofill; import static com.android.server.autofill.Helper.DEBUG; -import android.annotation.Nullable; -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; @@ -29,12 +26,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.graphics.Rect; -import android.graphics.PixelFormat; import android.os.Binder; -import android.os.Bundle; +import android.util.ArraySet; import android.util.Slog; -import android.view.autofill.AutoFillId; import android.view.autofill.Dataset; import android.view.autofill.FillResponse; import android.view.Gravity; @@ -44,27 +40,28 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.widget.Toast; -import com.android.internal.annotations.GuardedBy; import com.android.server.UiThread; -import com.android.server.autofill.AutoFillManagerServiceImpl.Session; import com.android.server.autofill.AutoFillManagerServiceImpl.ViewState; import java.io.PrintWriter; -import java.util.Arrays; -import java.util.List; /** * Handles all auto-fill related UI tasks. */ // TODO(b/33197203): document exactly what once the auto-fill bar is implemented final class AutoFillUI { - private static final String TAG = "AutoFillUI"; + private static final String EXTRA_AUTH_INTENT_SENDER = + "com.android.server.autofill.extra.AUTH_INTENT_SENDER"; + private static final String EXTRA_AUTH_FILL_IN_INTENT = + "com.android.server.autofill.extra.AUTH_FILL_IN_INTENT"; + private final Context mContext; - private final Session mSession; private final WindowManager mWm; + // TODO(b/33197203) Fix locking - some state requires lock and some not - requires refactoring + // Fill UI variables private AnchoredWindow mFillWindow; private DatasetPicker mFillView; @@ -72,21 +69,39 @@ final class AutoFillUI { private Rect mBounds; private String mFilterText; + private AutoFillUiCallback mCallback; + private int mClientId; + + public interface AutoFillUiCallback { + void authenticate(IntentSender intent, Intent fillInIntent); + void fill(Dataset dataset); + void save(); + } + /** * Custom snackbar UI used for saving autofill or other informational messages. */ private View mSnackbar; - AutoFillUI(Context context, Session session) { + AutoFillUI(Context context) { mContext = context; - mSession = session; mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } + void setCallback(AutoFillUiCallback callback, int clientId) { + hideAll(); + mCallback = callback; + mClientId = clientId; + } + /** * Displays an error message to the user. */ void showError(CharSequence message) { + if (!hasCallback()) { + return; + } + hideAll(); // TODO(b/33197203): proper implementation UiThread.getHandler().runWithScissors(() -> { Toast.makeText(mContext, "AutoFill error: " + message, Toast.LENGTH_LONG).show(); @@ -95,21 +110,17 @@ final class AutoFillUI { /** * Hides the fill UI. - * Shows the options from a {@link FillResponse} so the user can pick up the proper - * {@link Dataset} (when the response has one) for a given view (identified by - * {@code autoFillId}). */ void hideFillUi() { UiThread.getHandler().runWithScissors(() -> { - hideFillUiLocked(); + hideFillUiUiThread(); }, 0); } - // Must be called in inside UI Thread - private void hideFillUiLocked() { + @android.annotation.UiThread + private void hideFillUiUiThread() { if (mFillWindow != null) { - if (DEBUG) Slog.d(TAG, "hideFillUiLocked(): hide" + mFillWindow); - + if (DEBUG) Slog.d(TAG, "hideFillUiUiThread(): hide" + mFillWindow); mFillWindow.hide(); } @@ -120,7 +131,6 @@ final class AutoFillUI { mFillWindow = null; } - /** * Shows the fill UI, removing the previous fill UI if the has changed. * @@ -129,26 +139,31 @@ final class AutoFillUI { * @param bounds bounds of the view to be filled, used if changed * @param filterText text of the view to be filled, used if changed */ - void showFillUi(ViewState viewState, List<Dataset> datasets, Rect bounds, + void showFillUi(ViewState viewState, ArraySet<Dataset> datasets, Rect bounds, String filterText) { + if (!hasCallback()) { + return; + } + hideAll(); UiThread.getHandler().runWithScissors(() -> { if (mViewState != viewState) { - // new - hideFillUi(); - mViewState = viewState; mFillView = new DatasetPicker(mContext, datasets, (dataset) -> { - mSession.autoFillApp(dataset); + final AutoFillUiCallback callback; + synchronized (mLock) { + callback = mCallback; + } + callback.fill(dataset); hideFillUi(); }); + // TODO: No magical numbers mFillWindow = new AnchoredWindow( mWm, mFillView, 800, ViewGroup.LayoutParams.WRAP_CONTENT); if (DEBUG) Slog.d(TAG, "show FillUi"); } - if (!bounds.equals(mBounds)) { if (DEBUG) Slog.d(TAG, "update FillUi bounds: " + mBounds); mBounds = bounds; @@ -170,27 +185,14 @@ final class AutoFillUI { * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint or tap to * autofill" or "Tap to autofill", depending on the value of {@code usesFingerprint}. */ - void showFillResponseAuthenticationRequest(boolean usesFingerprint, - Bundle extras, int flags) { - // TODO(b/33197203): proper implementation - showAuthNotification(usesFingerprint, extras, flags); - } - - /** - * Shows an UI affordance asking indicating that user action is required before a - * {@link Dataset} can be used. - * - * <p>It typically replaces the auto-fill bar with a message saying "Press fingerprint to - * autofill". - */ - void showDatasetFingerprintAuthenticationRequest(Dataset dataset) { - if (DEBUG) Slog.d(TAG, "showDatasetAuthenticationRequest(): dataset=" + dataset); - - // TODO(b/33197203): proper implementation (either pop up a fingerprint dialog or replace - // the auto-fill bar with a new message. + void showFillResponseAuthRequest(IntentSender intent, Intent fillInIntent) { + if (!hasCallback()) { + return; + } + hideAll(); UiThread.getHandler().runWithScissors(() -> { - Toast.makeText(mContext, "AutoFill: press fingerprint to unlock " + dataset.getName(), - Toast.LENGTH_LONG).show(); + // TODO(b/33197203): proper implementation + showFillResponseAuthUiUiThread(intent, fillInIntent); }, 0); } @@ -198,46 +200,36 @@ final class AutoFillUI { * Shows the UI asking the user to save for auto-fill. */ void showSaveUi() { - showSnackbar(new SavePrompt(mContext, new SavePrompt.OnSaveListener() { - @Override - public void onSaveClick() { - hideSnackbar(); - - // TODO(b/33197203): add MetricsLogger call - mSession.requestSave(); - } - @Override - public void onCancelClick() { - hideSnackbar(); - } - })); - } - - /** - * Called by service after the user user the fingerprint sensors to authenticate. - */ - void dismissFingerprintRequest(boolean success) { - if (DEBUG) Slog.d(TAG, "dismissFingerprintRequest(): ok=" + success); - - dismissAuthNotification(); - - 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); + if (!hasCallback()) { + return; } + hideAll(); + UiThread.getHandler().runWithScissors(() -> { + showSnackbarUiThread(new SavePrompt(mContext, + new SavePrompt.OnSaveListener() { + @Override + public void onSaveClick() { + hideSnackbarUiThread(); + // TODO(b/33197203): add MetricsLogger call + mCallback.save(); + } + + @Override + public void onCancelClick() { + hideSnackbarUiThread(); + } + })); + }, 0); } /** - * Closes all UI affordances. + * Hides all UI affordances. */ - void closeAll() { - if (DEBUG) Slog.d(TAG, "closeAll()"); - + void hideAll() { UiThread.getHandler().runWithScissors(() -> { - hideSnackbarLocked(); - hideFillUiLocked(); + hideSnackbarUiThread(); + hideFillUiUiThread(); + hideFillResponseAuthUiUiThread(); }, 0); } @@ -245,7 +237,7 @@ final class AutoFillUI { pw.println("AufoFill UI"); final String prefix = " "; pw.print(prefix); pw.print("sResultCode: "); pw.println(sResultCode); - pw.print(prefix); pw.print("mSessionId: "); pw.println(mSession.mId); + pw.print(prefix); pw.print("mClientId: "); pw.println(mClientId); pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar); pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState); pw.print(prefix); pw.print("mBounds: "); pw.println(mBounds); @@ -254,7 +246,7 @@ final class AutoFillUI { //similar to a snackbar, but can be a bit custom since it is more than just text. This will //allow two buttons for saving or not saving the autofill for instance as well. - private void showSnackbar(View snackBar) { + private void showSnackbarUiThread(View snackBar) { final LayoutParams params = new LayoutParams(); params.setTitle("AutoFill Save"); params.type = LayoutParams.TYPE_PHONE; // TODO(b/33197203) use app window token @@ -274,20 +266,19 @@ final class AutoFillUI { }, 0); } - private void hideSnackbar() { - UiThread.getHandler().runWithScissors(() -> { - hideSnackbarLocked(); - }, 0); - } - - // Must be called in inside UI Thread - private void hideSnackbarLocked() { + private void hideSnackbarUiThread() { if (mSnackbar != null) { mWm.removeView(mSnackbar); mSnackbar = null; } } + private boolean hasCallback() { + synchronized (mLock) { + return mCallback != null; + } + } + ///////////////////////////////////////////////////////////////////////////////// // TODO(b/33197203): temporary code using a notification to request auto-fill. // // Will be removed once UX decide the right way to present it to the user. // @@ -297,25 +288,13 @@ final class AutoFillUI { private static final String NOTIFICATION_AUTO_FILL_INTENT = "com.android.internal.autofill.action.REQUEST_AUTOFILL"; - // 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_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_AUTH_RESPONSE = "auth_response"; - private BroadcastReceiver mNotificationReceiver; private final Object mLock = new Object(); // Hack used to generate unique pending intents static int sResultCode = 0; - private void setNotificationListener() { + private void ensureNotificationListener() { synchronized (mLock) { if (mNotificationReceiver == null) { mNotificationReceiver = new NotificationReceiver(); @@ -328,83 +307,58 @@ final class AutoFillUI { final class NotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - final String type = intent.getStringExtra(EXTRA_NOTIFICATION_TYPE); - if (type == null) { - Slog.wtf(TAG, "No extra " + EXTRA_NOTIFICATION_TYPE + " on intent " + intent); - return; - } - final Dataset dataset = intent.getParcelableExtra(EXTRA_DATASET); - final int flags = intent.getIntExtra(EXTRA_FLAGS, 0); - - if (DEBUG) Slog.d(TAG, "Notification received: type=" + type - + ", sessionId=" + mSession.mId); + final AutoFillUiCallback callback; synchronized (mLock) { - switch (type) { - case TYPE_AUTH_RESPONSE: - mSession.notifyResponseAuthenticationResult( - intent.getBundleExtra(EXTRA_AUTH_REQUIRED_EXTRAS), flags); - break; - default: { - Slog.w(TAG, "Unknown notification type: " + type); - } - } + callback = mCallback; + } + if (callback != null) { + IntentSender intentSender = intent.getParcelableExtra(EXTRA_AUTH_INTENT_SENDER); + Intent fillInIntent = intent.getParcelableExtra(EXTRA_AUTH_FILL_IN_INTENT); + callback.authenticate(intentSender, fillInIntent); } collapseStatusBar(); } } - private static Intent newNotificationIntent(String type) { - final Intent intent = new Intent(NOTIFICATION_AUTO_FILL_INTENT); - intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); - return intent; - } - - private void showAuthNotification(boolean usesFingerprint, - Bundle extras, int flags) { - final long token = Binder.clearCallingIdentity(); - try { - showAuthNotificationAsSystem(usesFingerprint, extras, flags); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private void showAuthNotificationAsSystem( - boolean usesFingerprint, Bundle extras, int flags) { + @android.annotation.UiThread + private void showFillResponseAuthUiUiThread(IntentSender intent, Intent fillInIntent) { final String title = "AutoFill Authentication"; final StringBuilder subTitle = new StringBuilder("Provider require user authentication.\n"); - final Intent authIntent = newNotificationIntent(TYPE_AUTH_RESPONSE); - 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); + final Intent authIntent = new Intent(NOTIFICATION_AUTO_FILL_INTENT); + authIntent.putExtra(EXTRA_AUTH_INTENT_SENDER, intent); + authIntent.putExtra(EXTRA_AUTH_FILL_IN_INTENT, fillInIntent); - if (usesFingerprint) { - subTitle.append("But kindly accepts your fingerprint instead" - + "\n(tap fingerprint sensor to trigger it)"); + final PendingIntent authPendingIntent = PendingIntent.getBroadcast( + mContext, ++sResultCode, authIntent, PendingIntent.FLAG_ONE_SHOT); - } else { - subTitle.append("Tap notification to launch its authentication UI."); - } + 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); + .setStyle(new Notification.BigTextStyle().bigText(subTitle.toString())) + .setContentIntent(authPendingIntent); + + ensureNotificationListener(); + + final long identity = Binder.clearCallingIdentity(); + try { + NotificationManager.from(mContext).notify(mClientId, notification.build()); + } finally { + Binder.restoreCallingIdentity(identity); } - NotificationManager.from(mContext).notify(mSession.mId, notification.build()); } - private void dismissAuthNotification() { - NotificationManager.from(mContext).cancel(mSession.mId); + @android.annotation.UiThread + private void hideFillResponseAuthUiUiThread() { + final long identity = Binder.clearCallingIdentity(); + try { + NotificationManager.from(mContext).cancel(mClientId); + } finally { + Binder.restoreCallingIdentity(identity); + } } private Notification.Builder newNotificationBuilder() { diff --git a/services/autofill/java/com/android/server/autofill/DatasetPicker.java b/services/autofill/java/com/android/server/autofill/DatasetPicker.java index db516d85efd7..8212cf15d257 100644 --- a/services/autofill/java/com/android/server/autofill/DatasetPicker.java +++ b/services/autofill/java/com/android/server/autofill/DatasetPicker.java @@ -17,6 +17,7 @@ package com.android.server.autofill; import android.content.Context; import android.graphics.Color; +import android.util.ArraySet; import android.view.autofill.Dataset; import android.view.View; import android.view.ViewGroup; @@ -44,7 +45,7 @@ final class DatasetPicker extends ListView implements OnItemClickListener { private final Listener mListener; - DatasetPicker(Context context, List<Dataset> datasets, Listener listener) { + DatasetPicker(Context context, ArraySet<Dataset> datasets, Listener listener) { super(context); mListener = listener; diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index 9171dac04d81..7fff41008866 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -17,6 +17,9 @@ package com.android.server.autofill; import android.os.Bundle; +import android.util.ArraySet; +import android.view.autofill.Dataset; +import android.view.autofill.FillResponse; import java.util.Arrays; import java.util.Objects; @@ -55,4 +58,26 @@ final class Helper { private Helper() { throw new UnsupportedOperationException("contains static members only"); } + + /** + * Finds a data set by id in a response. + * + * @param id The dataset id. + * @param response The response to search. + * @return The dataset if found or null. + */ + static Dataset findDatasetById(String id, FillResponse response) { + ArraySet<Dataset> datasets = response.getDatasets(); + if (datasets == null || datasets.isEmpty()) { + return null; + } + final int datasetCount = datasets.size(); + for (int i = 0; i < datasetCount; i++) { + Dataset dataset = datasets.valueAt(i); + if (dataset.getId().equals(id)) { + return dataset; + } + } + return null; + } } diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java new file mode 100644 index 000000000000..c070f779c6f9 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -0,0 +1,521 @@ +/* + * 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.annotation.NonNull; +import android.annotation.Nullable; +import android.app.assist.AssistStructure; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IBinder.DeathRecipient; +import android.os.ICancellationSignal; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.autofill.AutoFillService; +import android.service.autofill.IAutoFillService; +import android.service.autofill.IFillCallback; +import android.service.autofill.ISaveCallback; +import android.text.format.DateUtils; +import android.util.Slog; +import android.view.autofill.FillResponse; +import com.android.internal.os.HandlerCaller; +import com.android.server.FgThread; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; + +/** + * This class represents a remote fill service. It abstracts away the binding + * and unbinding from the remote implementation. + * + * <p>Clients can call methods of this class without worrying about when and + * how to bind/unbind/timeout. All state of this class is modified on a handler + * thread. + */ +final class RemoteFillService implements DeathRecipient { + private static final String LOG_TAG = "RemoteFillService"; + + private static final boolean DEBUG = Helper.DEBUG; + + // How long after the last interaction with the service we would unbind + private static final long TIMEOUT_IDLE_BIND_MILLIS = 5 * DateUtils.MINUTE_IN_MILLIS; + + private final Context mContext; + + private final ComponentName mComponentName; + + private final Intent mIntent; + + private final FillServiceCallbacks mCallbacks; + + private final int mUserId; + + private final ServiceConnection mServiceConnection = new RemoteServiceConnection(); + + private final HandlerCaller mHandler; + + private IAutoFillService mAutoFillService; + + private boolean mBinding; + + private boolean mDestroyed; + + private boolean mServiceDied; + + private boolean mCompleted; + + private PendingRequest mPendingRequest; + + public interface FillServiceCallbacks { + void onFillRequestSuccess(FillResponse response); + void onFillRequestFailure(CharSequence message); + void onSaveRequestSuccess(); + void onSaveRequestFailure(CharSequence message); + void onServiceDied(RemoteFillService service); + } + + public RemoteFillService(Context context, ComponentName componentName, + int userId, FillServiceCallbacks callbacks) { + mContext = context; + mCallbacks = callbacks; + mComponentName = componentName; + mIntent = new Intent(AutoFillService.SERVICE_INTERFACE) + .setComponent(mComponentName); + mUserId = userId; + mHandler = new MyHandler(context); + } + + public void destroy() { + mHandler.obtainMessage(MyHandler.MSG_DESTROY).sendToTarget(); + } + + private void handleDestroy() { + if (mPendingRequest != null) { + mPendingRequest.cancel(); + mPendingRequest = null; + } + ensureUnbound(); + mDestroyed = true; + } + + @Override + public void binderDied() { + mHandler.obtainMessage(MyHandler.MSG_BINDER_DIED).sendToTarget(); + } + + private void handleBinderDied() { + if (mAutoFillService != null) { + mAutoFillService.asBinder().unlinkToDeath(this, 0); + } + mAutoFillService = null; + mServiceDied = true; + mCallbacks.onServiceDied(this); + } + + public void onFillRequest(@NonNull AssistStructure structure, @Nullable Bundle extras) { + cancelScheduledUnbind(); + PendingFillRequest request = new PendingFillRequest(structure, extras, this); + mHandler.obtainMessageO(MyHandler.MSG_ON_PENDING_REQUEST, request).sendToTarget(); + } + + public void onSaveRequest(@NonNull AssistStructure structure, @Nullable Bundle extras) { + cancelScheduledUnbind(); + PendingSaveRequest request = new PendingSaveRequest(structure, extras, this); + mHandler.obtainMessageO(MyHandler.MSG_ON_PENDING_REQUEST, request).sendToTarget(); + } + + // Note: we are dumping without a lock held so this is a bit racy but + // adding a lock to a class that offloads to a handler thread would + // mean adding a lock adding overhead to normal runtime operation. + public void dump(@NonNull String prefix, @NonNull PrintWriter pw) { + String tab = " "; + pw.append(prefix).append("service:").println(); + pw.append(prefix).append(tab).append("userId=") + .append(String.valueOf(mUserId)).println(); + pw.append(prefix).append(tab).append("componentName=") + .append(mComponentName.flattenToString()).println(); + pw.append(prefix).append(tab).append("destroyed=") + .append(String.valueOf(mDestroyed)).println(); + pw.append(prefix).append(tab).append("bound=") + .append(String.valueOf(isBound())).println(); + pw.append(prefix).append(tab).append("hasPendingRequest=") + .append(String.valueOf(mPendingRequest != null)).println(); + pw.println(); + } + + private void cancelScheduledUnbind() { + mHandler.removeMessages(MyHandler.MSG_UNBIND); + } + + private void scheduleUnbind() { + cancelScheduledUnbind(); + Message message = mHandler.obtainMessage(MyHandler.MSG_UNBIND); + mHandler.sendMessageDelayed(message, TIMEOUT_IDLE_BIND_MILLIS); + } + + private void handleUnbind() { + ensureUnbound(); + } + + private void handlePendingRequest(PendingRequest pendingRequest) { + if (mDestroyed || mCompleted) { + return; + } + if (pendingRequest.isFinal()) { + mCompleted = true; + } + if (!isBound()) { + if (mPendingRequest != null) { + mPendingRequest.cancel(); + } + mPendingRequest = pendingRequest; + ensureBound(); + } else { + if (DEBUG) { + Slog.d(LOG_TAG, "[user: " + mUserId + "] handleOnFillRequest()"); + } + pendingRequest.run(); + } + } + + private boolean isBound() { + return mAutoFillService != null; + } + + private void ensureBound() { + if (isBound() || mBinding) { + return; + } + if (DEBUG) { + Slog.d(LOG_TAG, "[user: " + mUserId + "] ensureBound()"); + } + mBinding = true; + + boolean willBind = mContext.bindServiceAsUser(mIntent, mServiceConnection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, + new UserHandle(mUserId)); + + if (!willBind) { + if (DEBUG) { + Slog.d(LOG_TAG, "[user: " + mUserId + "] could not bind to " + mIntent); + } + mBinding = false; + + if (!mServiceDied) { + handleBinderDied(); + } + } + } + + private void ensureUnbound() { + if (!isBound() && !mBinding) { + return; + } + if (DEBUG) { + Slog.d(LOG_TAG, "[user: " + mUserId + "] ensureUnbound()"); + } + mBinding = false; + if (isBound()) { + mAutoFillService.asBinder().unlinkToDeath(this, 0); + mAutoFillService = null; + } + mContext.unbindService(mServiceConnection); + } + + private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, + FillResponse response) { + mHandler.getHandler().post(() -> { + if (handleResponseCallbackCommon(pendingRequest)) { + mCallbacks.onFillRequestSuccess(response); + } + }); + } + + private void dispatchOnFillRequestFailure(PendingRequest pendingRequest, + CharSequence message) { + mHandler.getHandler().post(() -> { + if (handleResponseCallbackCommon(pendingRequest)) { + mCallbacks.onFillRequestFailure(message); + } + }); + } + + private void dispatchOnSaveRequestSuccess(PendingRequest pendingRequest) { + mHandler.getHandler().post(() -> { + if (handleResponseCallbackCommon(pendingRequest)) { + mCallbacks.onSaveRequestSuccess(); + } + }); + } + + private void dispatchOnSaveRequestFailure(PendingRequest pendingRequest, + CharSequence message) { + mHandler.getHandler().post(() -> { + if (handleResponseCallbackCommon(pendingRequest)) { + mCallbacks.onSaveRequestFailure(message); + } + }); + } + + private boolean handleResponseCallbackCommon(PendingRequest pendingRequest) { + if (mDestroyed) { + return false; + } + if (mPendingRequest == pendingRequest) { + mPendingRequest = null; + } + if (mPendingRequest == null) { + scheduleUnbind(); + } + return true; + } + + private class RemoteServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (mDestroyed || !mBinding) { + mContext.unbindService(mServiceConnection); + return; + } + mBinding = false; + mAutoFillService = IAutoFillService.Stub.asInterface(service); + try { + service.linkToDeath(RemoteFillService.this, 0); + } catch (RemoteException re) { + handleBinderDied(); + return; + } + + if (mPendingRequest != null) { + handlePendingRequest(mPendingRequest); + } + + mServiceDied = false; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mBinding = true; + mAutoFillService = null; + } + } + + private final class MyHandler extends HandlerCaller { + public static final int MSG_DESTROY = 1; + public static final int MSG_BINDER_DIED = 2; + public static final int MSG_UNBIND = 3; + public static final int MSG_ON_PENDING_REQUEST = 4; + + public MyHandler(Context context) { + // Cannot use lambda - doesn't compile + super(context, FgThread.getHandler().getLooper(), new Callback() { + @Override + public void executeMessage(Message message) { + if (mDestroyed) { + Slog.w(LOG_TAG, "Not handling " + message + " as service for " + + mComponentName + " is already destroyed"); + return; + } + switch (message.what) { + case MSG_DESTROY: { + handleDestroy(); + } break; + + case MSG_BINDER_DIED: { + handleBinderDied(); + } break; + + case MSG_UNBIND: { + handleUnbind(); + } break; + + case MSG_ON_PENDING_REQUEST: { + handlePendingRequest((PendingRequest) message.obj); + } break; + } + } + }, false); + } + } + + private static abstract class PendingRequest implements Runnable { + void cancel() { + + } + + /** + * @return whether this request leads to a final state where no + * other requests can be made. + */ + boolean isFinal() { + return false; + } + } + + private static final class PendingFillRequest extends PendingRequest { + private final Object mLock = new Object(); + private final WeakReference<RemoteFillService> mWeakService; + private AssistStructure mStructure; + private Bundle mExtras; + private final IFillCallback mCallback; + private ICancellationSignal mCancellation; + private boolean mCancelled; + + public PendingFillRequest(AssistStructure structure, + Bundle extras, RemoteFillService service) { + mStructure = structure; + mExtras = extras; + mWeakService = new WeakReference<>(service); + mCallback = new IFillCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + synchronized (mLock) { + final boolean cancelled; + synchronized (mLock) { + mCancellation = cancellation; + cancelled = mCancelled; + } + if (cancelled) { + try { + cancellation.cancel(); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error requesting a cancellation", e); + } + } + } + } + + @Override + public void onSuccess(FillResponse response) { + RemoteFillService remoteService = mWeakService.get(); + if (remoteService != null) { + remoteService.dispatchOnFillRequestSuccess( + PendingFillRequest.this, response); + } + } + + @Override + public void onFailure(CharSequence message) { + RemoteFillService remoteService = mWeakService.get(); + if (remoteService != null) { + remoteService.dispatchOnFillRequestFailure( + PendingFillRequest.this, message); + } + } + }; + } + + @Override + public void run() { + RemoteFillService remoteService = mWeakService.get(); + if (remoteService != null) { + try { + remoteService.mAutoFillService.onFillRequest(mStructure, + mExtras, mCallback); + synchronized (mLock) { + mStructure = null; + mExtras = null; + } + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling on fill request", e); + cancel(); + } + } + } + + @Override + public void cancel() { + final ICancellationSignal cancellation; + synchronized (mLock) { + if (mCancelled) { + return; + } + mCancelled = true; + cancellation = mCancellation; + } + if (cancellation == null) { + return; + } + try { + cancellation.cancel(); + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error cancelling a fill request", e); + } + } + } + + private static final class PendingSaveRequest extends PendingRequest { + private final Object mLock = new Object(); + private final WeakReference<RemoteFillService> mWeakService; + private AssistStructure mStructure; + private Bundle mExtras; + private final ISaveCallback mCallback; + + public PendingSaveRequest(@NonNull AssistStructure structure, + @Nullable Bundle extras, @NonNull RemoteFillService service) { + mStructure = structure; + mExtras = extras; + mWeakService = new WeakReference<>(service); + mCallback = new ISaveCallback.Stub() { + @Override + public void onSuccess() { + RemoteFillService service = mWeakService.get(); + if (service != null) { + service.dispatchOnSaveRequestSuccess( + PendingSaveRequest.this); + } + } + + @Override + public void onFailure(CharSequence message) { + RemoteFillService service = mWeakService.get(); + if (service != null) { + service.dispatchOnSaveRequestFailure( + PendingSaveRequest.this, message); + } + } + }; + } + + @Override + public void run() { + RemoteFillService service = mWeakService.get(); + if (service != null) { + try { + service.mAutoFillService.onSaveRequest(mStructure, + mExtras, mCallback); + synchronized (mLock) { + mStructure = null; + mExtras = null; + } + } catch (RemoteException e) { + Slog.e(LOG_TAG, "Error calling on save request", e); + } + } + } + + @Override + public boolean isFinal() { + return true; + } + } +} |