/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.autofill; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_VIEW_REQUESTS_CREDMAN_SERVICE; import static com.android.server.autofill.Helper.sDebug; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; import android.service.autofill.FillResponse; import android.util.DebugUtils; import android.util.Slog; import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import java.io.PrintWriter; /** * State for a given view with a AutofillId. * * <p>This class holds state about a view and calls its listener when the fill UI is ready to * be displayed for the view. */ final class ViewState { interface Listener { /** * Called when the fill UI is ready to be shown for this view. */ void onFillReady(@NonNull FillResponse fillResponse, @NonNull AutofillId focusedId, @Nullable AutofillValue value, int flags); } private static final String TAG = "ViewState"; /** Initial state. */ public static final int STATE_INITIAL = 0x001; /** View id is present in a dataset returned by the service. */ public static final int STATE_FILLABLE = 0x002; /** View was autofilled after user selected a dataset. */ public static final int STATE_AUTOFILLED = 0x004; /** View value was changed, but not by the service. */ public static final int STATE_CHANGED = 0x008; /** Set only in the View that started a session. */ public static final int STATE_STARTED_SESSION = 0x010; /** View that started a new partition when focused on. */ public static final int STATE_STARTED_PARTITION = 0x020; /** User select a dataset in this view, but service must authenticate first. */ public static final int STATE_WAITING_DATASET_AUTH = 0x040; /** Service does not care about this view. */ public static final int STATE_IGNORED = 0x080; /** User manually request autofill in this view, after it was already autofilled. */ public static final int STATE_RESTARTED_SESSION = 0x100; /** View is the URL bar of a package on compat mode. */ public static final int STATE_URL_BAR = 0x200; /** View was asked to autofill but failed to do so. */ public static final int STATE_AUTOFILL_FAILED = 0x400; /** View has been autofilled at least once. */ public static final int STATE_AUTOFILLED_ONCE = 0x800; /** View triggered the latest augmented autofill request. */ public static final int STATE_TRIGGERED_AUGMENTED_AUTOFILL = 0x1000; /** Inline suggestions were shown for this View. */ public static final int STATE_INLINE_SHOWN = 0x2000; /** A character was removed from the View value (not by the service). */ public static final int STATE_CHAR_REMOVED = 0x4000; /** Showing inline suggestions is not allowed for this View. */ public static final int STATE_INLINE_DISABLED = 0x8000; /** The View is waiting for an inline suggestions request from IME.*/ public static final int STATE_PENDING_CREATE_INLINE_REQUEST = 0x10000; /** Fill dialog were shown for this View. */ public static final int STATE_FILL_DIALOG_SHOWN = 0x20000; public final AutofillId id; private final Listener mListener; private final boolean mIsPrimaryCredential; /** * There are two sources of fill response. The fill response from the session's remote fill * service and the fill response from the secondary provider handler. Primary Fill Response * stores the fill response from the session's remote fill service. */ private FillResponse mPrimaryFillResponse; /** * Secondary fill response stores the fill response from the secondary provider handler. Based * on whether the user focuses on a credential view or an autofill view, the relevant fill * response will be used to show the autofill suggestions. */ private FillResponse mSecondaryFillResponse; private AutofillValue mCurrentValue; /** * Some apps clear the form before navigating to another activity. The mCandidateSaveValue * caches the value when a field with string longer than 2 characters are cleared. * * When showing save UI, if mCurrentValue of view state is empty, session would use * mCandidateSaveValue to prompt save instead. */ private AutofillValue mCandidateSaveValue; private AutofillValue mAutofilledValue; private AutofillValue mSanitizedValue; private Rect mVirtualBounds; private int mState; private String mDatasetId; ViewState(AutofillId id, Listener listener, int state, boolean isPrimaryCredential) { this.id = id; mListener = listener; mState = state; mIsPrimaryCredential = isPrimaryCredential; } /** * Gets the boundaries of the virtual view, or {@code null} if the the view is not virtual. */ @Nullable Rect getVirtualBounds() { return mVirtualBounds; } /** * Gets the current value of the view. */ @Nullable AutofillValue getCurrentValue() { return mCurrentValue; } void setCurrentValue(AutofillValue value) { mCurrentValue = value; } /** * Gets the candidate save value of the view. */ @Nullable AutofillValue getCandidateSaveValue() { return mCandidateSaveValue; } void setCandidateSaveValue(AutofillValue value) { mCandidateSaveValue = value; } @Nullable AutofillValue getAutofilledValue() { return mAutofilledValue; } void setAutofilledValue(@Nullable AutofillValue value) { mAutofilledValue = value; } @Nullable AutofillValue getSanitizedValue() { return mSanitizedValue; } void setSanitizedValue(@Nullable AutofillValue value) { mSanitizedValue = value; } @Nullable FillResponse getResponse() { return mPrimaryFillResponse; } @Nullable FillResponse getSecondaryResponse() { return mSecondaryFillResponse; } void setResponse(FillResponse response) { setResponse(response, /* isPrimary= */ true); } void setResponse(@Nullable FillResponse response, boolean isPrimary) { if (isPrimary) { mPrimaryFillResponse = response; } else { mSecondaryFillResponse = response; } } int getState() { return mState; } String getStateAsString() { return getStateAsString(mState); } static String getStateAsString(int state) { return DebugUtils.flagsToString(ViewState.class, "STATE_", state); } void setState(int state) { if (mState == STATE_INITIAL) { mState = state; } else { mState |= state; } if (state == STATE_AUTOFILLED) { mState |= STATE_AUTOFILLED_ONCE; } } void resetState(int state) { mState &= ~state; } @Nullable String getDatasetId() { return mDatasetId; } void setDatasetId(String datasetId) { mDatasetId = datasetId; } // TODO: refactor / rename / document this method (and maybeCallOnFillReady) to make it clear // that it can change the value and update the UI; similarly, should replace code that // directly sets mAutofillValue to use encapsulation. void update(@Nullable AutofillValue autofillValue, @Nullable Rect virtualBounds, int flags) { if (autofillValue != null) { mCurrentValue = autofillValue; } if (virtualBounds != null) { mVirtualBounds = virtualBounds; } maybeCallOnFillReady(flags); } /** * Calls {@link * Listener#onFillReady(FillResponse, AutofillId, AutofillValue, int)} if the * fill UI is ready to be displayed (i.e. when response and bounds are set). */ void maybeCallOnFillReady(int flags) { if ((mState & STATE_AUTOFILLED) != 0 && (flags & FLAG_MANUAL_REQUEST) == 0) { if (sDebug) Slog.d(TAG, "Ignoring UI for " + id + " on " + getStateAsString()); return; } // First try the current response associated with this View. FillResponse requestedResponse = requestingPrimaryResponse(flags) ? mPrimaryFillResponse : mSecondaryFillResponse; if (requestedResponse != null) { if (requestedResponse.getDatasets() != null || requestedResponse.getAuthentication() != null) { mListener.onFillReady(requestedResponse, this.id, mCurrentValue, flags); } } } private boolean requestingPrimaryResponse(int flags) { if (mIsPrimaryCredential) { return (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0; } else { return (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) == 0; } } @Override public String toString() { final StringBuilder builder = new StringBuilder("ViewState: [id=").append(id); if (mDatasetId != null) { builder.append(", datasetId:" ).append(mDatasetId); } builder.append(", state:").append(getStateAsString()); if (mCurrentValue != null) { builder.append(", currentValue:" ).append(mCurrentValue); } if (mCandidateSaveValue != null) { builder.append(", candidateSaveValue:").append(mCandidateSaveValue); } if (mAutofilledValue != null) { builder.append(", autofilledValue:" ).append(mAutofilledValue); } if (mSanitizedValue != null) { builder.append(", sanitizedValue:" ).append(mSanitizedValue); } if (mVirtualBounds != null) { builder.append(", virtualBounds:" ).append(mVirtualBounds); } builder.append("]"); return builder.toString(); } void dump(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("id:" ); pw.println(id); if (mDatasetId != null) { pw.print(prefix); pw.print("datasetId:" ); pw.println(mDatasetId); } pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString()); pw.print(prefix); pw.print("is primary credential:"); pw.println(mIsPrimaryCredential); if (mPrimaryFillResponse != null) { pw.print(prefix); pw.print("primary response id:"); pw.println(mPrimaryFillResponse.getRequestId()); } if (mSecondaryFillResponse != null) { pw.print(prefix); pw.print("secondary response id:"); pw.println(mSecondaryFillResponse.getRequestId()); } if (mCurrentValue != null) { pw.print(prefix); pw.print("currentValue:" ); pw.println(mCurrentValue); } if (mAutofilledValue != null) { pw.print(prefix); pw.print("autofilledValue:" ); pw.println(mAutofilledValue); } if (mCandidateSaveValue != null) { pw.print(prefix); pw.print("candidateSaveValue:"); pw.println(mCandidateSaveValue); } if (mSanitizedValue != null) { pw.print(prefix); pw.print("sanitizedValue:" ); pw.println(mSanitizedValue); } if (mVirtualBounds != null) { pw.print(prefix); pw.print("virtualBounds:" ); pw.println(mVirtualBounds); } } }