diff options
| author | 2017-02-06 05:47:40 +0000 | |
|---|---|---|
| committer | 2017-02-06 05:47:45 +0000 | |
| commit | 85a05cd9b5945c42f46ce1bcacb2d1e0aa8c7a44 (patch) | |
| tree | 514ee3ef5c6bd38f13390b13d7036c9447347b52 | |
| parent | ad00d0248c09fdfea9f5042d2c0fa0707689efc3 (diff) | |
| parent | 0f4928f1f73407485d6d94beda1dba1a2360ebbf (diff) | |
Merge "Refactoring of auto fill - lifecycle, auth, improvements"
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 39c6fbdb5777..4a16b839b934 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9509,6 +9509,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"; @@ -39133,26 +39137,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 { @@ -50165,10 +50162,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); } @@ -50180,12 +50176,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); } @@ -50611,7 +50606,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; + } + } +} |