| /* |
| * 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)}. |
| * |
| * <p>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<Dataset> mDatasets; |
| private final @Nullable SaveInfo mSaveInfo; |
| private final @Nullable Bundle mClientState; |
| private final @Nullable RemoteViews mPresentation; |
| private final @Nullable InlinePresentation mInlinePresentation; |
| 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 final @Nullable int[] mCancelIds; |
| private final boolean mSupportsInlineSuggestions; |
| // TODO(b/149240554): revert back to use ParceledListSlice after the bug is resolved. |
| private final @Nullable ArrayList<InlinePresentation> mInlineActions; |
| |
| 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; |
| 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; |
| mCancelIds = builder.mCancelIds; |
| mSupportsInlineSuggestions = builder.mSupportsInlineSuggestions; |
| mInlineActions = builder.mInlineActions; |
| } |
| |
| /** @hide */ |
| public @Nullable Bundle getClientState() { |
| return mClientState; |
| } |
| |
| /** @hide */ |
| public @Nullable List<Dataset> 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 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. |
| * |
| * <p>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; |
| } |
| |
| /** @hide */ |
| public @Nullable List<InlinePresentation> getInlineActions() { |
| return mInlineActions; |
| } |
| |
| /** |
| * 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<Dataset> mDatasets; |
| private SaveInfo mSaveInfo; |
| private Bundle mClientState; |
| private RemoteViews mPresentation; |
| private InlinePresentation mInlinePresentation; |
| 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; |
| private int[] mCancelIds; |
| private boolean mSupportsInlineSuggestions; |
| private ArrayList<InlinePresentation> mInlineActions; |
| |
| /** |
| * Triggers a custom UI before before autofilling the screen with any data set in this |
| * response. |
| * |
| * <p><b>Note:</b> 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. |
| * |
| * <p>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. |
| * |
| * <p>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). |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p><b>Note:</b> 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. |
| * |
| * <p>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: |
| * <ul> |
| * <li>{@code ids} is {@code null}</li> |
| * <li>{@code ids} is empty</li> |
| * <li>{@code ids} contains a {@code null} element</li> |
| * <li>both {@code authentication} and {@code presentation} are {@code null}</li> |
| * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li> |
| * </ul> |
| * |
| * @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; |
| } |
| |
| /** |
| * Triggers a custom UI before before autofilling the screen with any data set in this |
| * response. |
| * |
| * <p><b>Note:</b> 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. |
| * |
| * <p>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. |
| * |
| * <p><b>Note:</b> {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does |
| * not work with {@link InlinePresentation}.</p> |
| * |
| * @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: |
| * <ul> |
| * <li>{@code ids} is {@code null}</li> |
| * <li>{@code ids} is empty</li> |
| * <li>{@code ids} contains a {@code null} element</li> |
| * <li>both {@code authentication} and {@code presentation} are {@code null}</li> |
| * <li>both {@code authentication} and {@code presentation} are non-{@code null}</li> |
| * <li>both {@code authentication} and {@code inlinePresentation} are {@code null}</li> |
| * <li>both {@code authentication} and {@code inlinePresentation} are |
| * non-{@code null}</li> |
| * </ul> |
| * |
| * @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, |
| @Nullable InlinePresentation inlinePresentation) { |
| 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; |
| mAuthenticationIds = assertValid(ids); |
| return this; |
| } |
| |
| /** |
| * Specifies views that should not trigger new |
| * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, |
| * FillCallback)} requests. |
| * |
| * <p>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. |
| * |
| * <p><b>Note: </b> 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. |
| * |
| * <p>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()}. |
| * |
| * <p>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 |
| * <a href="AutofillService.html#FieldClassification">field classification</a> |
| * |
| * <p><b>Note:</b> 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. |
| * |
| * <p>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. |
| * |
| * <p>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}. |
| * |
| * <p>Autofill for the app or activity is automatically re-enabled after any of the |
| * following conditions: |
| * |
| * <ol> |
| * <li>{@code duration} milliseconds have passed. |
| * <li>The autofill service for the user has changed. |
| * <li>The device has rebooted. |
| * </ol> |
| * |
| * <p><b>Note:</b> 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. |
| * |
| * <p>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). |
| * |
| * <p>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. |
| * |
| * <p>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). |
| * |
| * <p>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. |
| * |
| * <p>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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Adds a new {@link InlinePresentation} to this response representing an action UI. |
| * |
| * <p> For example, the UI can be associated with an intent which can open an activity for |
| * the user to manage the Autofill provider settings. |
| * |
| * @return This builder. |
| */ |
| @NonNull |
| public Builder addInlineAction(@NonNull InlinePresentation inlineAction) { |
| throwIfDestroyed(); |
| throwIfAuthenticationCalled(); |
| if (mInlineActions == null) { |
| mInlineActions = new ArrayList<>(); |
| } |
| mInlineActions.add(inlineAction); |
| return this; |
| } |
| |
| /** |
| * Builds a new {@link FillResponse} instance. |
| * |
| * @throws IllegalStateException if any of the following conditions occur: |
| * <ol> |
| * <li>{@link #build()} was already called. |
| * <li>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...)}. |
| * <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called |
| * without any previous calls to {@link #addDataset(Dataset)}. |
| * </ol> |
| * |
| * @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 (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); |
| } |
| if (mCancelIds != null) { |
| builder.append(", mCancelIds=").append(mCancelIds.length); |
| } |
| builder.append(", mSupportInlinePresentations=").append(mSupportsInlineSuggestions); |
| if (mInlineActions != null) { |
| builder.append(", mInlineActions=" + mInlineActions); |
| } |
| 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(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.writeIntArray(mCancelIds); |
| parcel.writeTypedList(mInlineActions, flags); |
| parcel.writeInt(mRequestId); |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<FillResponse> CREATOR = |
| new Parcelable.Creator<FillResponse>() { |
| @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<Dataset> datasetSlice = parcel.readParcelable(null); |
| final List<Dataset> 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); |
| final InlinePresentation inlinePresentation = parcel.readParcelable(null); |
| if (authenticationIds != null) { |
| builder.setAuthentication(authenticationIds, authentication, presentation, |
| inlinePresentation); |
| } |
| 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 int[] cancelIds = parcel.createIntArray(); |
| builder.setPresentationCancelIds(cancelIds); |
| |
| final List<InlinePresentation> inlineActions = parcel.createTypedArrayList( |
| InlinePresentation.CREATOR); |
| if (inlineActions != null) { |
| for (InlinePresentation inlineAction : inlineActions) { |
| builder.addInlineAction(inlineAction); |
| } |
| } |
| |
| final FillResponse response = builder.build(); |
| response.setRequestId(parcel.readInt()); |
| |
| return response; |
| } |
| |
| @Override |
| public FillResponse[] newArray(int size) { |
| return new FillResponse[size]; |
| } |
| }; |
| } |