diff --git a/api/current.txt b/api/current.txt index 93cc84362fd50..3ca021e8381ed 100644 --- a/api/current.txt +++ b/api/current.txt @@ -37251,6 +37251,7 @@ package android.service.autofill { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int FLAG_DONT_SAVE_ON_FINISH = 2; // 0x2 field public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 1; // 0x1 field public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; // 0x0 field public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; // 0x1 @@ -37272,6 +37273,7 @@ package android.service.autofill { method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setTriggerId(android.view.autofill.AutofillId); method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } diff --git a/api/system-current.txt b/api/system-current.txt index 9767e441453bd..5c3e1c9d8ea72 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -40346,6 +40346,7 @@ package android.service.autofill { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int FLAG_DONT_SAVE_ON_FINISH = 2; // 0x2 field public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 1; // 0x1 field public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; // 0x0 field public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; // 0x1 @@ -40367,6 +40368,7 @@ package android.service.autofill { method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setTriggerId(android.view.autofill.AutofillId); method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } diff --git a/api/test-current.txt b/api/test-current.txt index 702ec3bacf057..132a787d4799b 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -37546,6 +37546,7 @@ package android.service.autofill { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int FLAG_DONT_SAVE_ON_FINISH = 2; // 0x2 field public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 1; // 0x1 field public static final int NEGATIVE_BUTTON_STYLE_CANCEL = 0; // 0x0 field public static final int NEGATIVE_BUTTON_STYLE_REJECT = 1; // 0x1 @@ -37567,6 +37568,7 @@ package android.service.autofill { method public android.service.autofill.SaveInfo.Builder setFlags(int); method public android.service.autofill.SaveInfo.Builder setNegativeAction(int, android.content.IntentSender); method public android.service.autofill.SaveInfo.Builder setOptionalIds(android.view.autofill.AutofillId[]); + method public android.service.autofill.SaveInfo.Builder setTriggerId(android.view.autofill.AutofillId); method public android.service.autofill.SaveInfo.Builder setValidator(android.service.autofill.Validator); } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index d988a422354e3..252959a04d4f5 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1879,7 +1879,7 @@ public class Activity extends ContextThemeWrapper if (isFinishing()) { if (mAutoFillResetNeeded) { - getAutofillManager().commit(); + getAutofillManager().onActivityFinished(); } else if (mIntent != null && mIntent.hasExtra(AutofillManager.EXTRA_RESTORE_SESSION_TOKEN)) { // Activity was launched when user tapped a link in the Autofill Save UI - since diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java index 9a25f5be9b023..953501c7b9ab5 100644 --- a/core/java/android/service/autofill/AutofillService.java +++ b/core/java/android/service/autofill/AutofillService.java @@ -65,7 +65,7 @@ import com.android.internal.os.SomeArgs; *
  • The service replies through {@link FillCallback#onSuccess(FillResponse)}. *
  • The Android System calls {@link #onDisconnected()} and unbinds from the * {@code AutofillService}. - *
  • The Android System displays an UI affordance with the options sent by the service. + *
  • The Android System displays an autofill UI with the options sent by the service. *
  • The user picks an option. *
  • The proper views are autofilled. * diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index 1b9240cc09432..fde2416f053e4 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -68,7 +68,7 @@ import java.util.Arrays; * .build(); * * - *

    The save type flags are used to display the appropriate strings in the save UI affordance. + *

    The save type flags are used to display the appropriate strings in the autofill save UI. * You can pass multiple values, but try to keep it short if possible. In the above example, just * {@code SaveInfo.SAVE_DATA_TYPE_PASSWORD} would be enough. * @@ -103,13 +103,17 @@ import java.util.Arrays; * .build(); * * + * + *

    Triggering a save request

    + * *

    The {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} can be triggered after * any of the following events: *

      *
    • The {@link Activity} finishes. - *
    • The app explicitly called {@link AutofillManager#commit()}. - *
    • All required views became invisible (if the {@link SaveInfo} was created with the + *
    • The app explicitly calls {@link AutofillManager#commit()}. + *
    • All required views become invisible (if the {@link SaveInfo} was created with the * {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} flag). + *
    • The user clicks a specific view (defined by {@link Builder#setTriggerId(AutofillId)}. *
    * *

    But it is only triggered when all conditions below are met: @@ -123,10 +127,13 @@ import java.util.Arrays; *

  • There is no {@link Dataset} in the last {@link FillResponse} that completely matches the * screen state (i.e., all required and optional fields in the dataset have the same value as * the fields in the screen). - *
  • The user explicitly tapped the UI affordance asking to save data for autofill. + *
  • The user explicitly tapped the autofill save UI asking to save data for autofill. * * - *

    The service can also customize some aspects of the save UI affordance: + * + *

    Customizing the autofill save UI

    + * + *

    The service can also customize some aspects of the autofill save UI: *

      *
    • Add a simple subtitle by calling {@link Builder#setDescription(CharSequence)}. *
    • Add a customized subtitle by calling @@ -212,16 +219,25 @@ public final class SaveInfo implements Parcelable { @interface SaveDataType{} /** - * Usually {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} - * is called once the {@link Activity} finishes. If this flag is set it is called once all - * saved views become invisible. + * Usually, a save request is only automatically triggered + * once the {@link Activity} finishes. If this flag is set, it is triggered once all saved views + * become invisible. */ public static final int FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE = 0x1; + /** + * By default, a save request is automatically triggered + * once the {@link Activity} finishes. If this flag is set, finishing the activity doesn't + * trigger a save request. + * + *

      This flag is typically used in conjunction with {@link Builder#setTriggerId(AutofillId)}. + */ + public static final int FLAG_DONT_SAVE_ON_FINISH = 0x2; + /** @hide */ @IntDef( flag = true, - value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}) + value = {FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE, FLAG_DONT_SAVE_ON_FINISH}) @Retention(RetentionPolicy.SOURCE) @interface SaveInfoFlags{} @@ -236,6 +252,7 @@ public final class SaveInfo implements Parcelable { private final InternalValidator mValidator; private final InternalSanitizer[] mSanitizerKeys; private final AutofillId[][] mSanitizerValues; + private final AutofillId mTriggerId; private SaveInfo(Builder builder) { mType = builder.mType; @@ -259,6 +276,7 @@ public final class SaveInfo implements Parcelable { mSanitizerValues[i] = builder.mSanitizers.valueAt(i); } } + mTriggerId = builder.mTriggerId; } /** @hide */ @@ -320,6 +338,12 @@ public final class SaveInfo implements Parcelable { return mSanitizerValues; } + /** @hide */ + @Nullable + public AutofillId getTriggerId() { + return mTriggerId; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -338,6 +362,7 @@ public final class SaveInfo implements Parcelable { private ArrayMap mSanitizers; // Set used to validate against duplicate ids. private ArraySet mSanitizerIds; + private AutofillId mTriggerId; /** * Creates a new builder. @@ -394,13 +419,15 @@ public final class SaveInfo implements Parcelable { /** * Sets flags changing the save behavior. * - * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE} or {@code 0}. + * @param flags {@link #FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE}, + * {@link #FLAG_DONT_SAVE_ON_FINISH}, or {@code 0}. * @return This builder. */ public @NonNull Builder setFlags(@SaveInfoFlags int flags) { throwIfDestroyed(); - mFlags = Preconditions.checkFlagsArgument(flags, FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE); + mFlags = Preconditions.checkFlagsArgument(flags, + FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE | FLAG_DONT_SAVE_ON_FINISH); return this; } @@ -493,8 +520,8 @@ public final class SaveInfo implements Parcelable { } /** - * Sets an object used to validate the user input - if the input is not valid, the Save UI - * affordance is not shown. + * Sets an object used to validate the user input - if the input is not valid, the + * autofill save UI is not shown. * *

      Typically used to validate credit card numbers. Examples: * @@ -520,7 +547,7 @@ public final class SaveInfo implements Parcelable { * ); * * - *

      NOTE: the example above is just for illustrative purposes; the same validator + *

      Note: the example above is just for illustrative purposes; the same validator * could be created using a single regex for the {@code OR} part: * *

      @@ -615,6 +642,27 @@ public final class SaveInfo implements Parcelable {
                   return this;
               }
       
      +       /**
      +         * Explicitly defines the view that should commit the autofill context when clicked.
      +         *
      +         * 

      Usually, the save request is only automatically + * triggered after the activity is + * finished or all relevant views become invisible, but there are scenarios where the + * autofill context is automatically commited too late + * —for example, when the activity manually clears the autofillable views when a + * button is tapped. This method can be used to trigger the autofill save UI earlier in + * these scenarios. + * + *

      Note: This method should only be used in scenarios where the automatic workflow + * is not enough, otherwise it could trigger the autofill save UI when it should not— + * for example, when the user entered invalid credentials for the autofillable views. + */ + public @NonNull Builder setTriggerId(@NonNull AutofillId id) { + throwIfDestroyed(); + mTriggerId = Preconditions.checkNotNull(id); + return this; + } + /** * Builds a new {@link SaveInfo} instance. * @@ -652,13 +700,14 @@ public final class SaveInfo implements Parcelable { .append(", description=").append(mDescription) .append(DebugUtils.flagsToString(SaveInfo.class, "NEGATIVE_BUTTON_STYLE_", mNegativeButtonStyle)) - .append(", mFlags=").append(mFlags) - .append(", mCustomDescription=").append(mCustomDescription) - .append(", validation=").append(mValidator) + .append(", flags=").append(mFlags) + .append(", customDescription=").append(mCustomDescription) + .append(", validator=").append(mValidator) .append(", sanitizerKeys=") .append(mSanitizerKeys == null ? "N/A:" : mSanitizerKeys.length) .append(", sanitizerValues=") .append(mSanitizerValues == null ? "N/A:" : mSanitizerValues.length) + .append(", triggerId=").append(mTriggerId) .append("]").toString(); } @@ -687,6 +736,7 @@ public final class SaveInfo implements Parcelable { parcel.writeParcelableArray(mSanitizerValues[i], flags); } } + parcel.writeParcelable(mTriggerId, flags); parcel.writeInt(mFlags); } @@ -727,6 +777,10 @@ public final class SaveInfo implements Parcelable { builder.addSanitizer(sanitizers[i], autofillIds); } } + final AutofillId triggerId = parcel.readParcelable(null); + if (triggerId != null) { + builder.setTriggerId(triggerId); + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0d1258d6af3f0..2ee83bc7daef4 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -6397,6 +6397,60 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } + /** + * Set by {@link AutofillManager} if it needs to be notified when this view is clicked. + */ + private boolean mNotifyAutofillManagerOnClick; + + /** + * Temporary variable used to make sure the autofill manager is not called twice on + * {@link #performClickInternal()}. + */ + private boolean mAlreadyNotifiedAutofillManagerOnClick; + + /** @hide */ + public void setNotifyAutofillManagerOnClick(boolean notify) { + mNotifyAutofillManagerOnClick = notify; + } + + private void notifyAutofillManagerOnClick() { + if (!mNotifyAutofillManagerOnClick || mAlreadyNotifiedAutofillManagerOnClick) { + return; + } + // Must notify manager first to avoid scenarios where app has a listener + // that changes the state of views the autofill service might be interested on. + try { + getAutofillManager().notifyViewClicked(this); + } finally { + // Set it to already called so it's not called twice when + mAlreadyNotifiedAutofillManagerOnClick = true; + } + } + + /** + * Entry point for {@link #performClick()} - other methods on View should call it instead of + * {@code performClick()} directly to make sure the autofill manager is notified when + * necessary (as subclasses could extend {@code performClick()} without calling the parent's + * method). + */ + private boolean performClickInternal() { + mAlreadyNotifiedAutofillManagerOnClick = false; + + // Must notify autofill manager before performing the click actions to avoid scenarios where + // the app has a click listener that changes the state of views the autofill service might + // be interested on. + notifyAutofillManagerOnClick(); + + boolean performed; + try { + performed = performClick(); + } finally { + // Reset it for next call. + mAlreadyNotifiedAutofillManagerOnClick = false; + } + return performed; + } + /** * Call this view's OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing @@ -6405,7 +6459,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ + // NOTE: other methods on View should not call this method directly, but performClickInternal() + // instead, to guarantee that the autofill manager is notified when necessary (as subclasses + // could extend this method without calling super.performClick()). public boolean performClick() { + try { + // We still need to call this method to handle the cases where performClick() was called + // externally, instead of through performClickInternal() + notifyAutofillManagerOnClick(); + } finally { + // Reset it for next call. + mAlreadyNotifiedAutofillManagerOnClick = false; + } + final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { @@ -11503,7 +11569,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (isClickable()) { - performClick(); + performClickInternal(); return true; } } break; @@ -12615,7 +12681,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // This is a tap, so remove the longpress check removeLongPressCallback(); if (!event.isCanceled()) { - return performClick(); + return performClickInternal(); } } } @@ -13187,7 +13253,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { - performClick(); + performClickInternal(); } } } @@ -18228,10 +18294,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @SuppressWarnings({"UnusedDeclaration"}) public void outputDirtyFlags(String indent, boolean clear, int clearMask) { - Log.d("View", indent + this + " DIRTY(" + (mPrivateFlags & View.PFLAG_DIRTY_MASK) + - ") DRAWN(" + (mPrivateFlags & PFLAG_DRAWN) + ")" + " CACHE_VALID(" + - (mPrivateFlags & View.PFLAG_DRAWING_CACHE_VALID) + - ") INVALIDATED(" + (mPrivateFlags & PFLAG_INVALIDATED) + ")"); + Log.d(VIEW_LOG_TAG, indent + this + " DIRTY(" + + (mPrivateFlags & View.PFLAG_DIRTY_MASK) + + ") DRAWN(" + (mPrivateFlags & PFLAG_DRAWN) + ")" + " CACHE_VALID(" + + (mPrivateFlags & View.PFLAG_DRAWING_CACHE_VALID) + + ") INVALIDATED(" + (mPrivateFlags & PFLAG_INVALIDATED) + ")"); if (clear) { mPrivateFlags &= clearMask; } @@ -20008,7 +20075,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean changed = false; if (DBG) { - Log.d("View", this + " View.setFrame(" + left + "," + top + "," + Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } @@ -25054,7 +25121,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private final class PerformClick implements Runnable { @Override public void run() { - performClick(); + performClickInternal(); } } diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 4fb2a99af5754..867bbd9f3b8b4 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -91,10 +91,10 @@ import java.util.Objects; *

    * *

    When the service returns datasets, the Android System displays an autofill dataset picker - * UI affordance associated with the view, when the view is focused on and is part of a dataset. - * The application can be notified when the affordance is shown by registering an + * UI associated with the view, when the view is focused on and is part of a dataset. + * The application can be notified when the UI is shown by registering an * {@link AutofillCallback} through {@link #registerCallback(AutofillCallback)}. When the user - * selects a dataset from the affordance, all views present in the dataset are autofilled, through + * selects a dataset from the UI, all views present in the dataset are autofilled, through * calls to {@link View#autofill(AutofillValue)} or {@link View#autofill(SparseArray)}. * *

    When the service returns ids of savable views, the Android System keeps track of changes @@ -108,7 +108,7 @@ import java.util.Objects; * * *

    Finally, after the autofill context is commited (i.e., not cancelled), the Android System - * shows a save UI affordance if the value of savable views have changed. If the user selects the + * shows an autofill save UI if the value of savable views have changed. If the user selects the * option to Save, the current value of the views is then sent to the autofill service. * *

    It is safe to call into its methods from any thread. @@ -311,6 +311,14 @@ public final class AutofillManager { @GuardedBy("mLock") @Nullable private ArraySet mFillableIds; + /** If set, session is commited when the field is clicked. */ + @GuardedBy("mLock") + @Nullable private AutofillId mSaveTriggerId; + + /** If set, session is commited when the activity is finished; otherwise session is canceled. */ + @GuardedBy("mLock") + private boolean mSaveOnFinish; + /** @hide */ public interface AutofillClient { /** @@ -834,6 +842,46 @@ public final class AutofillManager { } } + + /** + * Called when a {@link View} is clicked. Currently only used by views that should trigger save. + * + * @hide + */ + public void notifyViewClicked(View view) { + final AutofillId id = view.getAutofillId(); + + if (sVerbose) Log.v(TAG, "notifyViewClicked(): id=" + id + ", trigger=" + mSaveTriggerId); + + synchronized (mLock) { + if (mSaveTriggerId != null && mSaveTriggerId.equals(id)) { + if (sDebug) Log.d(TAG, "triggering commit by click of " + id); + commitLocked(); + mMetricsLogger.action(MetricsEvent.AUTOFILL_SAVE_EXPLICITLY_TRIGGERED, + mContext.getPackageName()); + } + } + } + + /** + * Called by {@link android.app.Activity} to commit or cancel the session on finish. + * + * @hide + */ + public void onActivityFinished() { + if (!hasAutofillFeature()) { + return; + } + synchronized (mLock) { + if (mSaveOnFinish) { + commitLocked(); + } else { + if (sDebug) Log.d(TAG, "Cancelling session on finish() as requested by service"); + cancelLocked(); + } + } + } + /** * Called to indicate the current autofill context should be commited. * @@ -850,14 +898,17 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } - - finishSessionLocked(); + commitLocked(); } } + private void commitLocked() { + if (!mEnabled && !isActiveLocked()) { + return; + } + finishSessionLocked(); + } + /** * Called to indicate the current autofill context should be cancelled. * @@ -874,14 +925,17 @@ public final class AutofillManager { return; } synchronized (mLock) { - if (!mEnabled && !isActiveLocked()) { - return; - } - - cancelSessionLocked(); + cancelLocked(); } } + private void cancelLocked() { + if (!mEnabled && !isActiveLocked()) { + return; + } + cancelSessionLocked(); + } + /** @hide */ public void disableOwnedAutofillServices() { disableAutofillServices(); @@ -1038,6 +1092,7 @@ public final class AutofillManager { mState = STATE_UNKNOWN; mTrackedViews = null; mFillableIds = null; + mSaveTriggerId = null; } private void updateSessionLocked(AutofillId id, Rect bounds, AutofillValue value, int action, @@ -1289,12 +1344,15 @@ public final class AutofillManager { /** * Set the tracked views. * - * @param trackedIds The views to be tracked + * @param trackedIds The views to be tracked. * @param saveOnAllViewsInvisible Finish the session once all tracked views are invisible. + * @param saveOnFinish Finish the session once the activity is finished. * @param fillableIds Views that might anchor FillUI. + * @param saveTriggerId View that when clicked triggers commit(). */ private void setTrackedViews(int sessionId, @Nullable AutofillId[] trackedIds, - boolean saveOnAllViewsInvisible, @Nullable AutofillId[] fillableIds) { + boolean saveOnAllViewsInvisible, boolean saveOnFinish, + @Nullable AutofillId[] fillableIds, @Nullable AutofillId saveTriggerId) { synchronized (mLock) { if (mEnabled && mSessionId == sessionId) { if (saveOnAllViewsInvisible) { @@ -1302,6 +1360,7 @@ public final class AutofillManager { } else { mTrackedViews = null; } + mSaveOnFinish = saveOnFinish; if (fillableIds != null) { if (mFillableIds == null) { mFillableIds = new ArraySet<>(fillableIds.length); @@ -1314,10 +1373,30 @@ public final class AutofillManager { + ", mFillableIds" + mFillableIds); } } + + if (mSaveTriggerId != null && !mSaveTriggerId.equals(saveTriggerId)) { + // Turn off trigger on previous view id. + setNotifyOnClickLocked(mSaveTriggerId, false); + } + + if (saveTriggerId != null && !saveTriggerId.equals(mSaveTriggerId)) { + // Turn on trigger on new view id. + mSaveTriggerId = saveTriggerId; + setNotifyOnClickLocked(mSaveTriggerId, true); + } } } } + private void setNotifyOnClickLocked(@NonNull AutofillId id, boolean notify) { + final View view = findView(id); + if (view == null) { + Log.w(TAG, "setNotifyOnClick(): invalid id: " + id); + return; + } + view.setNotifyAutofillManagerOnClick(notify); + } + private void setSaveUiState(int sessionId, boolean shown) { if (sDebug) Log.d(TAG, "setSaveUiState(" + sessionId + "): " + shown); synchronized (mLock) { @@ -1504,6 +1583,8 @@ public final class AutofillManager { pw.print(pfx2); pw.print("invisible:"); pw.println(mTrackedViews.mInvisibleTrackedIds); } pw.print(pfx); pw.print("fillable ids: "); pw.println(mFillableIds); + pw.print(pfx); pw.print("save trigger id: "); pw.println(mSaveTriggerId); + pw.print(pfx); pw.print("save on finish(): "); pw.println(mSaveOnFinish); } private String getStateAsStringLocked() { @@ -1752,7 +1833,7 @@ public final class AutofillManager { * Callback for autofill related events. * *

    Typically used for applications that display their own "auto-complete" views, so they can - * enable / disable such views when the autofill UI affordance is shown / hidden. + * enable / disable such views when the autofill UI is shown / hidden. */ public abstract static class AutofillCallback { @@ -1762,26 +1843,26 @@ public final class AutofillManager { public @interface AutofillEventType {} /** - * The autofill input UI affordance associated with the view was shown. + * The autofill input UI associated with the view was shown. * - *

    If the view provides its own auto-complete UI affordance and its currently shown, it + *

    If the view provides its own auto-complete UI and its currently shown, it * should be hidden upon receiving this event. */ public static final int EVENT_INPUT_SHOWN = 1; /** - * The autofill input UI affordance associated with the view was hidden. + * The autofill input UI associated with the view was hidden. * - *

    If the view provides its own auto-complete UI affordance that was hidden upon a + *

    If the view provides its own auto-complete UI that was hidden upon a * {@link #EVENT_INPUT_SHOWN} event, it could be shown again now. */ public static final int EVENT_INPUT_HIDDEN = 2; /** - * The autofill input UI affordance associated with the view isn't shown because + * The autofill input UI associated with the view isn't shown because * autofill is not available. * - *

    If the view provides its own auto-complete UI affordance but was not displaying it + *

    If the view provides its own auto-complete UI but was not displaying it * to avoid flickering, it could shown it upon receiving this event. */ public static final int EVENT_INPUT_UNAVAILABLE = 3; @@ -1883,12 +1964,12 @@ public final class AutofillManager { @Override public void setTrackedViews(int sessionId, AutofillId[] ids, - boolean saveOnAllViewsInvisible, AutofillId[] fillableIds) { + boolean saveOnAllViewsInvisible, boolean saveOnFinish, AutofillId[] fillableIds, + AutofillId saveTriggerId) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> - afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, fillableIds) - ); + afm.post(() -> afm.setTrackedViews(sessionId, ids, saveOnAllViewsInvisible, + saveOnFinish, fillableIds, saveTriggerId)); } } diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 3dabcec8636a0..56a22c22f4c54 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -53,7 +53,8 @@ oneway interface IAutoFillManagerClient { * the session is finished automatically. */ void setTrackedViews(int sessionId, in @nullable AutofillId[] savableIds, - boolean saveOnAllViewsInvisible, in @nullable AutofillId[] fillableIds); + boolean saveOnAllViewsInvisible, boolean saveOnFinish, + in @nullable AutofillId[] fillableIds, in AutofillId saveTriggerId); /** * Requests showing the fill UI. diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index 9b167c82be6d5..8e88359590844 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -4679,13 +4679,25 @@ message MetricsEvent { // SUBTYPE: 0 is off, 1 is on // CATEGORY: SETTINGS // OS: P - ACTION_ZEN_ALLOW_ALARMS = 1162; + ACTION_ZEN_ALLOW_ALARMS = 1226; // ACTION: DND Settings > Priority only allows > Media toggle // SUBTYPE: 0 is off, 1 is on // CATEGORY: SETTINGS // OS: P - ACTION_ZEN_ALLOW_MEDIA = 1163; + ACTION_ZEN_ALLOW_MEDIA = 1227; + + // An autofill service explicitly defined which view should commit the autofill context + // Package: Package of app that is autofilled + // OS: P + // Tag FIELD_AUTOFILL_SERVICE: Package of service that processed the request + AUTOFILL_EXPLICIT_SAVE_TRIGGER_DEFINITION = 1228; + + // The autofill context was commited when the user clicked a view explicitly marked by the + // service as committing it + // Package: Package of app that is autofilled + // OS: P + AUTOFILL_SAVE_EXPLICITLY_TRIGGERED = 1229; // Add new aosp constants above this line. // END OF AOSP CONSTANTS diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index ed00ffed4f633..fb5d78f055573 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1491,8 +1491,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ArraySet trackedViews = null; boolean saveOnAllViewsInvisible = false; + boolean saveOnFinish = true; final SaveInfo saveInfo = response.getSaveInfo(); + final AutofillId saveTriggerId; if (saveInfo != null) { + saveTriggerId = saveInfo.getTriggerId(); + if (saveTriggerId != null) { + writeLog(MetricsEvent.AUTOFILL_EXPLICIT_SAVE_TRIGGER_DEFINITION); + } saveOnAllViewsInvisible = (saveInfo.getFlags() & SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) != 0; @@ -1509,6 +1515,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Collections.addAll(trackedViews, saveInfo.getOptionalIds()); } } + if ((saveInfo.getFlags() & SaveInfo.FLAG_DONT_SAVE_ON_FINISH) != 0) { + saveOnFinish = false; + } + + } else { + saveTriggerId = null; } // Must also track that are part of datasets, otherwise the FillUI won't be hidden when @@ -1533,10 +1545,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState try { if (sVerbose) { - Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds); + Slog.v(TAG, "updateTrackedIdsLocked(): " + trackedViews + " => " + fillableIds + + " (triggering on " + saveTriggerId + ")"); } mClient.setTrackedViews(id, toArray(trackedViews), saveOnAllViewsInvisible, - toArray(fillableIds)); + saveOnFinish, toArray(fillableIds), saveTriggerId); } catch (RemoteException e) { Slog.w(TAG, "Cannot set tracked ids", e); }