All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.service.autofill.FillResponse Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.service.autofill;

import static android.service.autofill.AutofillServiceHelper.assertValid;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static android.view.autofill.Helper.sDebug;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.app.Activity;
import android.content.IntentSender;
import android.content.pm.ParceledListSlice;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.autofill.AutofillId;
import android.widget.RemoteViews;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Response for an {@link
 * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}.
 *
 * 

See the main {@link AutofillService} documentation for more details and examples. */ public final class FillResponse implements Parcelable { /** * Flag used to generate {@link FillEventHistory.Event events} of type * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}—if this flag is not passed to * {@link Builder#setFlags(int)}, these events are not generated. */ public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1; /** * Flag used to change the behavior of {@link FillResponse.Builder#disableAutofill(long)}— * when this flag is passed to {@link Builder#setFlags(int)}, autofill is disabled only for the * activiy that generated the {@link FillRequest}, not the whole app. */ public static final int FLAG_DISABLE_ACTIVITY_ONLY = 0x2; /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_TRACK_CONTEXT_COMMITED, FLAG_DISABLE_ACTIVITY_ONLY }) @Retention(RetentionPolicy.SOURCE) @interface FillResponseFlags {} private final @Nullable ParceledListSlice mDatasets; private final @Nullable SaveInfo mSaveInfo; private final @Nullable Bundle mClientState; private final @Nullable RemoteViews mPresentation; private final @Nullable RemoteViews mHeader; private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; private final long mDisableDuration; private final @Nullable AutofillId[] mFieldClassificationIds; private final int mFlags; private int mRequestId; private final @Nullable UserData mUserData; private FillResponse(@NonNull Builder builder) { mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null; mSaveInfo = builder.mSaveInfo; mClientState = builder.mClientState; mPresentation = builder.mPresentation; mHeader = builder.mHeader; mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; mFieldClassificationIds = builder.mFieldClassificationIds; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; mUserData = builder.mUserData; } /** @hide */ public @Nullable Bundle getClientState() { return mClientState; } /** @hide */ public @Nullable List getDatasets() { return (mDatasets != null) ? mDatasets.getList() : null; } /** @hide */ public @Nullable SaveInfo getSaveInfo() { return mSaveInfo; } /** @hide */ public @Nullable RemoteViews getPresentation() { return mPresentation; } /** @hide */ public @Nullable RemoteViews getHeader() { return mHeader; } /** @hide */ public @Nullable RemoteViews getFooter() { return mFooter; } /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; } /** @hide */ public @Nullable AutofillId[] getAuthenticationIds() { return mAuthenticationIds; } /** @hide */ public @Nullable AutofillId[] getIgnoredIds() { return mIgnoredIds; } /** @hide */ public long getDisableDuration() { return mDisableDuration; } /** @hide */ public @Nullable AutofillId[] getFieldClassificationIds() { return mFieldClassificationIds; } /** @hide */ public @Nullable UserData getUserData() { return mUserData; } /** @hide */ @TestApi public int getFlags() { return mFlags; } /** * Associates a {@link FillResponse} to a request. * *

Set inside of the {@link FillCallback} code, not the {@link AutofillService}. * * @param requestId The id of the request to associate the response to. * * @hide */ public void setRequestId(int requestId) { mRequestId = requestId; } /** @hide */ public int getRequestId() { return mRequestId; } /** * Builder for {@link FillResponse} objects. You must to provide at least * one dataset or set an authentication intent with a presentation view. */ public static final class Builder { private ArrayList mDatasets; private SaveInfo mSaveInfo; private Bundle mClientState; private RemoteViews mPresentation; private RemoteViews mHeader; private RemoteViews mFooter; private IntentSender mAuthentication; private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; private AutofillId[] mFieldClassificationIds; private int mFlags; private boolean mDestroyed; private UserData mUserData; /** * Triggers a custom UI before before autofilling the screen with any data set in this * response. * *

Note: Although the name of this method suggests that it should be used just for * authentication flow, it can be used for other advanced flows; see {@link AutofillService} * for examples. * *

This is typically useful when a user interaction is required to unlock their * data vault if you encrypt the data set labels and data set data. It is recommended * to encrypt only the sensitive data and not the data set labels which would allow * 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 * {@link Activity} which implements your authentication flow. Also if you provide an auth * intent you also need to specify the presentation view to be shown in the fill UI * for the user to trigger your authentication flow. * *

When a user triggers autofill, the system launches the provided intent * whose extras will have the * {@link android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE * client state}. Once you complete your authentication flow you should set the * {@link Activity} result to {@link android.app.Activity#RESULT_OK} and set the * {@link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra * with the fully populated {@link FillResponse response} (or {@code null} if the screen * cannot be autofilled). * *

For example, if you provided an empty {@link FillResponse response} because the * user's data was locked and marked that the response needs an authentication then * in the response returned if authentication succeeds you need to provide all * available data sets some of which may need to be further authenticated, for * example a credit card whose CVV needs to be entered. * *

If you provide an authentication intent you must also provide a presentation * which is used to visualize visualize the response for triggering the authentication * flow. * *

Note: Do not make the provided pending intent * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the * platform needs to fill in the authentication arguments. * *

Theme does not work with RemoteViews layout. Avoid hardcoded text color * or background color: Autofill on different platforms may have different themes. * * @param authentication Intent to an activity with your authentication flow. * @param presentation The presentation to visualize the response. * @param ids id of Views that when focused will display the authentication UI. * * @return This builder. * * @throws IllegalArgumentException if any of the following occurs: *

    *
  • {@code ids} is {@code null}
  • *
  • {@code ids} is empty
  • *
  • {@code ids} contains a {@code null} element
  • *
  • both {@code authentication} and {@code presentation} are {@code null}
  • *
  • both {@code authentication} and {@code presentation} are non-{@code null}
  • *
* * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a * {@link #setFooter(RemoteViews) footer} are already set for this builder. * * @see android.app.PendingIntent#getIntentSender() */ @NonNull public Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); if (mHeader != null || mFooter != null) { throw new IllegalStateException("Already called #setHeader() or #setFooter()"); } if (authentication == null ^ presentation == null) { throw new IllegalArgumentException("authentication and presentation" + " must be both non-null or null"); } mAuthentication = authentication; mPresentation = presentation; mAuthenticationIds = assertValid(ids); return this; } /** * Specifies views that should not trigger new * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, * FillCallback)} requests. * *

This is typically used when the service cannot autofill the view; for example, a * text field representing the result of a Captcha challenge. */ @NonNull public Builder setIgnoredIds(AutofillId...ids) { throwIfDestroyed(); mIgnoredIds = ids; return this; } /** * Adds a new {@link Dataset} to this response. * *

Note: on Android {@link android.os.Build.VERSION_CODES#O}, the total number of * datasets is limited by the Binder transaction size, so it's recommended to keep it * small (in the range of 10-20 at most) and use pagination by adding a fake * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated dataset} at the end * with a presentation string like "Next 10" that would return a new {@link FillResponse} * with the next 10 datasets, and so on. This limitation was lifted on * Android {@link android.os.Build.VERSION_CODES#O_MR1}, although the Binder transaction * size can still be reached if each dataset itself is too big. * * @return This builder. */ @NonNull public Builder addDataset(@Nullable Dataset dataset) { throwIfDestroyed(); throwIfDisableAutofillCalled(); if (dataset == null) { return this; } if (mDatasets == null) { mDatasets = new ArrayList<>(); } if (!mDatasets.add(dataset)) { return this; } return this; } /** * Sets the {@link SaveInfo} associated with this response. * * @return This builder. */ public @NonNull Builder setSaveInfo(@NonNull SaveInfo saveInfo) { throwIfDestroyed(); throwIfDisableAutofillCalled(); mSaveInfo = saveInfo; return this; } /** * Sets a bundle with state that is passed to subsequent APIs that manipulate this response. * *

You can use this bundle to store intermediate state that is passed to subsequent calls * to {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, * FillCallback)} and {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)}, and * you can also retrieve it by calling {@link FillEventHistory.Event#getClientState()}. * *

If this method is called on multiple {@link FillResponse} objects for the same * screen, just the latest bundle is passed back to the service. * * @param clientState The custom client state. * @return This builder. */ @NonNull public Builder setClientState(@Nullable Bundle clientState) { throwIfDestroyed(); throwIfDisableAutofillCalled(); mClientState = clientState; return this; } /** * Sets which fields are used for * field classification * *

Note: This method automatically adds the * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} to the {@link #setFlags(int) flags}. * @throws IllegalArgumentException is length of {@code ids} args is more than * {@link UserData#getMaxFieldClassificationIdsSize()}. * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was * already called. * @throws NullPointerException if {@code ids} or any element on it is {@code null}. */ @NonNull public Builder setFieldClassificationIds(@NonNull AutofillId... ids) { throwIfDestroyed(); throwIfDisableAutofillCalled(); Preconditions.checkArrayElementsNotNull(ids, "ids"); Preconditions.checkArgumentInRange(ids.length, 1, UserData.getMaxFieldClassificationIdsSize(), "ids length"); mFieldClassificationIds = ids; mFlags |= FLAG_TRACK_CONTEXT_COMMITED; return this; } /** * Sets flags changing the response behavior. * * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and * {@link #FLAG_DISABLE_ACTIVITY_ONLY}, or {@code 0}. * * @return This builder. */ @NonNull public Builder setFlags(@FillResponseFlags int flags) { throwIfDestroyed(); mFlags = Preconditions.checkFlagsArgument(flags, FLAG_TRACK_CONTEXT_COMMITED | FLAG_DISABLE_ACTIVITY_ONLY); return this; } /** * Disables autofill for the app or activity. * *

This method is useful to optimize performance in cases where the service knows it * can not autofill an app—for example, when the service has a list of "blacklisted" * apps such as office suites. * *

By default, it disables autofill for all activities in the app, unless the response is * {@link #setFlags(int) flagged} with {@link #FLAG_DISABLE_ACTIVITY_ONLY}. * *

Autofill for the app or activity is automatically re-enabled after any of the * following conditions: * *

    *
  1. {@code duration} milliseconds have passed. *
  2. The autofill service for the user has changed. *
  3. The device has rebooted. *
* *

Note: Activities that are running when autofill is re-enabled remain * disabled for autofill until they finish and restart. * * @param duration duration to disable autofill, in milliseconds. * * @return this builder * * @throws IllegalArgumentException if {@code duration} is not a positive number. * @throws IllegalStateException if either {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or * {@link #setFieldClassificationIds(AutofillId...)} was already called. */ @NonNull public Builder disableAutofill(long duration) { throwIfDestroyed(); if (duration <= 0) { throw new IllegalArgumentException("duration must be greater than 0"); } if (mAuthentication != null || mDatasets != null || mSaveInfo != null || mFieldClassificationIds != null || mClientState != null) { throw new IllegalStateException("disableAutofill() must be the only method called"); } mDisableDuration = duration; return this; } /** * Sets a header to be shown as the first element in the list of datasets. * *

When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this * method should only be used on {@link FillResponse FillResponses} that do not require * authentication (as the header could have been set directly in the main presentation in * these cases). * *

Theme does not work with RemoteViews layout. Avoid hardcoded text color * or background color: Autofill on different platforms may have different themes. * * @param header a presentation to represent the header. This presentation is not clickable * —calling * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would * have no effect. * * @return this builder * * @throws IllegalStateException if an * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was * already set for this builder. */ // TODO(b/69796626): make it sticky / update javadoc @NonNull public Builder setHeader(@NonNull RemoteViews header) { throwIfDestroyed(); throwIfAuthenticationCalled(); mHeader = Preconditions.checkNotNull(header); return this; } /** * Sets a footer to be shown as the last element in the list of datasets. * *

