Merge "Add support for remote views backed auto-fill UI"

This commit is contained in:
TreeHugger Robot
2017-02-20 10:59:34 +00:00
committed by Android (Google) Code Review
15 changed files with 350 additions and 385 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

@@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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