Merge "Add support for remote views backed auto-fill UI"
This commit is contained in:
committed by
Android (Google) Code Review
commit
7bde762f3b
@@ -36246,9 +36246,10 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class Dataset.Builder {
|
||||
ctor public Dataset.Builder(java.lang.CharSequence);
|
||||
ctor public Dataset.Builder();
|
||||
method public android.service.autofill.Dataset build();
|
||||
method public android.service.autofill.Dataset.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.Dataset.Builder setPresentation(android.widget.RemoteViews);
|
||||
method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
|
||||
}
|
||||
|
||||
@@ -36270,6 +36271,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.FillResponse.Builder setExtras(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setPresentation(android.widget.RemoteViews);
|
||||
}
|
||||
|
||||
public final class SaveCallback {
|
||||
@@ -51236,7 +51238,7 @@ package dalvik.system {
|
||||
method public static dalvik.system.DexFile loadDex(java.lang.String, java.lang.String, int) throws java.io.IOException;
|
||||
}
|
||||
|
||||
public final class InMemoryDexClassLoader extends java.lang.ClassLoader {
|
||||
public final class InMemoryDexClassLoader extends dalvik.system.BaseDexClassLoader {
|
||||
ctor public InMemoryDexClassLoader(java.nio.ByteBuffer, java.lang.ClassLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -39326,9 +39326,10 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class Dataset.Builder {
|
||||
ctor public Dataset.Builder(java.lang.CharSequence);
|
||||
ctor public Dataset.Builder();
|
||||
method public android.service.autofill.Dataset build();
|
||||
method public android.service.autofill.Dataset.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.Dataset.Builder setPresentation(android.widget.RemoteViews);
|
||||
method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
|
||||
}
|
||||
|
||||
@@ -39350,6 +39351,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.FillResponse.Builder setExtras(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setPresentation(android.widget.RemoteViews);
|
||||
}
|
||||
|
||||
public final class SaveCallback {
|
||||
@@ -55105,7 +55107,7 @@ package dalvik.system {
|
||||
method public static dalvik.system.DexFile loadDex(java.lang.String, java.lang.String, int) throws java.io.IOException;
|
||||
}
|
||||
|
||||
public final class InMemoryDexClassLoader extends java.lang.ClassLoader {
|
||||
public final class InMemoryDexClassLoader extends dalvik.system.BaseDexClassLoader {
|
||||
ctor public InMemoryDexClassLoader(java.nio.ByteBuffer, java.lang.ClassLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -36385,9 +36385,10 @@ package android.service.autofill {
|
||||
}
|
||||
|
||||
public static final class Dataset.Builder {
|
||||
ctor public Dataset.Builder(java.lang.CharSequence);
|
||||
ctor public Dataset.Builder();
|
||||
method public android.service.autofill.Dataset build();
|
||||
method public android.service.autofill.Dataset.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.Dataset.Builder setPresentation(android.widget.RemoteViews);
|
||||
method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutoFillId, android.view.autofill.AutoFillValue);
|
||||
}
|
||||
|
||||
@@ -36409,6 +36410,7 @@ package android.service.autofill {
|
||||
method public android.service.autofill.FillResponse build();
|
||||
method public android.service.autofill.FillResponse.Builder setAuthentication(android.content.IntentSender);
|
||||
method public android.service.autofill.FillResponse.Builder setExtras(android.os.Bundle);
|
||||
method public android.service.autofill.FillResponse.Builder setPresentation(android.widget.RemoteViews);
|
||||
}
|
||||
|
||||
public final class SaveCallback {
|
||||
@@ -51615,7 +51617,7 @@ package dalvik.system {
|
||||
method public static dalvik.system.DexFile loadDex(java.lang.String, java.lang.String, int) throws java.io.IOException;
|
||||
}
|
||||
|
||||
public final class InMemoryDexClassLoader extends java.lang.ClassLoader {
|
||||
public final class InMemoryDexClassLoader extends dalvik.system.BaseDexClassLoader {
|
||||
ctor public InMemoryDexClassLoader(java.nio.ByteBuffer, java.lang.ClassLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -7039,7 +7039,8 @@ public class Activity extends ContextThemeWrapper
|
||||
}
|
||||
}
|
||||
} else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
|
||||
getSystemService(AutoFillManager.class).onAuthenticationResult(data);
|
||||
Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
|
||||
getSystemService(AutoFillManager.class).onAuthenticationResult(resultData);
|
||||
} else {
|
||||
Fragment frag = mFragments.findFragmentByWho(who);
|
||||
if (frag != null) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.view.autofill.AutoFillId;
|
||||
import android.view.autofill.AutoFillValue;
|
||||
|
||||
import android.widget.RemoteViews;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -36,32 +36,27 @@ import java.util.ArrayList;
|
||||
* <p>It contains:
|
||||
*
|
||||
* <ol>
|
||||
* <li>A name used to identify the dataset in the UI.
|
||||
* <li>A list of id/value pairs for the fields that can be auto-filled.
|
||||
* <li>A list of savable ids in addition to the ones with a provided value.
|
||||
* <li>A list of values for input fields.
|
||||
* <li>A presentation view to visualize.
|
||||
* <li>An optional intent to authenticate.
|
||||
* </ol>
|
||||
*
|
||||
* @see android.service.autofill.FillResponse for examples.
|
||||
*/
|
||||
public final class Dataset implements Parcelable {
|
||||
|
||||
private final CharSequence mName;
|
||||
private final ArrayList<AutoFillId> mFieldIds;
|
||||
private final ArrayList<AutoFillValue> mFieldValues;
|
||||
private final RemoteViews mPresentation;
|
||||
private final IntentSender mAuthentication;
|
||||
|
||||
private Dataset(Builder builder) {
|
||||
mName = builder.mName;
|
||||
mFieldIds = builder.mFieldIds;
|
||||
mFieldValues = builder.mFieldValues;
|
||||
mPresentation = builder.mPresentation;
|
||||
mAuthentication = builder.mAuthentication;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public @NonNull CharSequence getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public @Nullable ArrayList<AutoFillId> getFieldIds() {
|
||||
return mFieldIds;
|
||||
@@ -72,6 +67,11 @@ public final class Dataset implements Parcelable {
|
||||
return mFieldValues;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public @Nullable RemoteViews getPresentation() {
|
||||
return mPresentation;
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public @Nullable IntentSender getAuthentication() {
|
||||
return mAuthentication;
|
||||
@@ -86,11 +86,12 @@ public final class Dataset implements Parcelable {
|
||||
public String toString() {
|
||||
if (!DEBUG) return super.toString();
|
||||
|
||||
final StringBuilder builder = new StringBuilder("Dataset [name=").append(mName)
|
||||
return new StringBuilder("Dataset [")
|
||||
.append(", fieldIds=").append(mFieldIds)
|
||||
.append(", fieldValues=").append(mFieldValues)
|
||||
.append(", hasAuthentication=").append(mAuthentication != null);
|
||||
return builder.append(']').toString();
|
||||
.append(", hasPresentation=").append(mPresentation != null)
|
||||
.append(", hasAuthentication=").append(mAuthentication != null)
|
||||
.append(']').toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,21 +99,22 @@ public final class Dataset implements Parcelable {
|
||||
* one value for a field or set an authentication intent.
|
||||
*/
|
||||
public static final class Builder {
|
||||
private CharSequence mName;
|
||||
private ArrayList<AutoFillId> mFieldIds;
|
||||
private ArrayList<AutoFillValue> mFieldValues;
|
||||
private RemoteViews mPresentation;
|
||||
private IntentSender mAuthentication;
|
||||
private boolean mDestroyed;
|
||||
|
||||
/**
|
||||
* Creates a new builder.
|
||||
* Sets the presentation used to visualize this dataset.
|
||||
*
|
||||
* @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 a user-provided name
|
||||
* (like "My Work Address").
|
||||
* @param presentation The presentation view.
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
public Builder(@NonNull CharSequence name) {
|
||||
mName = Preconditions.checkStringNotEmpty(name, "name cannot be empty or null");
|
||||
public @NonNull Builder setPresentation(@Nullable RemoteViews presentation) {
|
||||
mPresentation = presentation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +123,7 @@ public final class Dataset implements Parcelable {
|
||||
* <p>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.</p>
|
||||
*
|
||||
@@ -147,6 +149,7 @@ public final class Dataset implements Parcelable {
|
||||
* platform needs to fill in the authentication arguments.</p>
|
||||
*
|
||||
* @param authentication Intent to an activity with your authentication flow.
|
||||
* @return This builder.
|
||||
*
|
||||
* @see android.app.PendingIntent
|
||||
*/
|
||||
@@ -162,6 +165,7 @@ public final class Dataset implements Parcelable {
|
||||
* @param id id returned by {@link
|
||||
* android.app.assist.AssistStructure.ViewNode#getAutoFillId()}.
|
||||
* @param value value to be auto filled.
|
||||
* @return This builder.
|
||||
*/
|
||||
public @NonNull Builder setValue(@NonNull AutoFillId id, @NonNull AutoFillValue value) {
|
||||
throwIfDestroyed();
|
||||
@@ -184,14 +188,21 @@ public final class Dataset implements Parcelable {
|
||||
|
||||
/**
|
||||
* Creates a new {@link Dataset} instance. You should not interact
|
||||
* with this builder once this method is called.
|
||||
* with this builder once this method is called. It is required
|
||||
* that you specified at least one field. Also it is mandatory to
|
||||
* provide a presentation view to visualize the data set in the UI.
|
||||
*
|
||||
* @return The built dataset.
|
||||
*/
|
||||
public @NonNull Dataset build() {
|
||||
throwIfDestroyed();
|
||||
mDestroyed = true;
|
||||
if (mFieldIds == null && mAuthentication == null) {
|
||||
if (mFieldIds == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"at least one value or an authentication must be set");
|
||||
"at least one value must be set");
|
||||
}
|
||||
if (mPresentation == null) {
|
||||
throw new IllegalArgumentException("presentation must be set");
|
||||
}
|
||||
return new Dataset(this);
|
||||
}
|
||||
@@ -214,9 +225,9 @@ public final class Dataset implements Parcelable {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeCharSequence(mName);
|
||||
parcel.writeTypedArrayList(mFieldIds, 0);
|
||||
parcel.writeTypedArrayList(mFieldValues, 0);
|
||||
parcel.writeTypedArrayList(mFieldIds, flags);
|
||||
parcel.writeTypedArrayList(mFieldValues, flags);
|
||||
parcel.writeParcelable(mPresentation, flags);
|
||||
parcel.writeParcelable(mAuthentication, flags);
|
||||
}
|
||||
|
||||
@@ -226,7 +237,7 @@ public final class Dataset implements Parcelable {
|
||||
// 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.readCharSequence());
|
||||
final Builder builder = new Builder();
|
||||
final ArrayList<AutoFillId> ids = parcel.readTypedArrayList(null);
|
||||
final ArrayList<AutoFillValue> values = parcel.readTypedArrayList(null);
|
||||
final int idCount = (ids != null) ? ids.size() : 0;
|
||||
@@ -236,6 +247,7 @@ public final class Dataset implements Parcelable {
|
||||
AutoFillValue value = (valueCount > i) ? values.get(i) : null;
|
||||
builder.setValue(id, value);
|
||||
}
|
||||
builder.setPresentation(parcel.readParcelable(null));
|
||||
builder.setAuthentication(parcel.readParcelable(null));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ import android.os.Parcelable;
|
||||
import android.util.ArraySet;
|
||||
import android.view.autofill.AutoFillId;
|
||||
import android.view.autofill.AutoFillManager;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
/**
|
||||
* Response for a {@link
|
||||
* AutoFillService#onFillRequest(android.app.assist.AssistStructure,
|
||||
* Bundle, android.os.CancellationSignal, FillCallback)} and
|
||||
* authentication requests.
|
||||
* Bundle, android.os.CancellationSignal, FillCallback)}.
|
||||
*
|
||||
* <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
|
||||
@@ -43,7 +43,8 @@ import android.view.autofill.AutoFillManager;
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* 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;
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* 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;
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* 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;
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* 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();
|
||||
* </pre>
|
||||
*
|
||||
* <p>Then after the user picks the {@code Homer} dataset and taps the {@code Street} field to
|
||||
* <p>Then after the user picks the second dataset and taps the street field to
|
||||
* trigger another auto-fill request, the second response could be:
|
||||
*
|
||||
* <pre class="prettyprint">
|
||||
* 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.</p>
|
||||
* 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.</p>
|
||||
*/
|
||||
public final class FillResponse implements Parcelable {
|
||||
|
||||
private final ArraySet<Dataset> mDatasets;
|
||||
private final ArraySet<AutoFillId> 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<Dataset> mDatasets;
|
||||
private ArraySet<AutoFillId> 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.</p>
|
||||
* 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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* @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)}.
|
||||
*
|
||||
* <p>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<FillResponse> 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
23
core/res/res/layout/autofill_dataset_picker.xml
Normal file
23
core/res/res/layout/autofill_dataset_picker.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="fill_parent"
|
||||
android:divider="?android:attr/listDivider"
|
||||
android:background="#ffffffff">
|
||||
</ListView>
|
||||
@@ -2840,6 +2840,7 @@
|
||||
<java-symbol type="dimen" name="autofill_fill_item_height" />
|
||||
<java-symbol type="dimen" name="autofill_fill_min_margin" />
|
||||
<java-symbol type="layout" name="autofill_save"/>
|
||||
<java-symbol type="layout" name="autofill_dataset_picker"/>
|
||||
<java-symbol type="id" name="autofill_save_title" />
|
||||
<java-symbol type="id" name="autofill_save_no" />
|
||||
<java-symbol type="id" name="autofill_save_yes" />
|
||||
|
||||
@@ -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<AutoFillId> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Dataset> 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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>A fill session starts when a View is clicked and FillResponse is supplied.
|
||||
* <p>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<Dataset> datasets, Listener listener) {
|
||||
private final ArrayAdapter<ViewItem> mAdapter;
|
||||
|
||||
DatasetPicker(Context context, ArraySet<Dataset> datasets, AutoFillId filteredViewId,
|
||||
Listener listener) {
|
||||
super(context);
|
||||
mListener = listener;
|
||||
|
||||
final List<ViewItem> 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<ViewItem> adapter = new ArrayAdapter<ViewItem>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_1,
|
||||
android.R.id.text1,
|
||||
items) {
|
||||
mAdapter = new ArrayAdapter<ViewItem>(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<ViewItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Dataset> 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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user