When this method is called, you must also {@link #addDataset(Dataset) add a dataset}, * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this * method should only be used on {@link FillResponse FillResponses} that do not require * authentication (as the footer could have been set directly in the main presentation in * these cases). * *

Theme does not work with RemoteViews layout. Avoid hardcoded text color * or background color: Autofill on different platforms may have different themes. * * @param footer a presentation to represent the footer. This presentation is not clickable * —calling * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would * have no effect. * * @return this builder * * @throws IllegalStateException if the FillResponse * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) * requires authentication}. */ // TODO(b/69796626): make it sticky / update javadoc @NonNull public Builder setFooter(@NonNull RemoteViews footer) { throwIfDestroyed(); throwIfAuthenticationCalled(); mFooter = Preconditions.checkNotNull(footer); return this; } /** * Sets a specific {@link UserData} for field classification for this request only. * *

Any fields in this UserData will override corresponding fields in the generic * UserData object * * @return this builder * @throws IllegalStateException if the FillResponse * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) * requires authentication}. */ @NonNull public Builder setUserData(@NonNull UserData userData) { throwIfDestroyed(); throwIfAuthenticationCalled(); mUserData = Preconditions.checkNotNull(userData); return this; } /** * Builds a new {@link FillResponse} instance. * * @throws IllegalStateException if any of the following conditions occur: *

    *
  1. {@link #build()} was already called. *
  2. No call was made to {@link #addDataset(Dataset)}, * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, * {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)}, * {@link #setClientState(Bundle)}, * or {@link #setFieldClassificationIds(AutofillId...)}. *
  3. {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called * without any previous calls to {@link #addDataset(Dataset)}. *
* * @return A built response. */ @NonNull public FillResponse build() { throwIfDestroyed(); if (mAuthentication == null && mDatasets == null && mSaveInfo == null && mDisableDuration == 0 && mFieldClassificationIds == null && mClientState == null) { throw new IllegalStateException("need to provide: at least one DataSet, or a " + "SaveInfo, or an authentication with a presentation, " + "or a FieldsDetection, or a client state, or disable autofill"); } if (mDatasets == null && (mHeader != null || mFooter != null)) { throw new IllegalStateException( "must add at least 1 dataset when using header or footer"); } mDestroyed = true; return new FillResponse(this); } private void throwIfDestroyed() { if (mDestroyed) { throw new IllegalStateException("Already called #build()"); } } private void throwIfDisableAutofillCalled() { if (mDisableDuration > 0) { throw new IllegalStateException("Already called #disableAutofill()"); } } private void throwIfAuthenticationCalled() { if (mAuthentication != null) { throw new IllegalStateException("Already called #setAuthentication()"); } } } ///////////////////////////////////// // Object "contract" methods. // ///////////////////////////////////// @Override public String toString() { if (!sDebug) return super.toString(); // TODO: create a dump() method instead final StringBuilder builder = new StringBuilder( "FillResponse : [mRequestId=" + mRequestId); if (mDatasets != null) { builder.append(", datasets=").append(mDatasets.getList()); } if (mSaveInfo != null) { builder.append(", saveInfo=").append(mSaveInfo); } if (mClientState != null) { builder.append(", hasClientState"); } if (mPresentation != null) { builder.append(", hasPresentation"); } if (mHeader != null) { builder.append(", hasHeader"); } if (mFooter != null) { builder.append(", hasFooter"); } if (mAuthentication != null) { builder.append(", hasAuthentication"); } if (mAuthenticationIds != null) { builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)); } builder.append(", disableDuration=").append(mDisableDuration); if (mFlags != 0) { builder.append(", flags=").append(mFlags); } if (mFieldClassificationIds != null) { builder.append(Arrays.toString(mFieldClassificationIds)); } if (mUserData != null) { builder.append(", userData=").append(mUserData); } return builder.append("]").toString(); } ///////////////////////////////////// // Parcelable "contract" methods. // ///////////////////////////////////// @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mDatasets, flags); parcel.writeParcelable(mSaveInfo, flags); parcel.writeParcelable(mClientState, flags); parcel.writeParcelableArray(mAuthenticationIds, flags); parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); parcel.writeParcelable(mUserData, flags); parcel.writeParcelableArray(mIgnoredIds, flags); parcel.writeLong(mDisableDuration); parcel.writeParcelableArray(mFieldClassificationIds, flags); parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public FillResponse createFromParcel(Parcel parcel) { // Always go through the builder to ensure the data ingested by // the system obeys the contract of the builder to avoid attacks // using specially crafted parcels. final Builder builder = new Builder(); final ParceledListSlice datasetSlice = parcel.readParcelable(null); final List datasets = (datasetSlice != null) ? datasetSlice.getList() : null; final int datasetCount = (datasets != null) ? datasets.size() : 0; for (int i = 0; i < datasetCount; i++) { builder.addDataset(datasets.get(i)); } builder.setSaveInfo(parcel.readParcelable(null)); builder.setClientState(parcel.readParcelable(null)); // Sets authentication state. final AutofillId[] authenticationIds = parcel.readParcelableArray(null, AutofillId.class); final IntentSender authentication = parcel.readParcelable(null); final RemoteViews presentation = parcel.readParcelable(null); if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation); } final RemoteViews header = parcel.readParcelable(null); if (header != null) { builder.setHeader(header); } final RemoteViews footer = parcel.readParcelable(null); if (footer != null) { builder.setFooter(footer); } final UserData userData = parcel.readParcelable(null); if (userData != null) { builder.setUserData(userData); } builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class)); final long disableDuration = parcel.readLong(); if (disableDuration > 0) { builder.disableAutofill(disableDuration); } final AutofillId[] fieldClassifactionIds = parcel.readParcelableArray(null, AutofillId.class); if (fieldClassifactionIds != null) { builder.setFieldClassificationIds(fieldClassifactionIds); } builder.setFlags(parcel.readInt()); final FillResponse response = builder.build(); response.setRequestId(parcel.readInt()); return response; } @Override public FillResponse[] newArray(int size) { return new FillResponse[size]; } }; }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy