From 00c771dc7d8242362f1491ae4ce3efd641235b36 Mon Sep 17 00:00:00 2001
From: Svet Ganov It contains:
*
* This method is called when you need to provide an authentication
* UI for the data set. For example, when a data set 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
+ * asking for the verification code before filing in the data. Even if the
* data set is completely populated the system will launch the specified authentication
* intent and will need your approval to fill it in. Since the data set is "locked"
* until the user authenticates it, typically this data set name is masked
@@ -138,7 +140,7 @@ public final class Dataset implements Parcelable {
* android.app.Activity#RESULT_OK} and provide the fully populated {@link Dataset
* dataset} by setting it to the {@link
* android.view.autofill.AutoFillManager#EXTRA_AUTHENTICATION_RESULT} extra. For example,
- * if you provided an credit card information without the CVV for the data set in the
+ * if you provided credit card information without the CVV for the data set in the
* {@link FillResponse response} then the returned data set should contain the
* CVV entry.
- *
*
* @see android.service.autofill.FillResponse for examples.
*/
public final class Dataset implements Parcelable {
- private final CharSequence mName;
private final ArrayList
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 @@ -43,7 +43,8 @@ import android.view.autofill.AutoFillManager; * *
* new FillResponse.Builder()
- * .add(new Dataset.Builder("homer")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createPresentation())
* .setTextFieldValue(id1, "homer")
* .setTextFieldValue(id2, "D'OH!")
* .build())
@@ -54,11 +55,13 @@ import android.view.autofill.AutoFillManager;
*
*
* new FillResponse.Builder()
- * .add(new Dataset.Builder("Homer's Account")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createFirstPresentation())
* .setTextFieldValue(id1, "homer")
* .setTextFieldValue(id2, "D'OH!")
* .build())
- * .add(new Dataset.Builder("Bart's Account")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createSecondPresentation())
* .setTextFieldValue(id1, "elbarto")
* .setTextFieldValue(id2, "cowabonga")
* .build())
@@ -82,7 +85,8 @@ import android.view.autofill.AutoFillManager;
*
*
* new FillResponse.Builder()
- * .add(new Dataset.Builder("Homer")
+ * .add(new Dataset.Builder(")
+ * .setPresentation(createPresentation())
* .setTextFieldValue(id1, "Homer") // first name
* .setTextFieldValue(id2, "Simpson") // last name
* .setTextFieldValue(id3, "742 Evergreen Terrace") // street
@@ -110,27 +114,31 @@ import android.view.autofill.AutoFillManager;
*
*
* new FillResponse.Builder()
- * .add(new Dataset.Builder("Homer")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createFirstPresentation())
* .setTextFieldValue(id1, "Homer")
* .setTextFieldValue(id2, "Simpson")
* .build())
- * .add(new Dataset.Builder("Bart")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createSecondPresentation())
* .setTextFieldValue(id1, "Bart")
* .setTextFieldValue(id2, "Simpson")
* .build())
* .build();
*
*
- * Then after the user picks the {@code Homer} dataset and taps the {@code Street} field to
+ *
Then after the user picks the second dataset and taps the street field to
* trigger another auto-fill request, the second response could be:
*
*
* new FillResponse.Builder()
- * .add(new Dataset.Builder("Home")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createThirdPresentation())
* .setTextFieldValue(id3, "742 Evergreen Terrace")
* .setTextFieldValue(id4, "Springfield")
* .build())
- * .add(new Dataset.Builder("Work")
+ * .add(new Dataset.Builder()
+ * .setPresentation(createFourthPresentation())
* .setTextFieldValue(id3, "Springfield Power Plant")
* .setTextFieldValue(id4, "Springfield")
* .build())
@@ -141,29 +149,31 @@ import android.view.autofill.AutoFillManager;
* {@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 onAuthenticate. 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.
+ * which would allow you to provide a dataset presentation views with labels and if the user
+ * chooses one of 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 the presentation views for the
+ * possible options) which will start your auth flow and after successfully authenticating
+ * the user will be presented with the Home and Work options to pick one. Hence, you have
+ * flexibility how to implement your auth while storing labels non-encrypted and data
+ * encrypted provides a better user experience.
*/
public final class FillResponse implements Parcelable {
private final ArraySet mDatasets;
private final ArraySet mSavableIds;
private final Bundle mExtras;
+ private final RemoteViews mPresentation;
private final IntentSender mAuthentication;
private FillResponse(@NonNull Builder builder) {
mDatasets = builder.mDatasets;
mSavableIds = builder.mSavableIds;
mExtras = builder.mExtras;
+ mPresentation = builder.mPresentation;
mAuthentication = builder.mAuthentication;
}
@@ -182,6 +192,11 @@ public final class FillResponse implements Parcelable {
return mSavableIds;
}
+ /** @hide */
+ public @Nullable RemoteViews getPresentation() {
+ return mPresentation;
+ }
+
/** @hide */
public @Nullable IntentSender getAuthentication() {
return mAuthentication;
@@ -189,20 +204,31 @@ public final class FillResponse implements Parcelable {
/**
* Builder for {@link FillResponse} objects. You must to provide at least
- * one dataset or set an authentication intent.
+ * one dataset or set an authentication intent with a presentation view.
*/
public static final class Builder {
private ArraySet mDatasets;
private ArraySet mSavableIds;
private Bundle mExtras;
+ private RemoteViews mPresentation;
private IntentSender mAuthentication;
private boolean mDestroyed;
/**
- * Creates a new {@link FillResponse} builder.
+ * Sets the presentation used to visualize this response. You should
+ * set this only if you need an authentication as this is the only
+ * case the response needs to be presented to the user.
+ *
+ * @param presentation The presentation view.
+ *
+ * @return This builder.
+ *
+ * @see #setAuthentication(IntentSender)
*/
- public Builder() {
-
+ public @NonNull
+ FillResponse.Builder setPresentation(@Nullable RemoteViews presentation) {
+ mPresentation = presentation;
+ return this;
}
/**
@@ -215,14 +241,15 @@ public final class FillResponse implements Parcelable {
* auth on the data set 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. The provided {@link android.app.PendingIntent intent} must be an
- * activity which implements your authentication flow.
+ * activity which implements your authentication flow. Also if you provide an auth
+ * intent you also need to specify the presentation view to be shown in the fill UI
+ * for the user to trigger your authentication flow.
*
* When a user triggers auto-fill, the system launches the provided intent
- * whose extras will have the {@link
- * AutoFillManager#EXTRA_ASSIST_STRUCTURE screen
+ * whose extras will have the {@link AutoFillManager#EXTRA_ASSIST_STRUCTURE screen
* content}. Once you complete your authentication flow you should set the activity
- * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated {@link
- * FillResponse response} by setting it to the {@link
+ * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
+ * {@link FillResponse response} by setting it to the {@link
* AutoFillManager#EXTRA_AUTHENTICATION_RESULT} extra.
* For example, if you provided an empty {@link FillResponse resppnse} because the
* user's data was locked and marked that the response needs an authentication then
@@ -235,8 +262,10 @@ public final class FillResponse implements Parcelable {
* platform needs to fill in the authentication arguments.
*
* @param authentication Intent to an activity with your authentication flow.
+ * @return This builder.
*
* @see android.app.PendingIntent#getIntentSender()
+ * @see #setPresentation(RemoteViews)
*/
public @NonNull Builder setAuthentication(@Nullable IntentSender authentication) {
throwIfDestroyed();
@@ -245,10 +274,9 @@ public final class FillResponse implements Parcelable {
}
/**
- * Adds a new {@link Dataset} to this response. Adding a dataset with the
- * same id updates the existing one.
+ * Adds a new {@link Dataset} to this response.
*
- * @throws IllegalArgumentException if a dataset with same {@code name} already exists.
+ * @return This builder.
*/
public@NonNull Builder addDataset(@Nullable Dataset dataset) {
throwIfDestroyed();
@@ -258,23 +286,18 @@ public final class FillResponse implements Parcelable {
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<>();
+ if (dataset.getFieldIds() != null) {
+ 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);
}
- mSavableIds.add(id);
}
return this;
}
@@ -285,7 +308,10 @@ public final class FillResponse implements Parcelable {
* android.app.assist.AssistStructure, Bundle, SaveCallback)})
* but were not indirectly set through {@link #addDataset(Dataset)}.
*
- * See {@link FillResponse} for examples.
+ * @param ids The savable ids.
+ * @return This builder.
+ *
+ * @see FillResponse
*/
public @NonNull Builder addSavableFields(@Nullable AutoFillId... ids) {
throwIfDestroyed();
@@ -306,10 +332,11 @@ public final class FillResponse implements Parcelable {
* manipulate this response. For example, they are passed to subsequent
* calls to {@link AutoFillService#onFillRequest(
* android.app.assist.AssistStructure, Bundle, android.os.CancellationSignal,
- * FillCallback)} and {@link
- * AutoFillService#onSaveRequest(
- * android.app.assist.AssistStructure, Bundle,
- * SaveCallback)}.
+ * FillCallback)} and {@link AutoFillService#onSaveRequest(
+ * android.app.assist.AssistStructure, Bundle, SaveCallback)}.
+ *
+ * @param extras The response extras.
+ * @return This builder.
*/
public Builder setExtras(Bundle extras) {
throwIfDestroyed();
@@ -318,10 +345,22 @@ public final class FillResponse implements Parcelable {
}
/**
- * Builds a new {@link FillResponse} instance.
+ * Builds a new {@link FillResponse} instance. You must provide at least
+ * one dataset or some savable ids or an authentication with a presentation
+ * view.
+ *
+ * @return A built response.
*/
public FillResponse build() {
throwIfDestroyed();
+ if (mAuthentication == null ^ mPresentation == null) {
+ throw new IllegalArgumentException("authentication and presentation"
+ + " must be both non-null or null");
+ }
+ if (mAuthentication == null && mDatasets == null && mSavableIds == null) {
+ throw new IllegalArgumentException("need to provide at least one"
+ + " data set or savable ids or an authentication with a presentation");
+ }
mDestroyed = true;
return new FillResponse(this);
}
@@ -339,12 +378,13 @@ public final class FillResponse implements Parcelable {
@Override
public String toString() {
if (!DEBUG) return super.toString();
- final StringBuilder builder = new StringBuilder(
+ return new StringBuilder(
"FillResponse: [datasets=").append(mDatasets)
.append(", savableIds=").append(mSavableIds)
.append(", hasExtras=").append(mExtras != null)
- .append(", hasAuthentication=").append(mAuthentication != null);
- return builder.append(']').toString();
+ .append(", hasPresentation=").append(mPresentation != null)
+ .append(", hasAuthentication=").append(mAuthentication != null)
+ .toString();
}
/////////////////////////////////////
@@ -358,10 +398,11 @@ public final class FillResponse implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeTypedArraySet(mDatasets, 0);
- parcel.writeTypedArraySet(mSavableIds, 0);
- parcel.writeParcelable(mExtras, 0);
- parcel.writeParcelable(mAuthentication, 0);
+ parcel.writeTypedArraySet(mDatasets, flags);
+ parcel.writeTypedArraySet(mSavableIds, flags);
+ parcel.writeParcelable(mExtras, flags);
+ parcel.writeParcelable(mPresentation, flags);
+ parcel.writeParcelable(mAuthentication, flags);
}
public static final Parcelable.Creator CREATOR =
@@ -383,6 +424,7 @@ public final class FillResponse implements Parcelable {
builder.addSavableFields(fillIds.valueAt(i));
}
builder.setExtras(parcel.readParcelable(null));
+ builder.setPresentation(parcel.readParcelable(null));
builder.setAuthentication(parcel.readParcelable(null));
return builder.build();
}
diff --git a/core/java/android/view/autofill/AutoFillValue.java b/core/java/android/view/autofill/AutoFillValue.java
index af7036130cd8e..d9afa3ba2dee5 100644
--- a/core/java/android/view/autofill/AutoFillValue.java
+++ b/core/java/android/view/autofill/AutoFillValue.java
@@ -32,7 +32,6 @@ import android.view.View;
* {@code sub-type} define its semantics (like a postal address).
*/
public final class AutoFillValue implements Parcelable {
-
private final String mText;
private final int mListIndex;
private final boolean mToggle;
@@ -100,6 +99,12 @@ public final class AutoFillValue implements Parcelable {
return true;
}
+ /** @hide */
+ public String coerceToString() {
+ // TODO(b/33197203): How can we filter on toggles or list values?
+ return mText;
+ }
+
@Override
public String toString() {
if (!DEBUG) return super.toString();
diff --git a/core/res/res/layout/autofill_dataset_picker.xml b/core/res/res/layout/autofill_dataset_picker.xml
new file mode 100644
index 0000000000000..40cce7b04d875
--- /dev/null
+++ b/core/res/res/layout/autofill_dataset_picker.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e3156a3504c8e..89ded8d5fbe69 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2840,6 +2840,7 @@
+
diff --git a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
index a697a8ee5c4ac..081acf5a94d9d 100644
--- a/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutoFillManagerServiceImpl.java
@@ -451,7 +451,7 @@ final class AutoFillManagerServiceImpl {
* Called when the fill UI is ready to be shown for this view.
*/
void onFillReady(ViewState viewState, FillResponse fillResponse, Rect bounds,
- @Nullable AutoFillValue value);
+ AutoFillId focusedId, @Nullable AutoFillValue value);
}
final AutoFillId mId;
@@ -509,12 +509,14 @@ final class AutoFillManagerServiceImpl {
}
/**
- * Calls {@link Listener#onFillReady(ViewState, FillResponse, Rect, AutoFillValue)} if the
+ * Calls {@link
+ * Listener#onFillReady(ViewState, FillResponse, Rect, AutoFillId, AutoFillValue)} if the
* fill UI is ready to be displayed (i.e. when response and bounds are set).
*/
void maybeCallOnFillReady() {
- if (mResponse != null && mBounds != null) {
- mListener.onFillReady(this, mResponse, mBounds, mAutoFillValue);
+ if (mResponse != null && (mResponse.getAuthentication() != null
+ || mResponse.getDatasets() != null) && mBounds != null) {
+ mListener.onFillReady(this, mResponse, mBounds, mId, mAutoFillValue);
}
}
@@ -641,8 +643,14 @@ final class AutoFillManagerServiceImpl {
// FillServiceCallbacks
@Override
- public void authenticate(IntentSender intent, Intent fillInIntent) {
- startAuthentication(intent, fillInIntent);
+ public void authenticate(IntentSender intent) {
+ final Intent fillInIntent;
+ synchronized (mLock) {
+ fillInIntent = createAuthFillInIntent(mStructure);
+ }
+ mHandlerCaller.getHandler().post(() -> {
+ startAuthentication(intent, fillInIntent);
+ });
}
// FillServiceCallbacks
@@ -675,15 +683,10 @@ final class AutoFillManagerServiceImpl {
processResponseLocked(mCurrentResponse);
} else if (result instanceof Dataset) {
Dataset dataset = (Dataset) result;
- final int datasetIndex = Helper.indexOfDataset(
- dataset.getName(), mCurrentResponse);
- if (datasetIndex <= 0) {
- Slog.e(TAG, "Response for a dataset auth has"
- + " an invalid dataset result: " + dataset.getName());
- }
- mCurrentResponse.getDatasets().removeAt(datasetIndex);
+ mCurrentResponse.getDatasets().remove(mAutoFilledDataset);
mCurrentResponse.getDatasets().add(dataset);
- autoFill(dataset);
+ mAutoFilledDataset = dataset;
+ processResponseLocked(mCurrentResponse);
}
}
}
@@ -727,6 +730,7 @@ final class AutoFillManagerServiceImpl {
return;
}
}
+
// Nothing changed...
if (DEBUG) Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities");
}
@@ -802,8 +806,11 @@ final class AutoFillManagerServiceImpl {
}
}
- // Just change value, don't update the UI
+ // Change value
viewState.mAutoFillValue = value;
+
+ // Update the chooser UI
+ mUi.updateFillUi(value.coerceToString());
return;
}
@@ -838,7 +845,7 @@ final class AutoFillManagerServiceImpl {
@Override
public void onFillReady(ViewState viewState, FillResponse response, Rect bounds,
- @Nullable AutoFillValue value) {
+ AutoFillId filledId, @Nullable AutoFillValue value) {
String filterText = "";
if (value != null) {
// TODO(b/33197203): Handle other AutoFillValue types
@@ -848,8 +855,7 @@ final class AutoFillManagerServiceImpl {
}
}
- getUiForShowing().showFillUi(mActivityToken, viewState, response.getDatasets(),
- bounds, filterText);
+ getUiForShowing().showFillUi(filledId, response, bounds, filterText);
}
private void processResponseLocked(FillResponse response) {
@@ -869,26 +875,17 @@ final class AutoFillManagerServiceImpl {
if (mCurrentResponse.getAuthentication() != null) {
// Handle authentication.
final Intent fillInIntent = createAuthFillInIntent(mStructure);
-
mCurrentViewState.setResponse(mCurrentResponse, fillInIntent);
return;
}
- final ArraySet savableIds = mCurrentResponse.getSavableIds();
- if (savableIds == null || savableIds.isEmpty()) {
- // NOTE: it's assuming the response has no datasets, since when a dataset is added
- // it's view id is automatically added to savable_ids
- if (DEBUG) Slog.d(TAG, "processResponseLocked(): nothing to do");
-
- removeSelf();
- return;
- }
-
mCurrentViewState.setResponse(mCurrentResponse);
}
void autoFill(Dataset dataset) {
synchronized (mLock) {
+ mAutoFilledDataset = dataset;
+
// Autofill it directly...
if (dataset.getAuthentication() == null) {
autoFillApp(dataset);
@@ -948,9 +945,7 @@ final class AutoFillManagerServiceImpl {
synchronized (mLock) {
try {
if (DEBUG) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
-
mClient.autoFill(dataset.getFieldIds(), dataset.getFieldValues());
- mAutoFilledDataset = dataset;
} catch (RemoteException e) {
Slog.w(TAG, "Error auto-filling activity: " + e);
}
@@ -999,7 +994,6 @@ final class AutoFillManagerServiceImpl {
private void destroyLocked() {
mRemoteFillService.destroy();
- mUi.hideAll();
mUi.setCallbackLocked(null, null);
}
diff --git a/services/autofill/java/com/android/server/autofill/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/AutoFillUI.java
index e83dc1e90137d..78dd247f2a027 100644
--- a/services/autofill/java/com/android/server/autofill/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/AutoFillUI.java
@@ -18,32 +18,23 @@ package com.android.server.autofill;
import static com.android.server.autofill.Helper.DEBUG;
import android.annotation.Nullable;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.StatusBarManager;
-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.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.service.autofill.Dataset;
-import android.util.ArraySet;
-import android.os.Looper;
+import android.service.autofill.FillResponse;
import android.text.format.DateUtils;
import android.util.Slog;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.view.autofill.AutoFillId;
import android.widget.Toast;
-import com.android.internal.os.HandlerCaller;
import com.android.server.UiThread;
-import com.android.server.autofill.AutoFillManagerServiceImpl.ViewState;
import java.io.PrintWriter;
@@ -53,36 +44,24 @@ import java.io.PrintWriter;
// 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 long SNACK_BAR_LIFETIME_MS = 30 * DateUtils.SECOND_IN_MILLIS;
+
+ private static final long SNACK_BAR_LIFETIME_MS = 5 * DateUtils.SECOND_IN_MILLIS;
+
private static final int MSG_HIDE_SNACK_BAR = 1;
+ private final Handler mHandler = UiThread.getHandler();
+
private final Context mContext;
+
private final WindowManager mWm;
- // TODO(b/33197203) Fix locking - some state requires lock and some not - requires refactoring
- private final Object mLock = new Object();
-
- // Fill UI variables
private AnchoredWindow mFillWindow;
- private View mFillView;
- private ViewState mViewState;
+
+ private DatasetPicker mDatasetPicker;
private AutoFillUiCallback mCallback;
- private IBinder mActivityToken;
- private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
- switch (msg.what) {
- case MSG_HIDE_SNACK_BAR: {
- hideSnackbarUiThread();
- return;
- }
- default: {
- Slog.w(TAG, "Invalid message: " + msg);
- }
- }
- };
- private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
- mHandlerCallback, true);
+ private IBinder mActivityToken;
/**
* Custom snackbar UI used for saving autofill or other informational messages.
@@ -95,32 +74,32 @@ final class AutoFillUI {
}
void setCallbackLocked(AutoFillUiCallback callback, IBinder activityToken) {
- hideAll();
- mCallback = callback;
- mActivityToken = activityToken;
+ mHandler.post(() -> {
+ hideAllUiThread();
+ mCallback = callback;
+ mActivityToken = activityToken;
+ });
}
/**
* Displays an error message to the user.
*/
void showError(CharSequence message) {
- if (!hasCallback()) {
- return;
- }
- hideAll();
// TODO(b/33197203): proper implementation
- UiThread.getHandler().runWithScissors(() -> {
+ UiThread.getHandler().post(() -> {
+ if (!hasCallback()) {
+ return;
+ }
+ hideAllUiThread();
Toast.makeText(mContext, "AutoFill error: " + message, Toast.LENGTH_LONG).show();
- }, 0);
+ });
}
/**
* Hides the fill UI.
*/
void hideFillUi() {
- UiThread.getHandler().runWithScissors(() -> {
- hideFillUiUiThread();
- }, 0);
+ mHandler.post(() -> hideFillUiUiThread());
}
@android.annotation.UiThread
@@ -129,111 +108,80 @@ final class AutoFillUI {
if (DEBUG) Slog.d(TAG, "hideFillUiUiThread(): hide" + mFillWindow);
mFillWindow.hide();
}
-
- mViewState = null;
- mFillView = null;
mFillWindow = null;
+ mDatasetPicker = null;
+ }
+
+ void updateFillUi(@Nullable String filterText) {
+ mHandler.post(() -> {
+ if (!hasCallback()) {
+ return;
+ }
+ hideSnackbarUiThread();
+ if (mDatasetPicker != null) {
+ mDatasetPicker.update(filterText);
+ }
+ });
}
/**
* Shows the fill UI, removing the previous fill UI if the has changed.
*
- * @param appToken the token of the app to be autofilled
- * @param viewState the view state, compared by reference to know if new UI should be shown
- * @param datasets the datasets to show, not used if viewState is the same
+ * @param focusedId the currently focused field
+ * @param response the current fill response
* @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(IBinder appToken, ViewState viewState, @Nullable ArraySet datasets,
- Rect bounds, String filterText) {
- if (!hasCallback()) {
- return;
- }
-
- UiThread.getHandler().runWithScissors(() -> {
+ void showFillUi(AutoFillId focusedId, @Nullable FillResponse response, Rect bounds,
+ String filterText) {
+ mHandler.post(() -> {
+ if (!hasCallback()) {
+ return;
+ }
hideSnackbarUiThread();
- }, 0);
-
- if (datasets == null && viewState.mAuthIntent == null) {
- // TODO(b/33197203): shouldn't be called, but keeping the WTF for a while just to be
- // safe, otherwise it would crash system server...
- Slog.wtf(TAG, "showFillUI(): no dataset");
- return;
- }
-
- // TODO(b/33197203): should not display UI after we launched an authentication intent, since
- // we have no warranty the provider will call onFailure() if the authentication failed or
- // user dismissed the auth window
- // because if the service does not handle calling the callback,
-
-
- UiThread.getHandler().runWithScissors(() -> {
- // The dataset picker is only shown when authentication is not required...
- DatasetPicker datasetPicker = null;
-
- if (mViewState == null || !mViewState.mId.equals(viewState.mId)) {
- hideFillUiUiThread();
-
- mViewState = viewState;
- if (viewState.mAuthIntent != null) {
- final CharSequence serviceName = viewState.getServiceName();
-
- mFillView = new SignInPrompt(mContext, serviceName, (e) -> {
- final IntentSender intentSender = viewState.mResponse.getAuthentication();
- final AutoFillUiCallback callback;
- final Intent authIntent;
- synchronized (mLock) {
- callback = mCallback;
- authIntent = viewState.mAuthIntent;
- // Must reset the authentication intent so UI display the datasets after
- // the user authenticated.
- viewState.mAuthIntent = null;
+ final View content;
+ if (response.getPresentation() != null) {
+ content = response.getPresentation().apply(mContext, null);
+ content.setOnClickListener((view) -> {
+ if (mCallback != null) {
+ mCallback.authenticate(response.getAuthentication());
+ }
+ hideFillUiUiThread();
+ });
+ } else {
+ mDatasetPicker = new DatasetPicker(mContext, response.getDatasets(),
+ focusedId, new DatasetPicker.Listener() {
+ @Override
+ public void onDatasetPicked(Dataset dataset) {
+ if (mCallback != null) {
+ mCallback.fill(dataset);
}
- if (callback != null) {
- callback.authenticate(intentSender, authIntent);
- } else {
- // TODO(b/33197203): need to figure out why it's null sometimes
- Slog.w(TAG, "no callback on showFillUi().auth for " + viewState.mId);
- }
- });
+ hideFillUiUiThread();
+ }
- } else {
- mFillView = datasetPicker = new DatasetPicker(mContext, datasets,
- (dataset) -> {
- final AutoFillUiCallback callback;
- synchronized (mLock) {
- callback = mCallback;
- }
- if (callback != null) {
- callback.fill(dataset);
- } else {
- // TODO(b/33197203): need to figure out why it's null sometimes
- Slog.w(TAG, "no callback on showFillUi() for " + viewState.mId);
- }
- hideFillUiUiThread();
- });
- }
- mFillWindow = new AnchoredWindow(mWm, appToken, mFillView);
+ @Override
+ public void onCanceled() {
+ hideFillUiUiThread();
+ }
+ });
+ mDatasetPicker.update(filterText);
+ content = mDatasetPicker;
+ }
- if (DEBUG) Slog.d(TAG, "showFillUi(): view changed for: " + viewState.mId);
- }
- if (datasetPicker != null) {
- datasetPicker.update(filterText);
- }
+ mFillWindow = new AnchoredWindow(mWm, mActivityToken, content);
mFillWindow.show(bounds);
-
- }, 0);
+ });
}
/**
* Shows the UI asking the user to save for auto-fill.
*/
void showSaveUi() {
- if (!hasCallback()) {
- return;
- }
- hideAll();
- UiThread.getHandler().runWithScissors(() -> {
+ mHandler.post(() -> {
+ if (!hasCallback()) {
+ return;
+ }
+ hideAllUiThread();
showSnackbarUiThread(new SavePrompt(mContext,
new SavePrompt.OnSaveListener() {
@Override
@@ -249,17 +197,20 @@ final class AutoFillUI {
hideSnackbarUiThread();
}
}));
- }, 0);
+ });
}
/**
* Hides all UI affordances.
*/
void hideAll() {
- UiThread.getHandler().runWithScissors(() -> {
- hideSnackbarUiThread();
- hideFillUiUiThread();
- }, 0);
+ mHandler.post(() -> hideAllUiThread());
+ }
+
+ @android.annotation.UiThread
+ private void hideAllUiThread() {
+ hideSnackbarUiThread();
+ hideFillUiUiThread();
}
void dump(PrintWriter pw) {
@@ -267,14 +218,13 @@ final class AutoFillUI {
final String prefix = " ";
pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("mSnackBar: "); pw.println(mSnackbar);
- pw.print(prefix); pw.print("mViewState: "); pw.println(mViewState);
}
//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.
+ @android.annotation.UiThread
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
params.flags =
LayoutParams.FLAG_NOT_FOCUSABLE // don't receive input events,
@@ -286,20 +236,21 @@ final class AutoFillUI {
params.width = LayoutParams.MATCH_PARENT;
params.height = LayoutParams.WRAP_CONTENT;
- UiThread.getHandler().runWithScissors(() -> {
+ mHandler.post(() -> {
mSnackbar = snackBar;
mWm.addView(mSnackbar, params);
- }, 0);
+ });
if (DEBUG) {
Slog.d(TAG, "showSnackbar(): auto dismissing it in " + SNACK_BAR_LIFETIME_MS + " ms");
}
- mHandlerCaller.sendMessageDelayed(mHandlerCaller.obtainMessage(MSG_HIDE_SNACK_BAR),
- SNACK_BAR_LIFETIME_MS);
+ mHandler.sendMessageDelayed(mHandler
+ .obtainMessage(MSG_HIDE_SNACK_BAR), SNACK_BAR_LIFETIME_MS);
}
+ @android.annotation.UiThread
private void hideSnackbarUiThread() {
- mHandlerCaller.getHandler().removeMessages(MSG_HIDE_SNACK_BAR);
+ mHandler.removeMessages(MSG_HIDE_SNACK_BAR);
if (mSnackbar != null) {
mWm.removeView(mSnackbar);
mSnackbar = null;
@@ -307,13 +258,11 @@ final class AutoFillUI {
}
private boolean hasCallback() {
- synchronized (mLock) {
- return mCallback != null;
- }
+ return mCallback != null;
}
interface AutoFillUiCallback {
- void authenticate(IntentSender intent, Intent fillInIntent);
+ void authenticate(IntentSender intent);
void fill(Dataset dataset);
void save();
}
diff --git a/services/autofill/java/com/android/server/autofill/DatasetPicker.java b/services/autofill/java/com/android/server/autofill/DatasetPicker.java
index a54cab928f4b2..3190f600e649d 100644
--- a/services/autofill/java/com/android/server/autofill/DatasetPicker.java
+++ b/services/autofill/java/com/android/server/autofill/DatasetPicker.java
@@ -16,73 +16,72 @@
package com.android.server.autofill;
import android.content.Context;
-import android.graphics.Color;
import android.service.autofill.Dataset;
-import android.text.TextUtils;
import android.util.ArraySet;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.autofill.AutoFillId;
+import android.view.autofill.AutoFillValue;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
-import android.widget.Filter.FilterListener;
+import android.widget.FrameLayout;
import android.widget.ListView;
-import android.widget.TextView;
+import android.widget.RemoteViews;
+import com.android.internal.R;
+import com.android.internal.R;
import java.util.ArrayList;
import java.util.List;
/**
- * View for dataset picker.
- *
- * A fill session starts when a View is clicked and FillResponse is supplied.
- *
A fill session ends when 1) the user takes action in the UI, 2) another View is clicked, or
- * 3) the View is detached.
+ * This class manages the dataset selection UI.
*/
-final class DatasetPicker extends ListView implements OnItemClickListener {
+final class DatasetPicker extends FrameLayout implements OnItemClickListener {
interface Listener {
void onDatasetPicked(Dataset dataset);
+ void onCanceled();
}
private final Listener mListener;
- DatasetPicker(Context context, ArraySet datasets, Listener listener) {
+ private final ArrayAdapter mAdapter;
+
+ DatasetPicker(Context context, ArraySet datasets, AutoFillId filteredViewId,
+ Listener listener) {
super(context);
mListener = listener;
final List items = new ArrayList<>(datasets.size());
for (Dataset dataset : datasets) {
- items.add(new ViewItem(dataset));
+ final int index = dataset.getFieldIds().indexOf(filteredViewId);
+ if (index >= 0) {
+ AutoFillValue value = dataset.getFieldValues().get(index);
+ items.add(new ViewItem(dataset, value.coerceToString()));
+ }
}
- final ArrayAdapter adapter = new ArrayAdapter(
- context,
- android.R.layout.simple_list_item_1,
- android.R.id.text1,
- items) {
+ mAdapter = new ArrayAdapter(context, 0, items) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- final TextView textView = (TextView) super.getView(position, convertView, parent);
- textView.setSingleLine();
- textView.setEllipsize(TextUtils.TruncateAt.END);
- textView.setMinHeight(
- getDimen(com.android.internal.R.dimen.autofill_fill_item_height));
- return textView;
+ RemoteViews presentation = getItem(position).getDataset().getPresentation();
+ return presentation.apply(context, parent);
}
};
- setAdapter(adapter);
- setBackgroundColor(Color.WHITE);
- setDivider(null);
- setElevation(getDimen(com.android.internal.R.dimen.autofill_fill_elevation));
- setOnItemClickListener(this);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ListView content = (ListView) inflater.inflate(
+ com.android.internal.R.layout.autofill_dataset_picker, this, true)
+ .findViewById(com.android.internal.R.id.list);
+ content.setAdapter(mAdapter);
+ content.setOnItemClickListener(this);
}
public void update(String prefix) {
- final ArrayAdapter adapter = (ArrayAdapter) getAdapter();
- adapter.getFilter().filter(prefix, new FilterListener() {
- @Override
- public void onFilterComplete(int count) {
- setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+ mAdapter.getFilter().filter(prefix, (count) -> {
+ if (count <= 0 && mListener != null) {
+ mListener.onCanceled();
}
});
}
@@ -91,29 +90,27 @@ final class DatasetPicker extends ListView implements OnItemClickListener {
public void onItemClick(AdapterView> adapterView, View view, int pos, long id) {
if (mListener != null) {
final ViewItem vi = (ViewItem) adapterView.getItemAtPosition(pos);
- mListener.onDatasetPicked(vi.getData());
+ mListener.onDatasetPicked(vi.getDataset());
}
}
- private int getDimen(int resId) {
- return getContext().getResources().getDimensionPixelSize(resId);
- }
-
private static class ViewItem {
- private final Dataset mData;
+ private final String mValue;
+ private final Dataset mDataset;
- ViewItem(Dataset data) {
- mData = data;
+ ViewItem(Dataset dataset, String value) {
+ mDataset = dataset;
+ mValue = value;
}
- public Dataset getData() {
- return mData;
+ public Dataset getDataset() {
+ return mDataset;
}
@Override
public String toString() {
// used by ArrayAdapter
- return mData.getName().toString();
+ return mValue;
}
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index 48ae635c281f2..2f600c21d1fd0 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -19,8 +19,6 @@ package com.android.server.autofill;
import android.annotation.Nullable;
import android.os.Bundle;
import android.service.autofill.Dataset;
-import android.service.autofill.FillResponse;
-import android.util.ArraySet;
import android.view.autofill.AutoFillId;
import android.view.autofill.AutoFillValue;
@@ -77,28 +75,6 @@ final class Helper {
return null;
}
- /**
- * Finds the index of a data set given its name.
- *
- * @param name The dataset name.
- * @param response The response to search.
- * @return The index of dataset if found or -1.
- */
- static int indexOfDataset(CharSequence name, FillResponse response) {
- ArraySet datasets = response.getDatasets();
- if (datasets == null || datasets.isEmpty()) {
- return -1;
- }
- final int datasetCount = datasets.size();
- for (int i = 0; i < datasetCount; i++) {
- Dataset dataset = datasets.valueAt(i);
- if (dataset.getName().toString().equals(name.toString())) {
- return i;
- }
- }
- return -1;
- }
-
private Helper() {
throw new UnsupportedOperationException("contains static members only");
}
diff --git a/services/autofill/java/com/android/server/autofill/SavePrompt.java b/services/autofill/java/com/android/server/autofill/SavePrompt.java
index f0b51e260dcdd..85c6d7d2c2d38 100644
--- a/services/autofill/java/com/android/server/autofill/SavePrompt.java
+++ b/services/autofill/java/com/android/server/autofill/SavePrompt.java
@@ -17,9 +17,7 @@
package com.android.server.autofill;
import android.content.Context;
-import android.graphics.Color;
import android.widget.RelativeLayout;
-import android.widget.RelativeLayout.LayoutParams;
import android.widget.TextView;
import android.view.LayoutInflater;
import android.view.View;
@@ -55,6 +53,5 @@ final class SavePrompt extends RelativeLayout {
mListener.onSaveClick();
});
- //addView(view);
}
}
diff --git a/services/autofill/java/com/android/server/autofill/SignInPrompt.java b/services/autofill/java/com/android/server/autofill/SignInPrompt.java
deleted file mode 100644
index 6d17acda4bdad..0000000000000
--- a/services/autofill/java/com/android/server/autofill/SignInPrompt.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.content.Context;
-import android.view.View;
-import android.widget.Button;
-
-/**
- * A view displaying the sign-in prompt for an auto-fill service.
- */
-final class SignInPrompt extends Button {
-
- SignInPrompt(Context context, CharSequence serviceName, View.OnClickListener listener) {
- super(context);
- // TODO(b/33197203): use strings.xml
- final String text = serviceName != null
- ? "Sign in to " + serviceName + " to autofill"
- : "Sign in to autofill";
-
- // TODO(b/33197203): polish UI / use better altenative than a button...
- setText(text);
- setOnClickListener(listener);
- }
-}