Refactoring of auto fill - lifecycle, auth, improvements

1. Move management of the remote fill service in a dedicated
   class that abstracts away the async and ephemeral nature
   of the binding.

2. Update auth to move fingerprint out of the platform and
   allow response and dataset auth.

3. Cleaned up the fill and save callback classes.

4. The UI is now shared among all sessions and cleaned up.

5. Reshuffled the remote callbacks to have cleaner separation.

6. Cleaned up and tightened the reponse and dataset classes.

7. Added API to support communicationn with intent based auth.

Test: CTS + manually

bug:31001899

Change-Id: Idc924a01d1aea82807e0397ff7293d2b8470d4d6
This commit is contained in:
Svet Ganov
2017-02-02 20:02:51 -08:00
committed by Svetoslav Ganov
parent 21c8595d76
commit 0f4928f1f7
25 changed files with 1843 additions and 2078 deletions

View File

@@ -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 \

View File

@@ -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 {

View File

@@ -9504,6 +9504,10 @@ package android.content {
field public static final java.lang.String EXTRA_ASSIST_INPUT_HINT_KEYBOARD = "android.intent.extra.ASSIST_INPUT_HINT_KEYBOARD";
field public static final java.lang.String EXTRA_ASSIST_PACKAGE = "android.intent.extra.ASSIST_PACKAGE";
field public static final java.lang.String EXTRA_ASSIST_UID = "android.intent.extra.ASSIST_UID";
field public static final java.lang.String EXTRA_AUTO_FILL_ASSIST_STRUCTURE = "android.intent.extra.AUTO_FILL_ASSIST_STRUCTURE";
field public static final java.lang.String EXTRA_AUTO_FILL_CALLBACK = "android.intent.extra.AUTO_FILL_CALLBACK";
field public static final java.lang.String EXTRA_AUTO_FILL_EXTRAS = "android.intent.extra.AUTO_FILL_EXTRAS";
field public static final java.lang.String EXTRA_AUTO_FILL_ITEM_ID = "android.intent.extra.AUTO_FILL_ITEM_ID";
field public static final java.lang.String EXTRA_BCC = "android.intent.extra.BCC";
field public static final java.lang.String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
field public static final java.lang.String EXTRA_CC = "android.intent.extra.CC";
@@ -39128,26 +39132,19 @@ package android.service.autofill {
ctor public AutoFillService();
method public final android.os.IBinder onBind(android.content.Intent);
method public void onConnected();
method public void onDatasetAuthenticationRequest(android.os.Bundle, int);
method public void onDisconnected();
method public abstract void onFillRequest(android.app.assist.AssistStructure, android.os.Bundle, android.os.CancellationSignal, android.service.autofill.FillCallback);
method public void onFillResponseAuthenticationRequest(android.os.Bundle, int);
method public abstract void onSaveRequest(android.app.assist.AssistStructure, android.os.Bundle, android.service.autofill.SaveCallback);
field public static final java.lang.String EXTRA_DATASET_EXTRAS = "android.service.autofill.extra.DATASET_EXTRAS";
field public static final java.lang.String EXTRA_RESPONSE_EXTRAS = "android.service.autofill.extra.RESPONSE_EXTRAS";
field public static final int FLAG_AUTHENTICATION_ERROR = 4; // 0x4
field public static final int FLAG_AUTHENTICATION_REQUESTED = 1; // 0x1
field public static final int FLAG_AUTHENTICATION_SUCCESS = 2; // 0x2
field public static final int FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE = 8; // 0x8
field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutoFillService";
field public static final java.lang.String SERVICE_META_DATA = "android.autofill";
}
public final class FillCallback {
method public void onDatasetAuthentication(android.view.autofill.Dataset, int);
public final class FillCallback implements android.os.Parcelable {
method public int describeContents();
method public void onFailure(java.lang.CharSequence);
method public void onFillResponseAuthentication(int);
method public void onSuccess(android.view.autofill.FillResponse);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.autofill.FillCallback> CREATOR;
}
public final class SaveCallback {
@@ -50160,10 +50157,9 @@ package android.view.autofill {
}
public static final class Dataset.Builder {
ctor public Dataset.Builder(java.lang.CharSequence);
ctor public Dataset.Builder(java.lang.String, java.lang.CharSequence);
method public android.view.autofill.Dataset build();
method public android.view.autofill.Dataset.Builder requiresCustomAuthentication(android.os.Bundle, int);
method public android.view.autofill.Dataset.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.Dataset.Builder setAuthentication(android.content.IntentSender);
method public android.view.autofill.Dataset.Builder setExtras(android.os.Bundle);
method public android.view.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
}
@@ -50175,12 +50171,11 @@ package android.view.autofill {
}
public static final class FillResponse.Builder {
ctor public FillResponse.Builder();
ctor public FillResponse.Builder(java.lang.String);
method public android.view.autofill.FillResponse.Builder addDataset(android.view.autofill.Dataset);
method public android.view.autofill.FillResponse.Builder addSavableFields(android.view.autofill.AutoFillId...);
method public android.view.autofill.FillResponse build();
method public android.view.autofill.FillResponse.Builder requiresCustomAuthentication(android.os.Bundle, int);
method public android.view.autofill.FillResponse.Builder requiresFingerprintAuthentication(android.hardware.fingerprint.FingerprintManager.CryptoObject, android.os.Bundle, int);
method public android.view.autofill.FillResponse.Builder setAuthentication(android.content.IntentSender);
method public android.view.autofill.FillResponse.Builder setExtras(android.os.Bundle);
}
@@ -50606,7 +50601,7 @@ package android.view.textclassifier {
public final class TextClassificationManager {
method public java.util.List<android.view.textclassifier.TextLanguage> detectLanguages(java.lang.CharSequence);
method public android.view.textclassifier.TextClassifier getDefaultTextClassifier();
method public synchronized android.view.textclassifier.TextClassifier getDefaultTextClassifier();
}
public final class TextClassificationResult {

View File

@@ -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 {

View File

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

View File

@@ -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);
@@ -1375,6 +1376,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.
*

View File

@@ -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)
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 save(AssistStructure structure, IAutoFillServerCallback callback,
Bundle extras) throws RemoteException {
mHandlerCaller
.obtainMessageOOO(MSG_SAVE, structure, callback, extras)
.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()");
}
}

View File

@@ -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;
}

View File

@@ -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();
}
public void onFailure(@Nullable CharSequence message) {
assertNotCalled();
mCalled = true;
try {
mCallback.onFailure(message);
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
}
/**
* Notifies the Android System when the user authenticated a {@link FillResponse} previously
* passed to {@link #onSuccess(FillResponse)}.
*
* @param flags must contain either
* {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or
* {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}.
*/
public void onFillResponseAuthentication(int flags) {
if (DEBUG) Log.d(TAG, "onFillResponseAuthentication(): flags=" + flags);
synchronized (mCallback) {
assertOnStateLocked(STATE_WAITING_FILL_RESPONSE_AUTH_RESPONSE);
try {
mCallback.unlockFillResponse(flags);
setFinalStateLocked(STATE_FINISHED_AUTHENTICATED);
} catch (RemoteException e) {
setFinalStateLocked(STATE_FINISHED_ERROR);
e.rethrowAsRuntimeException();
}
}
}
/**
* Notifies the Android System when the user authenticated a {@link Dataset} previously passed
* to {@link #onSuccess(FillResponse)}.
*
* @param dataset values to fill the activity with in case of successful authentication of a
* previously locked (and empty) dataset).
* @param flags must contain either
* {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} or
* {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}.
*/
public void onDatasetAuthentication(@Nullable Dataset dataset, int flags) {
if (DEBUG) Log.d(TAG, "onDatasetAuthentication(): dataset=" + dataset + ", flags=" + flags);
synchronized (mCallback) {
assertOnStateLocked(STATE_WAITING_DATASET_AUTH_RESPONSE);
try {
mCallback.unlockDataset(dataset, flags);
setFinalStateLocked(STATE_FINISHED_AUTHENTICATED);
} catch (RemoteException e) {
setFinalStateLocked(STATE_FINISHED_ERROR);
e.rethrowAsRuntimeException();
}
}
}
@Override
public String toString() {
if (!DEBUG) return super.toString();
return "FillCallback: [mState = " + mState + "]";
}
/** @hide */
@Override
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("FillCallback: mState="); pw.println(mState);
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());
}
private void assertNotCalled() {
if (mCalled) {
throw new IllegalStateException("Already called");
}
}
/**
* 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();
public static final Creator<FillCallback> CREATOR = new Creator<FillCallback>() {
@Override
public FillCallback createFromParcel(Parcel parcel) {
return new FillCallback(parcel);
}
}
// TODO(b/33197203): move and/or re-add state check logic on server side to avoid malicious app
// calling the callback on wrong state.
// Make sure callback method is called during the proper lifecycle state.
private void assertOnStateLocked(int flags) {
if (DEBUG) Log.d(TAG, "assertOnState(): current=" + mState + ", required=" + flags);
Preconditions.checkState((flags & mState) != 0,
"invalid state: required " + flagsToString(FillCallback.class, "STATE_", flags)
+ ", current is " + flagsToString(FillCallback.class, "STATE_", mState));
}
@Override
public FillCallback[] newArray(int size) {
return new FillCallback[size];
}
};
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
static interface Finalizer {
void gone();
}
/**
* Interface to receive the result of a save request.
*
* @hide
*/
interface ISaveCallback {
void onSuccess();
void onFailure(CharSequence message);
}

View File

@@ -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();
}
assertNotCalled();
mCalled = true;
try {
mCallback.onFailure(message);
} catch (RemoteException e) {
e.rethrowAsRuntimeException();
}
}
/** @hide */
@Override
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("SaveCallback: mReplied="); pw.println(mReplied);
}
/** @hide */
@Override
public void setFinalizer(Finalizer f) {
synchronized (mCallback) {
mFinalizer = f;
}
}
@Override
public String toString() {
if (!DEBUG) return super.toString();
return "SaveCallback: [mReplied= " + mReplied + "]";
}
// There can be only one!!
private void 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");
}
}
}

View File

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

View File

@@ -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 {
}
}
}
}

View File

@@ -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() {
public @NonNull String getId() {
return mId;
}
/** @hide */
public @NonNull CharSequence getName() {
return mName;
}
/** @hide */
public List<DatasetField> getFields() {
return mFields;
public @Nullable ArrayList<AutoFillId> getFieldIds() {
return mFieldIds;
}
/** @hide */
public Bundle getExtras() {
public @Nullable ArrayList<AutoFillValue> getFieldValues() {
return mFieldValues;
}
/** @hide */
public @Nullable Bundle getExtras() {
return mExtras;
}
/** @hide */
public int getFlags() {
return mFlags;
}
/** @hide */
public boolean isAuthRequired() {
return mRequiresAuth;
public @Nullable IntentSender getAuthentication() {
return mAuthentication;
}
/** @hide */
public boolean isEmpty() {
return mFields.isEmpty();
}
/** @hide */
public boolean hasCryptoObject() {
return mHasCryptoObject;
}
/** @hide */
public long getCryptoObjectOpId() {
return mCryptoOpId;
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.
* <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>
*
* @param authentication Intent to trigger your authentication flow.
*
* @see android.app.PendingIntent#getIntentSender()
*/
public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) {
return requiresAuthentication(null, extras, flags);
}
/**
* Requires dataset authentication through the Fingerprint sensor before auto-filling the
* activity with this dataset.
*
* <p>This method is typically called when the dataset contains sensitive information (for
* example, credit card information) and the provider requires the user to re-authenticate
* before using it.
*
* <p>Since the dataset is "locked" until the user authenticates it, typically this dataset
* name is masked (for example, "VISA....1234").
*
* <p>When the user selects this dataset, the Android System displays an UI affordance
* asking the user to use the fingerprint sensor unlock the dataset, and what happens after
* a successful fingerprint authentication depends on whether the dataset is empty (no
* fields, only the masked name) or not:
*
* <ul>
* <li>If it's empty, the Android System will call {@link
* android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the
* flags.
* <li>If it's not empty, the activity will be auto-filled with its data.
* </ul>
*
* <p>If the fingerprint authentication fails, the Android System will call {@link
* android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
* passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the
* flags.
*
* <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use
* the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint
* authentication is available before using this method, and use other alternatives (such as
* {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is
* called when fingerprint is not available, Android System will call {@link
* android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
* passing {@link
* android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}
* in the flags, but it would be wasting system resources (and worsening the user
* experience) in the process.
*
* @param crypto object that will be authenticated.
* @param extras when set, will be passed back in the {@link
* android.service.autofill.AutoFillService#onDatasetAuthenticationRequest(Bundle, int)}
* call so it could be used by the service to handle state.
* @param flags optional parameters, currently ignored.
*/
public Builder requiresFingerprintAuthentication(CryptoObject crypto,
@Nullable Bundle extras, int flags) {
// TODO(b/33197203): should we allow crypto to be null?
Preconditions.checkArgument(crypto != null, "must pass a CryptoObject");
return requiresAuthentication(crypto, extras, flags);
}
private Builder requiresAuthentication(CryptoObject cryptoObject, Bundle extras,
int flags) {
// There can be only one!
Preconditions.checkState(!mRequiresAuth,
"requires-authentication methods already called");
// TODO(b/33197203): make sure that either this method or setExtras() is called, but
// not both
mExtras = extras;
mFlags = flags;
mRequiresAuth = true;
if (cryptoObject != null) {
mHasCryptoObject = true;
mCryptoOpId = cryptoObject.getOpId();
}
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() {
public @NonNull Builder setExtras(@Nullable Bundle extras) {
throwIfDestroyed();
mExtras = extras;
return this;
}
/**
* Creates a new {@link Dataset} instance. You should not interact
* with this builder once this method is called.
*/
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);
}
/**
* 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.
*/
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;
}
/**
* 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

View File

@@ -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];
}
};
}

View File

@@ -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;
mAuthentication = builder.mAuthentication;
}
/** @hide */
public List<Dataset> getDatasets() {
return mDatasets;
public @NonNull String getId() {
return mId;
}
/** @hide */
public AutoFillId[] getSavableIds() {
return mSavableIds;
}
/** @hide */
public Bundle getExtras() {
public @Nullable Bundle getExtras() {
return mExtras;
}
/** @hide */
public int getFlags() {
return mFlags;
public @Nullable ArraySet<Dataset> getDatasets() {
return mDatasets;
}
/** @hide */
public boolean isAuthRequired() {
return mRequiresAuth;
public @Nullable ArraySet<AutoFillId> getSavableIds() {
return mSavableIds;
}
/** @hide */
public boolean hasCryptoObject() {
return mHasCryptoObject;
}
/** @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;
/**
* Requires user authentication through the {@link android.service.autofill.AutoFillService}
* before handling an auto-fill request.
*
* <p>This method is typically called when the device (or the service) does not support
* fingerprint authentication (and hence it cannot use {@link
* #requiresFingerprintAuthentication(CryptoObject, Bundle, int)}) or when the service needs
* to use a custom authentication UI and is used in 2 scenarios:
*
* <ol>
* <li>When the user data is encrypted and the service must authenticate an object that
* will be used to decrypt it.
* <li>When the service already acquired the user data but wants to confirm the user's
* identity before the activity is filled with it.
* </ol>
*
* <p>When this method is called, the Android System displays an UI affordance asking the
* user to tap it to auto-fill the activity; if the user taps it, the Android System calls
* {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_REQUESTED} in the flags and
* the same {@code extras} passed to this method. The service can then displays its custom
* authentication UI, and then call the proper method on {@link FillCallback} depending on
* the authentication result and whether this response already contains the {@link Dataset}s
* need to auto-fill the activity:
*
* <ul>
* <li>If authentication failed, call {@link
* FillCallback#onFillResponseAuthentication(int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR} in the flags.
* <li>If authentication succeeded and this response is empty (no datasets), call {@link
* FillCallback#onSuccess(FillResponse)} with a new dataset (that does not require
* authentication).
* <li>If authentication succeeded and this response is not empty, call {@link
* FillCallback#onFillResponseAuthentication(int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS} in the flags.
* </ul>
*
* @param extras when set, will be passed back in the {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} call so it could be used by the service to handle state.
* @param flags optional parameters, currently ignored.
*/
public Builder requiresCustomAuthentication(@Nullable Bundle extras, int flags) {
return requiresAuthentication(null, extras, flags);
/** @hide */
// TODO(b/33197203): Remove once GCore migrates
public Builder() {
this(String.valueOf(System.currentTimeMillis()));
}
/**
* Requires user authentication through the Fingerprint sensor before handling an auto-fill
* request.
* Creates a new {@link FillResponse} builder.
*
* <p>The {@link android.service.autofill.AutoFillService} typically uses this method in 2
* situations:
*
* <ol>
* <li>When the user data is encrypted and the service must authenticate an object that
* will be used to decrypt it.
* <li>When the service already acquired the user data but wants to confirm the user's
* identity before the activity is filled with it.
* </ol>
*
* <p>When this method is called, the Android System displays an UI affordance asking the
* user to use the fingerprint sensor to auto-fill the activity, and what happens after a
* successful fingerprint authentication depends on the number of {@link Dataset}s included
* in this response:
*
* <ul>
* <li>If it's empty (scenario #1 above), the Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_SUCCESS}} in the
* flags.
* <li>If it contains one dataset, the activity will be auto-filled right away.
* <li>If it contains many datasets, the Android System will show dataset picker UI, and
* then auto-fill the activity once the user select the proper datased.
* </ul>
*
* <p>If the fingerprint authentication fails, the Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link android.service.autofill.AutoFillService#FLAG_AUTHENTICATION_ERROR}
* in the flags.
*
* <p><strong>NOTE: </note> the {@link android.service.autofill.AutoFillService} should use
* the {@link android.hardware.fingerprint.FingerprintManager} to check if fingerpint
* authentication is available before using this method, and use other alternatives (such as
* {@link #requiresCustomAuthentication(Bundle, int)}) if it is not: if this method is
* called when fingerprint is not available, Android System will call {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} passing {@link
* android.service.autofill.AutoFillService#FLAG_FINGERPRINT_AUTHENTICATION_NOT_AVAILABLE}
* in the flags, but it would be wasting system resources (and worsening the user
* experience) in the process.
*
* @param crypto object that will be authenticated.
* @param extras when set, will be passed back in the {@link
* android.service.autofill.AutoFillService#onFillResponseAuthenticationRequest(Bundle,
* int)} call so it could be used by the service to handle state.
* @param flags optional parameters, currently ignored.
* @param id A required id to identify this dataset for future interactions related to it.
*/
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);
public Builder(@NonNull String id) {
mId = Preconditions.checkStringNotEmpty(id, "id cannot be empty or null");
}
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();
}
/**
* 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>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>
*
* <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>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>
*
* <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></><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>
*
* @param authentication Intent to trigger your authentication flow.
*
* @see android.app.PendingIntent#getIntentSender()
*/
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

View File

@@ -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;
if (activityToken != null) {
synchronized (mLock) {
final AutoFillManagerServiceImpl service =
getOrCreateServiceForUserLocked(userId);
service.requestAutoFillLocked(activityToken, null, null, 0);
}
}
synchronized (mLock) {
final AutoFillManagerServiceImpl service = getServiceForUserLocked(userId);
if (service == null) {
Slog.w(TAG, "no service for user " + userId);
return;
}
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);
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}