Merge "Refactoring of auto fill - lifecycle, auth, improvements"

This commit is contained in:
TreeHugger Robot
2017-02-06 05:47:40 +00:00
committed by Android (Google) Code Review
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

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

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