/* * 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.DrawableRes; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.Activity; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; import android.content.pm.ParceledListSlice; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.service.assist.classification.FieldClassification; 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; import java.util.Objects; import java.util.Set; /** * 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 { // common_typos_disable /** * 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; /** * Flag used to request to wait for a delayed fill from the remote Autofill service if it's * passed to {@link Builder#setFlags(int)}. * *

Some datasets (i.e. OTP) take time to produce. This flags allows remote service to send * a {@link FillResponse} to the latest {@link FillRequest} via * {@link FillRequest#getDelayedFillIntentSender()} even if the original {@link FillCallback} * has timed out. */ public static final int FLAG_DELAY_FILL = 0x4; /** * @hide */ public static final int FLAG_CREDENTIAL_MANAGER_RESPONSE = 0x8; /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = { FLAG_TRACK_CONTEXT_COMMITED, FLAG_DISABLE_ACTIVITY_ONLY, FLAG_DELAY_FILL, FLAG_CREDENTIAL_MANAGER_RESPONSE }) @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 InlinePresentation mInlinePresentation; private final @Nullable InlinePresentation mInlineTooltipPresentation; private final @Nullable RemoteViews mDialogPresentation; private final @Nullable RemoteViews mDialogHeader; 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 @Nullable AutofillId[] mFillDialogTriggerIds; private final long mDisableDuration; private final @Nullable AutofillId[] mFieldClassificationIds; private final int mFlags; private int mRequestId; private final @Nullable UserData mUserData; private final @Nullable int[] mCancelIds; private final boolean mSupportsInlineSuggestions; private final @DrawableRes int mIconResourceId; private final @StringRes int mServiceDisplayNameResourceId; private final boolean mShowFillDialogIcon; private final boolean mShowSaveDialogIcon; private final @Nullable FieldClassification[] mDetectedFieldTypes; private final @Nullable PendingIntent mDialogPendingIntent; /** * Creates a shollow copy of the provided FillResponse. * * @hide */ public static FillResponse shallowCopy( FillResponse r, List datasets, SaveInfo saveInfo) { return new FillResponse( (datasets != null) ? new ParceledListSlice<>(datasets) : null, saveInfo, r.mClientState, r.mPresentation, r.mInlinePresentation, r.mInlineTooltipPresentation, r.mDialogPresentation, r.mDialogHeader, r.mHeader, r.mFooter, r.mAuthentication, r.mAuthenticationIds, r.mIgnoredIds, r.mFillDialogTriggerIds, r.mDisableDuration, r.mFieldClassificationIds, r.mFlags, r.mRequestId, r.mUserData, r.mCancelIds, r.mSupportsInlineSuggestions, r.mIconResourceId, r.mServiceDisplayNameResourceId, r.mShowFillDialogIcon, r.mShowSaveDialogIcon, r.mDetectedFieldTypes, r.mDialogPendingIntent); } private FillResponse(ParceledListSlice datasets, SaveInfo saveInfo, Bundle clientState, RemoteViews presentation, InlinePresentation inlinePresentation, InlinePresentation inlineTooltipPresentation, RemoteViews dialogPresentation, RemoteViews dialogHeader, RemoteViews header, RemoteViews footer, IntentSender authentication, AutofillId[] authenticationIds, AutofillId[] ignoredIds, AutofillId[] fillDialogTriggerIds, long disableDuration, AutofillId[] fieldClassificationIds, int flags, int requestId, UserData userData, int[] cancelIds, boolean supportsInlineSuggestions, int iconResourceId, int serviceDisplayNameResourceId, boolean showFillDialogIcon, boolean showSaveDialogIcon, FieldClassification[] detectedFieldTypes, PendingIntent dialogPendingIntent) { mDatasets = datasets; mSaveInfo = saveInfo; mClientState = clientState; mPresentation = presentation; mInlinePresentation = inlinePresentation; mInlineTooltipPresentation = inlineTooltipPresentation; mDialogPresentation = dialogPresentation; mDialogHeader = dialogHeader; mHeader = header; mFooter = footer; mAuthentication = authentication; mAuthenticationIds = authenticationIds; mIgnoredIds = ignoredIds; mFillDialogTriggerIds = fillDialogTriggerIds; mDisableDuration = disableDuration; mFieldClassificationIds = fieldClassificationIds; mFlags = flags; mRequestId = requestId; mUserData = userData; mCancelIds = cancelIds; mSupportsInlineSuggestions = supportsInlineSuggestions; mIconResourceId = iconResourceId; mServiceDisplayNameResourceId = serviceDisplayNameResourceId; mShowFillDialogIcon = showFillDialogIcon; mShowSaveDialogIcon = showSaveDialogIcon; mDetectedFieldTypes = detectedFieldTypes; mDialogPendingIntent = dialogPendingIntent; } private FillResponse(@NonNull Builder builder) { mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null; mSaveInfo = builder.mSaveInfo; mClientState = builder.mClientState; mPresentation = builder.mPresentation; mInlinePresentation = builder.mInlinePresentation; mInlineTooltipPresentation = builder.mInlineTooltipPresentation; mDialogPresentation = builder.mDialogPresentation; mDialogHeader = builder.mDialogHeader; mHeader = builder.mHeader; mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; mAuthenticationIds = builder.mAuthenticationIds; mFillDialogTriggerIds = builder.mFillDialogTriggerIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; mFieldClassificationIds = builder.mFieldClassificationIds; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; mUserData = builder.mUserData; mCancelIds = builder.mCancelIds; mSupportsInlineSuggestions = builder.mSupportsInlineSuggestions; mIconResourceId = builder.mIconResourceId; mServiceDisplayNameResourceId = builder.mServiceDisplayNameResourceId; mShowFillDialogIcon = builder.mShowFillDialogIcon; mShowSaveDialogIcon = builder.mShowSaveDialogIcon; mDetectedFieldTypes = builder.mDetectedFieldTypes; mDialogPendingIntent = builder.mDialogPendingIntent; } /** @hide */ @TestApi @NonNull public Set getDetectedFieldClassifications() { return Set.of(mDetectedFieldTypes); } /** @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 InlinePresentation getInlinePresentation() { return mInlinePresentation; } /** @hide */ public @Nullable InlinePresentation getInlineTooltipPresentation() { return mInlineTooltipPresentation; } /** @hide */ public @Nullable RemoteViews getDialogPresentation() { return mDialogPresentation; } /** @hide */ public @Nullable RemoteViews getDialogHeader() { return mDialogHeader; } /** @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[] getFillDialogTriggerIds() { return mFillDialogTriggerIds; } /** @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 */ public @DrawableRes int getIconResourceId() { return mIconResourceId; } /** @hide */ public @StringRes int getServiceDisplayNameResourceId() { return mServiceDisplayNameResourceId; } /** @hide */ public boolean getShowFillDialogIcon() { return mShowFillDialogIcon; } /** @hide */ public boolean getShowSaveDialogIcon() { return mShowSaveDialogIcon; } /** @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; } /** @hide */ @Nullable public int[] getCancelIds() { return mCancelIds; } /** @hide */ public boolean supportsInlineSuggestions() { return mSupportsInlineSuggestions; } /** * 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 InlinePresentation mInlinePresentation; private InlinePresentation mInlineTooltipPresentation; private RemoteViews mDialogPresentation; private RemoteViews mDialogHeader; private RemoteViews mHeader; private RemoteViews mFooter; private IntentSender mAuthentication; private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; private AutofillId[] mFieldClassificationIds; private AutofillId[] mFillDialogTriggerIds; private int mFlags; private boolean mDestroyed; private UserData mUserData; private int[] mCancelIds; private boolean mSupportsInlineSuggestions; private int mIconResourceId; private int mServiceDisplayNameResourceId; private boolean mShowFillDialogIcon = true; private boolean mShowSaveDialogIcon = true; private FieldClassification[] mDetectedFieldTypes; private PendingIntent mDialogPendingIntent; /** * Adds a new {@link FieldClassification} to this response, to * help the platform provide more accurate detection results. * * Call this when a field has been detected with a type. * * Altough similiarly named with {@link #setFieldClassificationIds}, * it provides a different functionality - setFieldClassificationIds should * be used when a field is only suspected to be Autofillable. * This method should be used when a field is certainly Autofillable * with a certain type. */ @NonNull public Builder setDetectedFieldClassifications( @NonNull Set fieldInfos) { throwIfDestroyed(); throwIfDisableAutofillCalled(); mDetectedFieldTypes = fieldInfos.toArray(new FieldClassification[0]); return this; } /** * Triggers a custom UI 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). * *

IMPORTANT: Extras must be non-null on the intent being set for Android 12 * otherwise it will cause a crash. Do not use {@link Activity#setResult(int)}, instead use * {@link Activity#setResult(int, Intent) with non-null extras. Consider setting { * @link android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} to null or use * {@link Bundle#EMPTY} with {@link Intent#putExtras(Bundle)} on the intent when * finishing activity to avoid crash).

* *

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 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: *

* * @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() * @deprecated Use * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} * instead. */ @Deprecated @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; } /** * Triggers a custom UI 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 method is similar to * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, but also accepts * an {@link InlinePresentation} presentation which is required for authenticating through * the inline autofill flow. * *

Note: {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does * not work with {@link InlinePresentation}.

* * @param authentication Intent to an activity with your authentication flow. * @param presentation The presentation to visualize the response. * @param inlinePresentation The inlinePresentation to visualize the response inline. * @param ids id of Views that when focused will display the authentication UI. * * @return This builder. * * @throws IllegalArgumentException if any of the following occurs: * * * @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() * @deprecated Use * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} * instead. */ @Deprecated @NonNull public Builder setAuthentication(@NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation) { return setAuthentication(ids, authentication, presentation, inlinePresentation, null); } /** * Triggers a custom UI before autofilling the screen with any data set in this * response. * *

This method like * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews, InlinePresentation)} * but allows setting an {@link InlinePresentation} for the inline suggestion tooltip. * * @deprecated Use * {@link #setAuthentication(AutofillId[], IntentSender, Presentations)} * instead. */ @Deprecated @NonNull public Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, @Nullable InlinePresentation inlineTooltipPresentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); return setAuthentication(ids, authentication, presentation, inlinePresentation, inlineTooltipPresentation, null); } /** * Triggers a custom UI 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 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. * *

Note: {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does * not work with {@link InlinePresentation}.

* * @param ids id of Views that when focused will display the authentication UI. * @param authentication Intent to an activity with your authentication flow. * @param presentations The presentations to visualize the response. * * @throws IllegalArgumentException if any of the following occurs: * * * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a * {@link #setFooter(RemoteViews) footer} are already set for this builder. * * @return This builder. */ @NonNull public Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable Presentations presentations) { throwIfDestroyed(); throwIfDisableAutofillCalled(); if (presentations == null) { return setAuthentication(ids, authentication, null, null, null, null); } return setAuthentication(ids, authentication, presentations.getMenuPresentation(), presentations.getInlinePresentation(), presentations.getInlineTooltipPresentation(), presentations.getDialogPresentation()); } /** * Triggers a custom UI before autofilling the screen with any data set in this * response. */ @NonNull private Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, @Nullable IntentSender authentication, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, @Nullable InlinePresentation inlineTooltipPresentation, @Nullable RemoteViews dialogPresentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); if (mHeader != null || mFooter != null) { throw new IllegalStateException("Already called #setHeader() or #setFooter()"); } if (authentication == null ^ (presentation == null && inlinePresentation == null)) { throw new IllegalArgumentException("authentication and presentation " + "(dropdown or inline), must be both non-null or null"); } mAuthentication = authentication; mPresentation = presentation; mInlinePresentation = inlinePresentation; mInlineTooltipPresentation = inlineTooltipPresentation; mDialogPresentation = dialogPresentation; 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; } /** * @hide */ @NonNull public Builder setDatasets(ArrayList dataset) { mDatasets = dataset; 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 | FLAG_DELAY_FILL | FLAG_CREDENTIAL_MANAGER_RESPONSE); 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 "denylisted" * 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, Presentations)}, * {@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; } /** * Overwrites Save/Fill dialog header icon with a specific one specified by resource id. * The image is pulled from the package, so id should be defined in the manifest. * * @param id {@link android.graphics.drawable.Drawable} resource id of the icon to be used. * A value of 0 indicates to use the default header icon. * * @return this builder */ @NonNull public Builder setIconResourceId(@DrawableRes int id) { throwIfDestroyed(); mIconResourceId = id; return this; } /** * Overrides the service name in the Save Dialog header with a specific string defined * in the service provider's manifest.xml * * @param id Resoure Id of the custom string defined in the provider's manifest. If set * to 0, the default name will be used. * * @return this builder */ @NonNull public Builder setServiceDisplayNameResourceId(@StringRes int id) { throwIfDestroyed(); mServiceDisplayNameResourceId = id; return this; } /** * Whether or not to show the Autofill provider icon inside of the Fill Dialog * * @param show True to show, false to hide. Defaults to true. * * @return this builder */ @NonNull public Builder setShowFillDialogIcon(boolean show) { throwIfDestroyed(); mShowFillDialogIcon = show; return this; } /** * Whether or not to show the Autofill provider icon inside of the Save Dialog * * @param show True to show, false to hide. Defaults to true. * * @return this builder */ @NonNull public Builder setShowSaveDialogIcon(boolean show) { throwIfDestroyed(); mShowSaveDialogIcon = show; 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, Presentations) * 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 = Objects.requireNonNull(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, Presentations) * requires authentication}. */ // TODO(b/69796626): make it sticky / update javadoc @NonNull public Builder setFooter(@NonNull RemoteViews footer) { throwIfDestroyed(); throwIfAuthenticationCalled(); mFooter = Objects.requireNonNull(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, Presentations) * requires authentication}. */ @NonNull public Builder setUserData(@NonNull UserData userData) { throwIfDestroyed(); throwIfAuthenticationCalled(); mUserData = Objects.requireNonNull(userData); return this; } /** * Sets target resource IDs of the child view in {@link RemoteViews Presentation Template} * which will cancel the session when clicked. * Those targets will be respectively applied to a child of the header, footer and * each {@link Dataset}. * * @param ids array of the resource id. Empty list or non-existing id has no effect. * * @return this builder * * @throws IllegalStateException if {@link #build()} was already called. */ @NonNull public Builder setPresentationCancelIds(@Nullable int[] ids) { throwIfDestroyed(); mCancelIds = ids; return this; } /** * Sets the presentation of header in fill dialog UI. The header should have * a prompt for what datasets are shown in the dialog. If this is not set, * the dialog only shows your application icon. * * More details about the fill dialog, see * fill dialog UI */ @NonNull public Builder setDialogHeader(@NonNull RemoteViews header) { throwIfDestroyed(); Objects.requireNonNull(header); mDialogHeader = header; return this; } /** * Sets which fields are used for the fill dialog UI. * * More details about the fill dialog, see * fill dialog UI * * @throws IllegalStateException if {@link #build()} was already called. * @throws NullPointerException if {@code ids} or any element on it is {@code null}. */ @NonNull public Builder setFillDialogTriggerIds(@NonNull AutofillId... ids) { throwIfDestroyed(); Preconditions.checkArrayElementsNotNull(ids, "ids"); mFillDialogTriggerIds = ids; return this; } /** * Sets credential dialog pending intent. Framework will use the intent to launch the * selector UI. A replacement for previous fill bottom sheet. * * @throws IllegalStateException if {@link #build()} was already called. * @throws NullPointerException if {@code pendingIntent} is {@code null}. * * @hide */ @NonNull public Builder setDialogPendingIntent(@NonNull PendingIntent pendingIntent) { throwIfDestroyed(); Preconditions.checkNotNull(pendingIntent, "can't pass a null object to setDialogPendingIntent"); mDialogPendingIntent = pendingIntent; 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, Presentations)}, * {@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"); } if (mDatasets != null) { for (final Dataset dataset : mDatasets) { if (dataset.getFieldInlinePresentation(0) != null) { mSupportsInlineSuggestions = true; break; } } } else if (mInlinePresentation != null) { mSupportsInlineSuggestions = true; } 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 (mInlinePresentation != null) { builder.append(", hasInlinePresentation"); } if (mInlineTooltipPresentation != null) { builder.append(", hasInlineTooltipPresentation"); } if (mDialogPresentation != null) { builder.append(", hasDialogPresentation"); } if (mDialogHeader != null) { builder.append(", hasDialogHeader"); } if (mHeader != null) { builder.append(", hasHeader"); } if (mFooter != null) { builder.append(", hasFooter"); } if (mAuthentication != null) { builder.append(", hasAuthentication"); } if (mDialogPendingIntent != null) { builder.append(", hasDialogPendingIntent"); } if (mAuthenticationIds != null) { builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)); } if (mFillDialogTriggerIds != null) { builder.append(", fillDialogTriggerIds=") .append(Arrays.toString(mFillDialogTriggerIds)); } 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); } if (mCancelIds != null) { builder.append(", mCancelIds=").append(mCancelIds.length); } builder.append(", mSupportInlinePresentations=").append(mSupportsInlineSuggestions); 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(mInlinePresentation, flags); parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeParcelable(mDialogPresentation, flags); parcel.writeParcelable(mDialogHeader, flags); parcel.writeParcelable(mDialogPendingIntent, flags); parcel.writeParcelableArray(mFillDialogTriggerIds, 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.writeParcelableArray(mDetectedFieldTypes, flags); parcel.writeInt(mIconResourceId); parcel.writeInt(mServiceDisplayNameResourceId); parcel.writeBoolean(mShowFillDialogIcon); parcel.writeBoolean(mShowSaveDialogIcon); parcel.writeInt(mFlags); parcel.writeIntArray(mCancelIds); 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, android.content.pm.ParceledListSlice.class); 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, android.service.autofill.SaveInfo.class)); builder.setClientState(parcel.readParcelable(null, android.os.Bundle.class)); // Sets authentication state. final AutofillId[] authenticationIds = parcel.readParcelableArray(null, AutofillId.class); final IntentSender authentication = parcel.readParcelable(null, android.content.IntentSender.class); final RemoteViews presentation = parcel.readParcelable(null, android.widget.RemoteViews.class); final InlinePresentation inlinePresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); final InlinePresentation inlineTooltipPresentation = parcel.readParcelable(null, android.service.autofill.InlinePresentation.class); final RemoteViews dialogPresentation = parcel.readParcelable(null, android.widget.RemoteViews.class); if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation, inlinePresentation, inlineTooltipPresentation, dialogPresentation); } final RemoteViews dialogHeader = parcel.readParcelable(null, android.widget.RemoteViews.class); if (dialogHeader != null) { builder.setDialogHeader(dialogHeader); } final PendingIntent dialogPendingIntent = parcel.readParcelable(null, PendingIntent.class); if (dialogPendingIntent != null) { builder.setDialogPendingIntent(dialogPendingIntent); } final AutofillId[] triggerIds = parcel.readParcelableArray(null, AutofillId.class); if (triggerIds != null) { builder.setFillDialogTriggerIds(triggerIds); } final RemoteViews header = parcel.readParcelable(null, android.widget.RemoteViews.class); if (header != null) { builder.setHeader(header); } final RemoteViews footer = parcel.readParcelable(null, android.widget.RemoteViews.class); if (footer != null) { builder.setFooter(footer); } final UserData userData = parcel.readParcelable(null, android.service.autofill.UserData.class); 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); } final FieldClassification[] detectedFields = parcel.readParcelableArray(null, FieldClassification.class); if (detectedFields != null) { builder.setDetectedFieldClassifications(Set.of(detectedFields)); } builder.setIconResourceId(parcel.readInt()); builder.setServiceDisplayNameResourceId(parcel.readInt()); builder.setShowFillDialogIcon(parcel.readBoolean()); builder.setShowSaveDialogIcon(parcel.readBoolean()); builder.setFlags(parcel.readInt()); final int[] cancelIds = parcel.createIntArray(); builder.setPresentationCancelIds(cancelIds); final FillResponse response = builder.build(); response.setRequestId(parcel.readInt()); return response; } @Override public FillResponse[] newArray(int size) { return new FillResponse[size]; } }; }