From 2901b78aaa0c1ec12763692f11923f2930ca2a5b Mon Sep 17 00:00:00 2001 From: TYM Tsai Date: Fri, 26 Feb 2021 05:09:56 +0800 Subject: [PATCH 1/2] Add interface for the inline suggestion tooltip The inline suggestion tooltip is used to show some descriptions for a specific inline suggestion to let the user easily understand the suggestion and promo the suggestion. Or educates the user how to operate the inline strip. The patch defines some APIs to enable the suggestion provider to conveniently provide tooltips for the user. Bug: 172024354 Test: atest CtsAutoFillServiceTestCases:InlineTooltipTest Change-Id: I5b0fc481b7352da2804f0e4f4633f1698a69109b --- core/api/current.txt | 9 + .../android/service/autofill/Dataset.java | 151 ++++++++++++++++- .../service/autofill/FillResponse.java | 33 +++- .../service/autofill/InlinePresentation.java | 22 ++- .../inputmethod/InlineSuggestionsRequest.java | 160 ++++++++++++++++-- 5 files changed, 357 insertions(+), 18 deletions(-) diff --git a/core/api/current.txt b/core/api/current.txt index 9f2961e1e535c..cab376a044c71 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -37387,12 +37387,15 @@ package android.service.autofill { method @NonNull public android.service.autofill.Dataset.Builder setAuthentication(@Nullable android.content.IntentSender); method @NonNull public android.service.autofill.Dataset.Builder setId(@Nullable String); method @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.Dataset.Builder setInlinePresentation(@NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.Dataset.Builder setValue(@NonNull android.view.autofill.AutofillId, @Nullable android.view.autofill.AutofillValue, @Nullable java.util.regex.Pattern, @NonNull android.widget.RemoteViews, @NonNull android.service.autofill.InlinePresentation, @NonNull android.service.autofill.InlinePresentation); } public final class DateTransformation implements android.os.Parcelable android.service.autofill.Transformation { @@ -37493,6 +37496,7 @@ package android.service.autofill { method @NonNull public android.service.autofill.FillResponse.Builder disableAutofill(long); method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews); method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation); + method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation, @Nullable android.service.autofill.InlinePresentation); method @NonNull public android.service.autofill.FillResponse.Builder setClientState(@Nullable android.os.Bundle); method @NonNull public android.service.autofill.FillResponse.Builder setFieldClassificationIds(@NonNull android.view.autofill.AutofillId...); method @NonNull public android.service.autofill.FillResponse.Builder setFlags(int); @@ -37520,6 +37524,7 @@ package android.service.autofill { public final class InlinePresentation implements android.os.Parcelable { ctor public InlinePresentation(@NonNull android.app.slice.Slice, @NonNull android.widget.inline.InlinePresentationSpec, boolean); + method @NonNull public static android.service.autofill.InlinePresentation createTooltipPresentation(@NonNull android.app.slice.Slice, @NonNull android.widget.inline.InlinePresentationSpec); method public int describeContents(); method @NonNull public android.widget.inline.InlinePresentationSpec getInlinePresentationSpec(); method @NonNull public android.app.slice.Slice getSlice(); @@ -51327,6 +51332,7 @@ package android.view.inputmethod { method @NonNull public android.os.Bundle getExtras(); method @NonNull public String getHostPackageName(); method @NonNull public java.util.List getInlinePresentationSpecs(); + method @Nullable public android.widget.inline.InlinePresentationSpec getInlineTooltipPresentationSpec(); method public int getMaxSuggestionCount(); method @NonNull public android.os.LocaleList getSupportedLocales(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -51338,9 +51344,12 @@ package android.view.inputmethod { ctor public InlineSuggestionsRequest.Builder(@NonNull java.util.List); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder addInlinePresentationSpecs(@NonNull android.widget.inline.InlinePresentationSpec); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest build(); + method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setClientSupported(boolean); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setExtras(@NonNull android.os.Bundle); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(@NonNull java.util.List); + method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setInlineTooltipPresentationSpec(@NonNull android.widget.inline.InlinePresentationSpec); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setMaxSuggestionCount(int); + method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setServiceSupported(boolean); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setSupportedLocales(@NonNull android.os.LocaleList); } diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index e3d0741b46039..6147c58867a66 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -107,10 +107,12 @@ public final class Dataset implements Parcelable { private final ArrayList mFieldValues; private final ArrayList mFieldPresentations; private final ArrayList mFieldInlinePresentations; + private final ArrayList mFieldInlineTooltipPresentations; private final ArrayList mFieldFilters; @Nullable private final ClipData mFieldContent; private final RemoteViews mPresentation; @Nullable private final InlinePresentation mInlinePresentation; + @Nullable private final InlinePresentation mInlineTooltipPresentation; private final IntentSender mAuthentication; @Nullable String mId; @@ -119,10 +121,12 @@ public final class Dataset implements Parcelable { mFieldValues = builder.mFieldValues; mFieldPresentations = builder.mFieldPresentations; mFieldInlinePresentations = builder.mFieldInlinePresentations; + mFieldInlineTooltipPresentations = builder.mFieldInlineTooltipPresentations; mFieldFilters = builder.mFieldFilters; mFieldContent = builder.mFieldContent; mPresentation = builder.mPresentation; mInlinePresentation = builder.mInlinePresentation; + mInlineTooltipPresentation = builder.mInlineTooltipPresentation; mAuthentication = builder.mAuthentication; mId = builder.mId; } @@ -153,6 +157,14 @@ public final class Dataset implements Parcelable { return inlinePresentation != null ? inlinePresentation : mInlinePresentation; } + /** @hide */ + public @Nullable InlinePresentation getFieldInlineTooltipPresentation(int index) { + final InlinePresentation inlineTooltipPresentation = + mFieldInlineTooltipPresentations.get(index); + return inlineTooltipPresentation != null + ? inlineTooltipPresentation : mInlineTooltipPresentation; + } + /** @hide */ public @Nullable DatasetFieldFilter getFilter(int index) { return mFieldFilters.get(index); @@ -209,6 +221,10 @@ public final class Dataset implements Parcelable { if (mFieldInlinePresentations != null) { builder.append(", fieldInlinePresentations=").append(mFieldInlinePresentations.size()); } + if (mFieldInlineTooltipPresentations != null) { + builder.append(", fieldInlineTooltipInlinePresentations=").append( + mFieldInlineTooltipPresentations.size()); + } if (mFieldFilters != null) { builder.append(", fieldFilters=").append(mFieldFilters.size()); } @@ -218,6 +234,9 @@ public final class Dataset implements Parcelable { if (mInlinePresentation != null) { builder.append(", hasInlinePresentation"); } + if (mInlineTooltipPresentation != null) { + builder.append(", hasInlineTooltipPresentation"); + } if (mAuthentication != null) { builder.append(", hasAuthentication"); } @@ -245,10 +264,12 @@ public final class Dataset implements Parcelable { private ArrayList mFieldValues; private ArrayList mFieldPresentations; private ArrayList mFieldInlinePresentations; + private ArrayList mFieldInlineTooltipPresentations; private ArrayList mFieldFilters; @Nullable private ClipData mFieldContent; private RemoteViews mPresentation; @Nullable private InlinePresentation mInlinePresentation; + @Nullable private InlinePresentation mInlineTooltipPresentation; private IntentSender mAuthentication; private boolean mDestroyed; @Nullable private String mId; @@ -305,6 +326,31 @@ public final class Dataset implements Parcelable { return this; } + /** + * Visualizes this dataset as inline suggestions. + * + * @param inlinePresentation the {@link InlinePresentation} used to visualize this + * dataset as inline suggestions. If the dataset supports inline suggestions this + * should not be null. + * @param inlineTooltipPresentation the {@link InlinePresentation} used to show + * the tooltip for the {@code inlinePresentation}. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return this builder. + */ + public @NonNull Builder setInlinePresentation( + @NonNull InlinePresentation inlinePresentation, + @NonNull InlinePresentation inlineTooltipPresentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(inlinePresentation, "inlinePresentation must be non-null"); + Preconditions.checkNotNull(inlineTooltipPresentation, + "inlineTooltipPresentation must be non-null"); + mInlinePresentation = inlinePresentation; + mInlineTooltipPresentation = inlineTooltipPresentation; + return this; + } + /** * Triggers a custom UI before before autofilling the screen with the contents of this * dataset. @@ -596,6 +642,41 @@ public final class Dataset implements Parcelable { return this; } + /** + * Sets the value of a field, using a custom {@link RemoteViews presentation} to + * visualize it and an {@link InlinePresentation} to visualize it as an inline suggestion. + * + * @see #setValue(AutofillId, AutofillValue, RemoteViews, InlinePresentation) + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset + * as inline suggestions. If the dataset supports inline suggestions, + * this should not be null. + * @param inlineTooltipPresentation The {@link InlinePresentation} used to show + * the tooltip for the {@code inlinePresentation}. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return this builder. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull RemoteViews presentation, @NonNull InlinePresentation inlinePresentation, + @NonNull InlinePresentation inlineTooltipPresentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(presentation, "presentation cannot be null"); + Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); + Preconditions.checkNotNull(inlineTooltipPresentation, + "inlineTooltipPresentation cannot be null"); + setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, + inlineTooltipPresentation, null); + return this; + } + /** * Sets the value of a field, using a custom {@link RemoteViews presentation} to * visualize it and a explicit filter, and an @@ -640,6 +721,47 @@ public final class Dataset implements Parcelable { return this; } + /** + * Sets the value of a field, using a custom {@link RemoteViews presentation} to + * visualize it and a explicit filter, and an + * {@link InlinePresentation} to visualize it as an inline suggestion. + * + * @see #setValue(AutofillId, AutofillValue, Pattern, RemoteViews, InlinePresentation) + * + * @param id id returned by {@link + * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. + * @param value the value to be autofilled. Pass {@code null} if you do not have the value + * but the target view is a logical part of the dataset. For example, if + * the dataset needs authentication and you have no access to the value. + * @param filter regex used to determine if the dataset should be shown in the autofill UI; + * when {@code null}, it disables filtering on that dataset (this is the recommended + * approach when {@code value} is not {@code null} and field contains sensitive data + * such as passwords). + * @param presentation the presentation used to visualize this field. + * @param inlinePresentation The {@link InlinePresentation} used to visualize this dataset + * as inline suggestions. If the dataset supports inline suggestions, this + * should not be null. + * @param inlineTooltipPresentation The {@link InlinePresentation} used to show + * the tooltip for the {@code inlinePresentation}. + * + * @throws IllegalStateException if {@link #build()} was already called. + * + * @return this builder. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @Nullable Pattern filter, @NonNull RemoteViews presentation, + @NonNull InlinePresentation inlinePresentation, + @NonNull InlinePresentation inlineTooltipPresentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(presentation, "presentation cannot be null"); + Preconditions.checkNotNull(inlinePresentation, "inlinePresentation cannot be null"); + Preconditions.checkNotNull(inlineTooltipPresentation, + "inlineTooltipPresentation cannot be null"); + setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, + inlineTooltipPresentation, new DatasetFieldFilter(filter)); + return this; + } + /** * Sets the value of a field with an explicit filter, and using an * {@link InlinePresentation} to visualize it as an inline suggestion. @@ -680,6 +802,15 @@ public final class Dataset implements Parcelable { @Nullable AutofillValue value, @Nullable RemoteViews presentation, @Nullable InlinePresentation inlinePresentation, @Nullable DatasetFieldFilter filter) { + setLifeTheUniverseAndEverything(id, value, presentation, inlinePresentation, null, + filter); + } + + private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, + @Nullable AutofillValue value, @Nullable RemoteViews presentation, + @Nullable InlinePresentation inlinePresentation, + @Nullable InlinePresentation tooltip, + @Nullable DatasetFieldFilter filter) { Preconditions.checkNotNull(id, "id cannot be null"); if (mFieldIds != null) { final int existingIdx = mFieldIds.indexOf(id); @@ -687,6 +818,7 @@ public final class Dataset implements Parcelable { mFieldValues.set(existingIdx, value); mFieldPresentations.set(existingIdx, presentation); mFieldInlinePresentations.set(existingIdx, inlinePresentation); + mFieldInlineTooltipPresentations.set(existingIdx, tooltip); mFieldFilters.set(existingIdx, filter); return; } @@ -695,12 +827,14 @@ public final class Dataset implements Parcelable { mFieldValues = new ArrayList<>(); mFieldPresentations = new ArrayList<>(); mFieldInlinePresentations = new ArrayList<>(); + mFieldInlineTooltipPresentations = new ArrayList<>(); mFieldFilters = new ArrayList<>(); } mFieldIds.add(id); mFieldValues.add(value); mFieldPresentations.add(presentation); mFieldInlinePresentations.add(inlinePresentation); + mFieldInlineTooltipPresentations.add(tooltip); mFieldFilters.add(filter); } @@ -755,10 +889,12 @@ public final class Dataset implements Parcelable { public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); parcel.writeParcelable(mInlinePresentation, flags); + parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeTypedList(mFieldIds, flags); parcel.writeTypedList(mFieldValues, flags); parcel.writeTypedList(mFieldPresentations, flags); parcel.writeTypedList(mFieldInlinePresentations, flags); + parcel.writeTypedList(mFieldInlineTooltipPresentations, flags); parcel.writeTypedList(mFieldFilters, flags); parcel.writeParcelable(mFieldContent, flags); parcel.writeParcelable(mAuthentication, flags); @@ -770,6 +906,8 @@ public final class Dataset implements Parcelable { public Dataset createFromParcel(Parcel parcel) { final RemoteViews presentation = parcel.readParcelable(null); final InlinePresentation inlinePresentation = parcel.readParcelable(null); + final InlinePresentation inlineTooltipPresentation = + parcel.readParcelable(null); final ArrayList ids = parcel.createTypedArrayList(AutofillId.CREATOR); final ArrayList values = @@ -778,6 +916,8 @@ public final class Dataset implements Parcelable { parcel.createTypedArrayList(RemoteViews.CREATOR); final ArrayList inlinePresentations = parcel.createTypedArrayList(InlinePresentation.CREATOR); + final ArrayList inlineTooltipPresentations = + parcel.createTypedArrayList(InlinePresentation.CREATOR); final ArrayList filters = parcel.createTypedArrayList(DatasetFieldFilter.CREATOR); final ClipData fieldContent = parcel.readParcelable(null); @@ -790,8 +930,13 @@ public final class Dataset implements Parcelable { final Builder builder = (presentation != null) ? new Builder(presentation) : new Builder(); if (inlinePresentation != null) { - builder.setInlinePresentation(inlinePresentation); + if (inlineTooltipPresentation != null) { + builder.setInlinePresentation(inlinePresentation, inlineTooltipPresentation); + } else { + builder.setInlinePresentation(inlinePresentation); + } } + if (fieldContent != null) { builder.setContent(ids.get(0), fieldContent); } @@ -802,9 +947,11 @@ public final class Dataset implements Parcelable { final RemoteViews fieldPresentation = presentations.get(i); final InlinePresentation fieldInlinePresentation = i < inlinePresentationsSize ? inlinePresentations.get(i) : null; + final InlinePresentation fieldInlineTooltipPresentation = + i < inlinePresentationsSize ? inlineTooltipPresentations.get(i) : null; final DatasetFieldFilter filter = filters.get(i); builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, - fieldInlinePresentation, filter); + fieldInlinePresentation, fieldInlineTooltipPresentation, filter); } builder.setAuthentication(authentication); builder.setId(datasetId); diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index b1107a8c2efb3..740ae260999ff 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -23,6 +23,7 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.Activity; import android.content.IntentSender; @@ -76,6 +77,7 @@ public final class FillResponse implements Parcelable { private final @Nullable Bundle mClientState; private final @Nullable RemoteViews mPresentation; private final @Nullable InlinePresentation mInlinePresentation; + private final @Nullable InlinePresentation mInlineTooltipPresentation; private final @Nullable RemoteViews mHeader; private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; @@ -95,6 +97,7 @@ public final class FillResponse implements Parcelable { mClientState = builder.mClientState; mPresentation = builder.mPresentation; mInlinePresentation = builder.mInlinePresentation; + mInlineTooltipPresentation = builder.mInlineTooltipPresentation; mHeader = builder.mHeader; mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; @@ -134,6 +137,11 @@ public final class FillResponse implements Parcelable { return mInlinePresentation; } + /** @hide */ + public @Nullable InlinePresentation getInlineTooltipPresentation() { + return mInlineTooltipPresentation; + } + /** @hide */ public @Nullable RemoteViews getHeader() { return mHeader; @@ -219,6 +227,7 @@ public final class FillResponse implements Parcelable { private Bundle mClientState; private RemoteViews mPresentation; private InlinePresentation mInlinePresentation; + private InlinePresentation mInlineTooltipPresentation; private RemoteViews mHeader; private RemoteViews mFooter; private IntentSender mAuthentication; @@ -360,6 +369,22 @@ public final class FillResponse implements Parcelable { 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 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. + */ + @NonNull + public Builder setAuthentication(@SuppressLint("ArrayReturn") @NonNull AutofillId[] ids, + @Nullable IntentSender authentication, @Nullable RemoteViews presentation, + @Nullable InlinePresentation inlinePresentation, + @Nullable InlinePresentation inlineTooltipPresentation) { throwIfDestroyed(); throwIfDisableAutofillCalled(); if (mHeader != null || mFooter != null) { @@ -373,6 +398,7 @@ public final class FillResponse implements Parcelable { mAuthentication = authentication; mPresentation = presentation; mInlinePresentation = inlinePresentation; + mInlineTooltipPresentation = inlineTooltipPresentation; mAuthenticationIds = assertValid(ids); return this; } @@ -737,6 +763,9 @@ public final class FillResponse implements Parcelable { if (mInlinePresentation != null) { builder.append(", hasInlinePresentation"); } + if (mInlineTooltipPresentation != null) { + builder.append(", hasInlineTooltipPresentation"); + } if (mHeader != null) { builder.append(", hasHeader"); } @@ -784,6 +813,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); parcel.writeParcelable(mInlinePresentation, flags); + parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); parcel.writeParcelable(mUserData, flags); @@ -818,9 +848,10 @@ public final class FillResponse implements Parcelable { final IntentSender authentication = parcel.readParcelable(null); final RemoteViews presentation = parcel.readParcelable(null); final InlinePresentation inlinePresentation = parcel.readParcelable(null); + final InlinePresentation inlineTooltipPresentation = parcel.readParcelable(null); if (authenticationIds != null) { builder.setAuthentication(authenticationIds, authentication, presentation, - inlinePresentation); + inlinePresentation, inlineTooltipPresentation); } final RemoteViews header = parcel.readParcelable(null); if (header != null) { diff --git a/core/java/android/service/autofill/InlinePresentation.java b/core/java/android/service/autofill/InlinePresentation.java index fbf23b69addf4..40349576c460d 100644 --- a/core/java/android/service/autofill/InlinePresentation.java +++ b/core/java/android/service/autofill/InlinePresentation.java @@ -75,9 +75,23 @@ public final class InlinePresentation implements Parcelable { return hints.toArray(new String[hints.size()]); } + /** + * Creates a presentation for the inline suggestion tooltip + * + * @param slice Represents the UI content and the action for the inline suggestion tooltip. + * @param spec Specifies the UI specification for the inline suggestion tooltip. + * @return An {@link InlinePresentation} for the inline suggestion tooltip + */ + @NonNull + public static InlinePresentation createTooltipPresentation(@NonNull Slice slice, + @NonNull InlinePresentationSpec spec) { + return new InlinePresentation(slice, spec, /* pinned */ false); + + } - // Code below generated by codegen v1.0.20. + + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -259,10 +273,10 @@ public final class InlinePresentation implements Parcelable { }; @DataClass.Generated( - time = 1604456277638L, - codegenVersion = "1.0.20", + time = 1615968415006L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/service/autofill/InlinePresentation.java", - inputSignatures = "private final @android.annotation.NonNull android.app.slice.Slice mSlice\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final boolean mPinned\npublic @android.annotation.NonNull @android.annotation.Size java.lang.String[] getAutofillHints()\nclass InlinePresentation extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstDefs=true, genEqualsHashCode=true)") + inputSignatures = "private final @android.annotation.NonNull android.app.slice.Slice mSlice\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final boolean mPinned\npublic @android.annotation.NonNull @android.annotation.Size java.lang.String[] getAutofillHints()\npublic static @android.annotation.NonNull android.service.autofill.InlinePresentation createTooltipPresentation(android.app.slice.Slice,android.widget.inline.InlinePresentationSpec)\nclass InlinePresentation extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstDefs=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java index 0ab4e05227ba5..0c7a15e6b6539 100644 --- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java +++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java @@ -105,6 +105,27 @@ public final class InlineSuggestionsRequest implements Parcelable { */ private int mHostDisplayId; + /** + * Specifies the UI specification for the inline suggestion tooltip in the response. + */ + private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec; + + /** + * Whether the IME supports inline suggestions from the default Autofill service that + * provides the input view. + * + * Note: The default value is {@code true}. + */ + private boolean mServiceSupported; + + /** + * Whether the IME supports inline suggestions from the application that provides the + * input view. + * + * Note: The default value is {@code true}. + */ + private boolean mClientSupported; + /** * @hide * @see {@link #mHostInputToken}. @@ -151,6 +172,10 @@ public final class InlineSuggestionsRequest implements Parcelable { for (int i = 0; i < mInlinePresentationSpecs.size(); i++) { mInlinePresentationSpecs.get(i).filterContentTypes(); } + + if (mInlineTooltipPresentationSpec != null) { + mInlineTooltipPresentationSpec.filterContentTypes(); + } } private static int defaultMaxSuggestionCount() { @@ -161,6 +186,10 @@ public final class InlineSuggestionsRequest implements Parcelable { return ActivityThread.currentPackageName(); } + private static InlinePresentationSpec defaultInlineTooltipPresentationSpec() { + return null; + } + /** * The {@link InlineSuggestionsRequest#getSupportedLocales()} now returns empty locale list when * it's not set, instead of the default system locale. @@ -191,6 +220,14 @@ public final class InlineSuggestionsRequest implements Parcelable { return Bundle.EMPTY; } + private static boolean defaultServiceSupported() { + return true; + } + + private static boolean defaultClientSupported() { + return true; + } + /** @hide */ abstract static class BaseBuilder { abstract Builder setInlinePresentationSpecs( @@ -203,6 +240,16 @@ public final class InlineSuggestionsRequest implements Parcelable { abstract Builder setHostDisplayId(int value); } + /** @hide */ + public boolean isServiceSupported() { + return mServiceSupported; + } + + /** @hide */ + public boolean isClientSupported() { + return mClientSupported; + } + // Code below generated by codegen v1.0.22. @@ -226,7 +273,10 @@ public final class InlineSuggestionsRequest implements Parcelable { @NonNull LocaleList supportedLocales, @NonNull Bundle extras, @Nullable IBinder hostInputToken, - int hostDisplayId) { + int hostDisplayId, + @Nullable InlinePresentationSpec inlineTooltipPresentationSpec, + boolean serviceSupported, + boolean clientSupported) { this.mMaxSuggestionCount = maxSuggestionCount; this.mInlinePresentationSpecs = inlinePresentationSpecs; com.android.internal.util.AnnotationValidations.validate( @@ -242,6 +292,9 @@ public final class InlineSuggestionsRequest implements Parcelable { NonNull.class, null, mExtras); this.mHostInputToken = hostInputToken; this.mHostDisplayId = hostDisplayId; + this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec; + this.mServiceSupported = serviceSupported; + this.mClientSupported = clientSupported; onConstructed(); } @@ -324,6 +377,16 @@ public final class InlineSuggestionsRequest implements Parcelable { return mHostDisplayId; } + /** + * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. + * + * @see android.service.autofill.InlineTooltipPresentation + */ + @DataClass.Generated.Member + public @Nullable InlinePresentationSpec getInlineTooltipPresentationSpec() { + return mInlineTooltipPresentationSpec; + } + @Override @DataClass.Generated.Member public String toString() { @@ -337,7 +400,10 @@ public final class InlineSuggestionsRequest implements Parcelable { "supportedLocales = " + mSupportedLocales + ", " + "extras = " + mExtras + ", " + "hostInputToken = " + mHostInputToken + ", " + - "hostDisplayId = " + mHostDisplayId + + "hostDisplayId = " + mHostDisplayId + ", " + + "inlineTooltipPresentationSpec = " + mInlineTooltipPresentationSpec + ", " + + "serviceSupported = " + mServiceSupported + ", " + + "clientSupported = " + mClientSupported + " }"; } @@ -360,7 +426,10 @@ public final class InlineSuggestionsRequest implements Parcelable { && java.util.Objects.equals(mSupportedLocales, that.mSupportedLocales) && extrasEquals(that.mExtras) && java.util.Objects.equals(mHostInputToken, that.mHostInputToken) - && mHostDisplayId == that.mHostDisplayId; + && mHostDisplayId == that.mHostDisplayId + && java.util.Objects.equals(mInlineTooltipPresentationSpec, that.mInlineTooltipPresentationSpec) + && mServiceSupported == that.mServiceSupported + && mClientSupported == that.mClientSupported; } @Override @@ -377,6 +446,9 @@ public final class InlineSuggestionsRequest implements Parcelable { _hash = 31 * _hash + java.util.Objects.hashCode(mExtras); _hash = 31 * _hash + java.util.Objects.hashCode(mHostInputToken); _hash = 31 * _hash + mHostDisplayId; + _hash = 31 * _hash + java.util.Objects.hashCode(mInlineTooltipPresentationSpec); + _hash = 31 * _hash + Boolean.hashCode(mServiceSupported); + _hash = 31 * _hash + Boolean.hashCode(mClientSupported); return _hash; } @@ -386,9 +458,12 @@ public final class InlineSuggestionsRequest implements Parcelable { // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } - byte flg = 0; + int flg = 0; + if (mServiceSupported) flg |= 0x100; + if (mClientSupported) flg |= 0x200; if (mHostInputToken != null) flg |= 0x20; - dest.writeByte(flg); + if (mInlineTooltipPresentationSpec != null) flg |= 0x80; + dest.writeInt(flg); dest.writeInt(mMaxSuggestionCount); dest.writeParcelableList(mInlinePresentationSpecs, flags); dest.writeString(mHostPackageName); @@ -396,6 +471,7 @@ public final class InlineSuggestionsRequest implements Parcelable { dest.writeBundle(mExtras); parcelHostInputToken(dest, flags); dest.writeInt(mHostDisplayId); + if (mInlineTooltipPresentationSpec != null) dest.writeTypedObject(mInlineTooltipPresentationSpec, flags); } @Override @@ -409,7 +485,9 @@ public final class InlineSuggestionsRequest implements Parcelable { // You can override field unparcelling by defining methods like: // static FieldType unparcelFieldName(Parcel in) { ... } - byte flg = in.readByte(); + int flg = in.readInt(); + boolean serviceSupported = (flg & 0x100) != 0; + boolean clientSupported = (flg & 0x200) != 0; int maxSuggestionCount = in.readInt(); List inlinePresentationSpecs = new ArrayList<>(); in.readParcelableList(inlinePresentationSpecs, InlinePresentationSpec.class.getClassLoader()); @@ -418,6 +496,7 @@ public final class InlineSuggestionsRequest implements Parcelable { Bundle extras = in.readBundle(); IBinder hostInputToken = unparcelHostInputToken(in); int hostDisplayId = in.readInt(); + InlinePresentationSpec inlineTooltipPresentationSpec = (flg & 0x80) == 0 ? null : (InlinePresentationSpec) in.readTypedObject(InlinePresentationSpec.CREATOR); this.mMaxSuggestionCount = maxSuggestionCount; this.mInlinePresentationSpecs = inlinePresentationSpecs; @@ -434,6 +513,9 @@ public final class InlineSuggestionsRequest implements Parcelable { NonNull.class, null, mExtras); this.mHostInputToken = hostInputToken; this.mHostDisplayId = hostDisplayId; + this.mInlineTooltipPresentationSpec = inlineTooltipPresentationSpec; + this.mServiceSupported = serviceSupported; + this.mClientSupported = clientSupported; onConstructed(); } @@ -466,6 +548,9 @@ public final class InlineSuggestionsRequest implements Parcelable { private @NonNull Bundle mExtras; private @Nullable IBinder mHostInputToken; private int mHostDisplayId; + private @Nullable InlinePresentationSpec mInlineTooltipPresentationSpec; + private boolean mServiceSupported; + private boolean mClientSupported; private long mBuilderFieldsSet = 0L; @@ -597,10 +682,51 @@ public final class InlineSuggestionsRequest implements Parcelable { return this; } + /** + * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. + * + * @see android.service.autofill.InlineTooltipPresentation + */ + @DataClass.Generated.Member + public @NonNull Builder setInlineTooltipPresentationSpec(@NonNull InlinePresentationSpec value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80; + mInlineTooltipPresentationSpec = value; + return this; + } + + /** + * Whether the IME supports inline suggestions from the default Autofill service that + * provides the input view. + * + * Note: The default value is {@code true}. + */ + @DataClass.Generated.Member + public @NonNull Builder setServiceSupported(boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x100; + mServiceSupported = value; + return this; + } + + /** + * Whether the IME supports inline suggestions from the application that provides the + * input view. + * + * Note: The default value is {@code true}. + */ + @DataClass.Generated.Member + public @NonNull Builder setClientSupported(boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x200; + mClientSupported = value; + return this; + } + /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull InlineSuggestionsRequest build() { checkNotUsed(); - mBuilderFieldsSet |= 0x80; // Mark builder used + mBuilderFieldsSet |= 0x400; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mMaxSuggestionCount = defaultMaxSuggestionCount(); @@ -620,6 +746,15 @@ public final class InlineSuggestionsRequest implements Parcelable { if ((mBuilderFieldsSet & 0x40) == 0) { mHostDisplayId = defaultHostDisplayId(); } + if ((mBuilderFieldsSet & 0x80) == 0) { + mInlineTooltipPresentationSpec = defaultInlineTooltipPresentationSpec(); + } + if ((mBuilderFieldsSet & 0x100) == 0) { + mServiceSupported = defaultServiceSupported(); + } + if ((mBuilderFieldsSet & 0x200) == 0) { + mClientSupported = defaultClientSupported(); + } InlineSuggestionsRequest o = new InlineSuggestionsRequest( mMaxSuggestionCount, mInlinePresentationSpecs, @@ -627,12 +762,15 @@ public final class InlineSuggestionsRequest implements Parcelable { mSupportedLocales, mExtras, mHostInputToken, - mHostDisplayId); + mHostDisplayId, + mInlineTooltipPresentationSpec, + mServiceSupported, + mClientSupported); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x80) != 0) { + if ((mBuilderFieldsSet & 0x400) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -640,10 +778,10 @@ public final class InlineSuggestionsRequest implements Parcelable { } @DataClass.Generated( - time = 1612206506050L, + time = 1615798784918L, codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionsRequest.java", - inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate int mHostDisplayId\nprivate static final @android.compat.annotation.ChangeId @android.compat.annotation.EnabledSince long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY\npublic void setHostInputToken(android.os.IBinder)\nprivate boolean extrasEquals(android.os.Bundle)\nprivate void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic void setHostDisplayId(int)\nprivate void onConstructed()\npublic void filterContentTypes()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []") + inputSignatures = "public static final int SUGGESTION_COUNT_UNLIMITED\nprivate final int mMaxSuggestionCount\nprivate final @android.annotation.NonNull java.util.List mInlinePresentationSpecs\nprivate @android.annotation.NonNull java.lang.String mHostPackageName\nprivate @android.annotation.NonNull android.os.LocaleList mSupportedLocales\nprivate @android.annotation.NonNull android.os.Bundle mExtras\nprivate @android.annotation.Nullable android.os.IBinder mHostInputToken\nprivate int mHostDisplayId\nprivate @android.annotation.Nullable android.widget.inline.InlinePresentationSpec mInlineTooltipPresentationSpec\nprivate boolean mServiceSupported\nprivate boolean mClientSupported\nprivate static final @android.compat.annotation.ChangeId @android.compat.annotation.EnabledSince long IME_AUTOFILL_DEFAULT_SUPPORTED_LOCALES_IS_EMPTY\npublic void setHostInputToken(android.os.IBinder)\nprivate boolean extrasEquals(android.os.Bundle)\nprivate void parcelHostInputToken(android.os.Parcel,int)\nprivate @android.annotation.Nullable android.os.IBinder unparcelHostInputToken(android.os.Parcel)\npublic void setHostDisplayId(int)\nprivate void onConstructed()\npublic void filterContentTypes()\nprivate static int defaultMaxSuggestionCount()\nprivate static java.lang.String defaultHostPackageName()\nprivate static android.widget.inline.InlinePresentationSpec defaultInlineTooltipPresentationSpec()\nprivate static android.os.LocaleList defaultSupportedLocales()\nprivate static @android.annotation.Nullable android.os.IBinder defaultHostInputToken()\nprivate static @android.annotation.Nullable int defaultHostDisplayId()\nprivate static @android.annotation.NonNull android.os.Bundle defaultExtras()\nprivate static boolean defaultServiceSupported()\nprivate static boolean defaultClientSupported()\npublic boolean isServiceSupported()\npublic boolean isClientSupported()\nclass InlineSuggestionsRequest extends java.lang.Object implements [android.os.Parcelable]\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genBuilder=true)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(java.util.List)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostPackageName(java.lang.String)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostInputToken(android.os.IBinder)\nabstract android.view.inputmethod.InlineSuggestionsRequest.Builder setHostDisplayId(int)\nclass BaseBuilder extends java.lang.Object implements []") @Deprecated private void __metadata() {} From d66c967c719f15eacca6086853bb3a38902e7c90 Mon Sep 17 00:00:00 2001 From: TYM Tsai Date: Wed, 10 Mar 2021 16:19:59 +0800 Subject: [PATCH 2/2] Implement inline suggestion tooltip In the InlineSuggestionFactory, it will filter the tooltips by the field let only the first one can pass to the IME and drop others. When the IME inflates the inline suggestion, if there is a tooltip of the suggestion view, inflates the tooltip view at the same time. It will create the InlineTooltipUi that will be a popup window and contains the surface view of the tooltip. Bug: 172024354 Test: atest CtsAutoFillServiceTestCases:InlineTooltipTest Change-Id: I6f20dcad6d03432017e684964eca52dff2609205 --- .../view/inputmethod/InlineSuggestion.java | 121 ++++++- .../inputmethod/InlineSuggestionInfo.java | 56 ++- .../inputmethod/InlineSuggestionsRequest.java | 4 +- .../internal/view/inline/InlineTooltipUi.java | 337 ++++++++++++++++++ .../autofill/ui/InlineSuggestionFactory.java | 79 +++- 5 files changed, 572 insertions(+), 25 deletions(-) create mode 100644 core/java/com/android/internal/view/inline/InlineTooltipUi.java diff --git a/core/java/android/view/inputmethod/InlineSuggestion.java b/core/java/android/view/inputmethod/InlineSuggestion.java index b8893cee834d5..27c637bb59dd7 100644 --- a/core/java/android/view/inputmethod/InlineSuggestion.java +++ b/core/java/android/view/inputmethod/InlineSuggestion.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.util.Size; import android.util.Slog; import android.view.SurfaceControlViewHost; +import android.view.View; import android.view.ViewGroup; import android.widget.inline.InlineContentView; @@ -38,6 +39,7 @@ import com.android.internal.util.DataClass; import com.android.internal.util.Parcelling; import com.android.internal.view.inline.IInlineContentCallback; import com.android.internal.view.inline.IInlineContentProvider; +import com.android.internal.view.inline.InlineTooltipUi; import java.lang.ref.WeakReference; import java.util.concurrent.Executor; @@ -74,6 +76,15 @@ public final class InlineSuggestion implements Parcelable { @Nullable private InlineContentCallbackImpl mInlineContentCallback; + /** + * Used to show up the inline suggestion tooltip. + * + * @hide + */ + @Nullable + @DataClass.ParcelWith(InlineTooltipUiParceling.class) + private InlineTooltipUi mInlineTooltipUi; + /** * Creates a new {@link InlineSuggestion}, for testing purpose. * @@ -82,7 +93,8 @@ public final class InlineSuggestion implements Parcelable { @TestApi @NonNull public static InlineSuggestion newInlineSuggestion(@NonNull InlineSuggestionInfo info) { - return new InlineSuggestion(info, null, /* inlineContentCallback */ null); + return new InlineSuggestion(info, null, /* inlineContentCallback */ null, + /* inlineTooltipUi */ null); } /** @@ -92,7 +104,7 @@ public final class InlineSuggestion implements Parcelable { */ public InlineSuggestion(@NonNull InlineSuggestionInfo info, @Nullable IInlineContentProvider contentProvider) { - this(info, contentProvider, /* inlineContentCallback */ null); + this(info, contentProvider, /* inlineContentCallback */ null, /* inlineTooltipUi */ null); } /** @@ -136,9 +148,21 @@ public final class InlineSuggestion implements Parcelable { "size is neither between min:" + minSize + " and max:" + maxSize + ", nor wrap_content"); } - mInlineContentCallback = getInlineContentCallback(context, callbackExecutor, callback); + + InlineSuggestion toolTip = mInfo.getTooltip(); + if (toolTip != null) { + if (mInlineTooltipUi == null) { + mInlineTooltipUi = new InlineTooltipUi(context); + } + } else { + mInlineTooltipUi = null; + } + + mInlineContentCallback = getInlineContentCallback(context, callbackExecutor, callback, + mInlineTooltipUi); if (mContentProvider == null) { callbackExecutor.execute(() -> callback.accept(/* view */ null)); + mInlineTooltipUi = null; return; } try { @@ -148,6 +172,13 @@ public final class InlineSuggestion implements Parcelable { Slog.w(TAG, "Error creating suggestion content surface: " + e); callbackExecutor.execute(() -> callback.accept(/* view */ null)); } + if (toolTip == null) return; + + final Size tooltipSize = new Size(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mInfo.getTooltip().inflate(context, tooltipSize, callbackExecutor, view -> { + Handler.getMain().post(() -> mInlineTooltipUi.setTooltipView(view)); + }); } /** @@ -162,12 +193,13 @@ public final class InlineSuggestion implements Parcelable { } private synchronized InlineContentCallbackImpl getInlineContentCallback(Context context, - Executor callbackExecutor, Consumer callback) { + Executor callbackExecutor, Consumer callback, + InlineTooltipUi inlineTooltipUi) { if (mInlineContentCallback != null) { throw new IllegalStateException("Already called #inflate()"); } return new InlineContentCallbackImpl(context, mContentProvider, callbackExecutor, - callback); + callback, inlineTooltipUi); } /** @@ -267,14 +299,19 @@ public final class InlineSuggestion implements Parcelable { @Nullable private Consumer mSurfacePackageConsumer; + @Nullable + private InlineTooltipUi mInlineTooltipUi; + InlineContentCallbackImpl(@NonNull Context context, @Nullable IInlineContentProvider inlineContentProvider, @NonNull @CallbackExecutor Executor callbackExecutor, - @NonNull Consumer callback) { + @NonNull Consumer callback, + @Nullable InlineTooltipUi inlineTooltipUi) { mContext = context; mInlineContentProvider = inlineContentProvider; mCallbackExecutor = callbackExecutor; mCallback = callback; + mInlineTooltipUi = inlineTooltipUi; } @BinderThread @@ -305,6 +342,17 @@ public final class InlineSuggestion implements Parcelable { mCallbackExecutor.execute(() -> mCallback.accept(/* view */null)); } else { mView = new InlineContentView(mContext); + if (mInlineTooltipUi != null) { + mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, + int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (mInlineTooltipUi != null) { + mInlineTooltipUi.update(mView); + } + } + }); + } mView.setLayoutParams(new ViewGroup.LayoutParams(width, height)); mView.setChildSurfacePackageUpdater(getSurfacePackageUpdater()); mCallbackExecutor.execute(() -> mCallback.accept(mView)); @@ -425,10 +473,25 @@ public final class InlineSuggestion implements Parcelable { } } + /** + * This class used to provide parcelling logic for InlineContentCallbackImpl. It's intended to + * make this parcelling a no-op, since it can't be parceled and we don't need to parcel it. + */ + private static class InlineTooltipUiParceling implements + Parcelling { + @Override + public void parcel(InlineTooltipUi item, Parcel dest, int parcelFlags) { + } + + @Override + public InlineTooltipUi unparcel(Parcel source) { + return null; + } + } - // Code below generated by codegen v1.0.15. + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -446,18 +509,22 @@ public final class InlineSuggestion implements Parcelable { * * @param inlineContentCallback * Used to keep a strong reference to the callback so it doesn't get garbage collected. + * @param inlineTooltipUi + * Used to show up the inline suggestion tooltip. * @hide */ @DataClass.Generated.Member public InlineSuggestion( @NonNull InlineSuggestionInfo info, @Nullable IInlineContentProvider contentProvider, - @Nullable InlineContentCallbackImpl inlineContentCallback) { + @Nullable InlineContentCallbackImpl inlineContentCallback, + @Nullable InlineTooltipUi inlineTooltipUi) { this.mInfo = info; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mInfo); this.mContentProvider = contentProvider; this.mInlineContentCallback = inlineContentCallback; + this.mInlineTooltipUi = inlineTooltipUi; // onConstructed(); // You can define this method to get a callback } @@ -485,6 +552,16 @@ public final class InlineSuggestion implements Parcelable { return mInlineContentCallback; } + /** + * Used to show up the inline suggestion tooltip. + * + * @hide + */ + @DataClass.Generated.Member + public @Nullable InlineTooltipUi getInlineTooltipUi() { + return mInlineTooltipUi; + } + @Override @DataClass.Generated.Member public String toString() { @@ -494,7 +571,8 @@ public final class InlineSuggestion implements Parcelable { return "InlineSuggestion { " + "info = " + mInfo + ", " + "contentProvider = " + mContentProvider + ", " + - "inlineContentCallback = " + mInlineContentCallback + + "inlineContentCallback = " + mInlineContentCallback + ", " + + "inlineTooltipUi = " + mInlineTooltipUi + " }"; } @@ -513,7 +591,8 @@ public final class InlineSuggestion implements Parcelable { return true && java.util.Objects.equals(mInfo, that.mInfo) && java.util.Objects.equals(mContentProvider, that.mContentProvider) - && java.util.Objects.equals(mInlineContentCallback, that.mInlineContentCallback); + && java.util.Objects.equals(mInlineContentCallback, that.mInlineContentCallback) + && java.util.Objects.equals(mInlineTooltipUi, that.mInlineTooltipUi); } @Override @@ -526,6 +605,7 @@ public final class InlineSuggestion implements Parcelable { _hash = 31 * _hash + java.util.Objects.hashCode(mInfo); _hash = 31 * _hash + java.util.Objects.hashCode(mContentProvider); _hash = 31 * _hash + java.util.Objects.hashCode(mInlineContentCallback); + _hash = 31 * _hash + java.util.Objects.hashCode(mInlineTooltipUi); return _hash; } @@ -540,6 +620,17 @@ public final class InlineSuggestion implements Parcelable { } } + @DataClass.Generated.Member + static Parcelling sParcellingForInlineTooltipUi = + Parcelling.Cache.get( + InlineTooltipUiParceling.class); + static { + if (sParcellingForInlineTooltipUi == null) { + sParcellingForInlineTooltipUi = Parcelling.Cache.put( + new InlineTooltipUiParceling()); + } + } + @Override @DataClass.Generated.Member public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -549,10 +640,12 @@ public final class InlineSuggestion implements Parcelable { byte flg = 0; if (mContentProvider != null) flg |= 0x2; if (mInlineContentCallback != null) flg |= 0x4; + if (mInlineTooltipUi != null) flg |= 0x8; dest.writeByte(flg); dest.writeTypedObject(mInfo, flags); if (mContentProvider != null) dest.writeStrongInterface(mContentProvider); sParcellingForInlineContentCallback.parcel(mInlineContentCallback, dest, flags); + sParcellingForInlineTooltipUi.parcel(mInlineTooltipUi, dest, flags); } @Override @@ -570,12 +663,14 @@ public final class InlineSuggestion implements Parcelable { InlineSuggestionInfo info = (InlineSuggestionInfo) in.readTypedObject(InlineSuggestionInfo.CREATOR); IInlineContentProvider contentProvider = (flg & 0x2) == 0 ? null : IInlineContentProvider.Stub.asInterface(in.readStrongBinder()); InlineContentCallbackImpl inlineContentCallback = sParcellingForInlineContentCallback.unparcel(in); + InlineTooltipUi inlineTooltipUi = sParcellingForInlineTooltipUi.unparcel(in); this.mInfo = info; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mInfo); this.mContentProvider = contentProvider; this.mInlineContentCallback = inlineContentCallback; + this.mInlineTooltipUi = inlineTooltipUi; // onConstructed(); // You can define this method to get a callback } @@ -595,10 +690,10 @@ public final class InlineSuggestion implements Parcelable { }; @DataClass.Generated( - time = 1589396017700L, - codegenVersion = "1.0.15", + time = 1615562097666L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java", - inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer)\nprivate static boolean isValid(int,int,int)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") + inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineTooltipUiParceling.class) com.android.internal.view.inline.InlineTooltipUi mInlineTooltipUi\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer)\nprivate static boolean isValid(int,int,int)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer,com.android.internal.view.inline.InlineTooltipUi)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/inputmethod/InlineSuggestionInfo.java b/core/java/android/view/inputmethod/InlineSuggestionInfo.java index 10fd0e0368142..5798614b8c9f7 100644 --- a/core/java/android/view/inputmethod/InlineSuggestionInfo.java +++ b/core/java/android/view/inputmethod/InlineSuggestionInfo.java @@ -72,6 +72,11 @@ public final class InlineSuggestionInfo implements Parcelable { /** Whether the suggestion should be pinned or not. */ private final boolean mPinned; + /** + * @hide + */ + private final @Nullable InlineSuggestion mTooltip; + /** * Creates a new {@link InlineSuggestionInfo}, for testing purpose. * @@ -84,12 +89,30 @@ public final class InlineSuggestionInfo implements Parcelable { @NonNull @Source String source, @SuppressLint("NullableCollection") @Nullable String[] autofillHints, @NonNull @Type String type, boolean isPinned) { - return new InlineSuggestionInfo(presentationSpec, source, autofillHints, type, isPinned); + return new InlineSuggestionInfo(presentationSpec, source, autofillHints, type, isPinned, + null); + } + + /** + * Creates a new {@link InlineSuggestionInfo}, for testing purpose. + * + * @hide + */ + @NonNull + public static InlineSuggestionInfo newInlineSuggestionInfo( + @NonNull InlinePresentationSpec presentationSpec, + @NonNull @Source String source, + @Nullable String[] autofillHints, @NonNull @Type String type, boolean isPinned, + @Nullable InlineSuggestion tooltip) { + return new InlineSuggestionInfo(presentationSpec, source, autofillHints, type, isPinned, + tooltip); } - // Code below generated by codegen v1.0.20. + + + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -141,7 +164,8 @@ public final class InlineSuggestionInfo implements Parcelable { @NonNull @Source String source, @Nullable String[] autofillHints, @NonNull @Type String type, - boolean pinned) { + boolean pinned, + @Nullable InlineSuggestion tooltip) { this.mInlinePresentationSpec = inlinePresentationSpec; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mInlinePresentationSpec); @@ -171,6 +195,7 @@ public final class InlineSuggestionInfo implements Parcelable { com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mType); this.mPinned = pinned; + this.mTooltip = tooltip; // onConstructed(); // You can define this method to get a callback } @@ -215,6 +240,14 @@ public final class InlineSuggestionInfo implements Parcelable { return mPinned; } + /** + * @hide + */ + @DataClass.Generated.Member + public @Nullable InlineSuggestion getTooltip() { + return mTooltip; + } + @Override @DataClass.Generated.Member public String toString() { @@ -226,7 +259,8 @@ public final class InlineSuggestionInfo implements Parcelable { "source = " + mSource + ", " + "autofillHints = " + java.util.Arrays.toString(mAutofillHints) + ", " + "type = " + mType + ", " + - "pinned = " + mPinned + + "pinned = " + mPinned + ", " + + "tooltip = " + mTooltip + " }"; } @@ -247,7 +281,8 @@ public final class InlineSuggestionInfo implements Parcelable { && java.util.Objects.equals(mSource, that.mSource) && java.util.Arrays.equals(mAutofillHints, that.mAutofillHints) && java.util.Objects.equals(mType, that.mType) - && mPinned == that.mPinned; + && mPinned == that.mPinned + && java.util.Objects.equals(mTooltip, that.mTooltip); } @Override @@ -262,6 +297,7 @@ public final class InlineSuggestionInfo implements Parcelable { _hash = 31 * _hash + java.util.Arrays.hashCode(mAutofillHints); _hash = 31 * _hash + java.util.Objects.hashCode(mType); _hash = 31 * _hash + Boolean.hashCode(mPinned); + _hash = 31 * _hash + java.util.Objects.hashCode(mTooltip); return _hash; } @@ -274,11 +310,13 @@ public final class InlineSuggestionInfo implements Parcelable { byte flg = 0; if (mPinned) flg |= 0x10; if (mAutofillHints != null) flg |= 0x4; + if (mTooltip != null) flg |= 0x20; dest.writeByte(flg); dest.writeTypedObject(mInlinePresentationSpec, flags); dest.writeString(mSource); if (mAutofillHints != null) dest.writeStringArray(mAutofillHints); dest.writeString(mType); + if (mTooltip != null) dest.writeTypedObject(mTooltip, flags); } @Override @@ -298,6 +336,7 @@ public final class InlineSuggestionInfo implements Parcelable { String source = in.readString(); String[] autofillHints = (flg & 0x4) == 0 ? null : in.createStringArray(); String type = in.readString(); + InlineSuggestion tooltip = (flg & 0x20) == 0 ? null : (InlineSuggestion) in.readTypedObject(InlineSuggestion.CREATOR); this.mInlinePresentationSpec = inlinePresentationSpec; com.android.internal.util.AnnotationValidations.validate( @@ -328,6 +367,7 @@ public final class InlineSuggestionInfo implements Parcelable { com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mType); this.mPinned = pinned; + this.mTooltip = tooltip; // onConstructed(); // You can define this method to get a callback } @@ -347,10 +387,10 @@ public final class InlineSuggestionInfo implements Parcelable { }; @DataClass.Generated( - time = 1604456249219L, - codegenVersion = "1.0.20", + time = 1614287616672L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestionInfo.java", - inputSignatures = "public static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_AUTOFILL\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_PLATFORM\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_SUGGESTION\npublic static final @android.annotation.SuppressLint @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_ACTION\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String mSource\nprivate final @android.annotation.Nullable java.lang.String[] mAutofillHints\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String mType\nprivate final boolean mPinned\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(android.widget.inline.InlinePresentationSpec,java.lang.String,java.lang.String[],java.lang.String,boolean)\nclass InlineSuggestionInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") + inputSignatures = "public static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_AUTOFILL\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String SOURCE_PLATFORM\npublic static final @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_SUGGESTION\npublic static final @android.annotation.SuppressLint @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String TYPE_ACTION\nprivate final @android.annotation.NonNull android.widget.inline.InlinePresentationSpec mInlinePresentationSpec\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Source java.lang.String mSource\nprivate final @android.annotation.Nullable java.lang.String[] mAutofillHints\nprivate final @android.annotation.NonNull @android.view.inputmethod.InlineSuggestionInfo.Type java.lang.String mType\nprivate final boolean mPinned\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestion mTooltip\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(android.widget.inline.InlinePresentationSpec,java.lang.String,java.lang.String[],java.lang.String,boolean)\npublic static @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(android.widget.inline.InlinePresentationSpec,java.lang.String,java.lang.String[],java.lang.String,boolean,android.view.inputmethod.InlineSuggestion)\nclass InlineSuggestionInfo extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java index 0c7a15e6b6539..e1e175512edc9 100644 --- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java +++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java @@ -380,7 +380,7 @@ public final class InlineSuggestionsRequest implements Parcelable { /** * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. * - * @see android.service.autofill.InlineTooltipPresentation + * @see android.service.autofill.InlinePresentation#createTooltipPresentation(Slice, InlinePresentationSpec) */ @DataClass.Generated.Member public @Nullable InlinePresentationSpec getInlineTooltipPresentationSpec() { @@ -685,7 +685,7 @@ public final class InlineSuggestionsRequest implements Parcelable { /** * The {@link InlinePresentationSpec} for the inline suggestion tooltip in the response. * - * @see android.service.autofill.InlineTooltipPresentation + * @see android.service.autofill.InlinePresentation#createTooltipPresentation(Slice, InlinePresentationSpec)s */ @DataClass.Generated.Member public @NonNull Builder setInlineTooltipPresentationSpec(@NonNull InlinePresentationSpec value) { diff --git a/core/java/com/android/internal/view/inline/InlineTooltipUi.java b/core/java/com/android/internal/view/inline/InlineTooltipUi.java new file mode 100644 index 0000000000000..5ec8b30d6a7bf --- /dev/null +++ b/core/java/com/android/internal/view/inline/InlineTooltipUi.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2021 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.internal.view.inline; + +import static android.view.autofill.Helper.sVerbose; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.ContextWrapper; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.util.Slog; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.PopupWindow; +import android.widget.inline.InlineContentView; + +import java.io.PrintWriter; + +/** + * UI container for the inline suggestion tooltip. + */ +public final class InlineTooltipUi extends PopupWindow implements AutoCloseable { + private static final String TAG = "InlineTooltipUi"; + + private final WindowManager mWm; + private final ViewGroup mContentContainer; + + private boolean mShowing; + + private WindowManager.LayoutParams mWindowLayoutParams; + + private final View.OnAttachStateChangeListener mAnchorOnAttachStateChangeListener = + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + /* ignore - handled by the super class */ + } + + @Override + public void onViewDetachedFromWindow(View v) { + dismiss(); + } + }; + + private final View.OnLayoutChangeListener mAnchoredOnLayoutChangeListener = + new View.OnLayoutChangeListener() { + int mHeight; + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (mHeight != bottom - top) { + mHeight = bottom - top; + adjustPosition(); + } + } + }; + + public InlineTooltipUi(@NonNull Context context) { + mContentContainer = new LinearLayout(new ContextWrapper(context)); + mWm = context.getSystemService(WindowManager.class); + + setTouchModal(false); + setOutsideTouchable(true); + setInputMethodMode(INPUT_METHOD_NOT_NEEDED); + setFocusable(false); + } + + /** + * Sets the content view for inline suggestions tooltip + * @param v the content view of {@link android.widget.inline.InlineContentView} + */ + public void setTooltipView(@NonNull InlineContentView v) { + mContentContainer.removeAllViews(); + mContentContainer.addView(v); + mContentContainer.setVisibility(View.VISIBLE); + } + + @Override + public void close() { + hide(); + } + + @Override + protected boolean hasContentView() { + return true; + } + + @Override + protected boolean hasDecorView() { + return true; + } + + @Override + protected WindowManager.LayoutParams getDecorViewLayoutParams() { + return mWindowLayoutParams; + } + + /** + * The effective {@code update} method that should be called by its clients. + */ + public void update(View anchor) { + // set to the application type with the highest z-order + setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); + + // The first time to show up, the height of tooltip is zero, + // so set the offset Y to 2 * anchor height. + final int achoredHeight = mContentContainer.getHeight(); + final int offsetY = (achoredHeight == 0) + ? -anchor.getHeight() << 1 : -anchor.getHeight() - achoredHeight; + if (!isShowing()) { + setWidth(WindowManager.LayoutParams.WRAP_CONTENT); + setHeight(WindowManager.LayoutParams.WRAP_CONTENT); + showAsDropDown(anchor, 0 , offsetY, Gravity.TOP | Gravity.CENTER_HORIZONTAL); + } else { + update(anchor, 0 , offsetY, WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT); + } + } + + @Override + protected void update(View anchor, WindowManager.LayoutParams params) { + // update content view for the anchor is scrolling + if (anchor.isVisibleToUser()) { + show(params); + } else { + hide(); + } + } + + @Override + public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { + if (isShowing()) { + return; + } + + setShowing(true); + setDropDown(true); + attachToAnchor(anchor, xoff, yoff, gravity); + final WindowManager.LayoutParams p = mWindowLayoutParams = createPopupLayoutParams( + anchor.getWindowToken()); + final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, + p.width, p.height, gravity, getAllowScrollingAnchorParent()); + updateAboveAnchor(aboveAnchor); + p.accessibilityIdOfAnchor = anchor.getAccessibilityViewId(); + p.packageName = anchor.getContext().getPackageName(); + show(p); + } + + @Override + protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { + super.attachToAnchor(anchor, xoff, yoff, gravity); + anchor.addOnAttachStateChangeListener(mAnchorOnAttachStateChangeListener); + } + + @Override + protected void detachFromAnchor() { + final View anchor = getAnchor(); + if (anchor != null) { + anchor.removeOnAttachStateChangeListener(mAnchorOnAttachStateChangeListener); + } + super.detachFromAnchor(); + } + + @Override + public void dismiss() { + if (!isShowing() || isTransitioningToDismiss()) { + return; + } + + setShowing(false); + setTransitioningToDismiss(true); + + hide(); + detachFromAnchor(); + if (getOnDismissListener() != null) { + getOnDismissListener().onDismiss(); + } + } + + private void adjustPosition() { + View anchor = getAnchor(); + if (anchor == null) return; + update(anchor); + } + + private void show(WindowManager.LayoutParams params) { + if (sVerbose) { + Slog.v(TAG, "show()"); + } + mWindowLayoutParams = params; + + try { + params.packageName = "android"; + params.setTitle("Autofill Inline Tooltip"); // Title is set for debugging purposes + if (!mShowing) { + params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mContentContainer.addOnLayoutChangeListener(mAnchoredOnLayoutChangeListener); + mWm.addView(mContentContainer, params); + mShowing = true; + } else { + mWm.updateViewLayout(mContentContainer, params); + } + } catch (WindowManager.BadTokenException e) { + Slog.d(TAG, "Failed with token " + params.token + " gone."); + } catch (IllegalStateException e) { + // WM throws an ISE if mContentView was added twice; this should never happen - + // since show() and hide() are always called in the UIThread - but when it does, + // it should not crash the system. + Slog.wtf(TAG, "Exception showing window " + params, e); + } + } + + private void hide() { + if (sVerbose) { + Slog.v(TAG, "hide()"); + } + try { + if (mShowing) { + mContentContainer.removeOnLayoutChangeListener(mAnchoredOnLayoutChangeListener); + mWm.removeView(mContentContainer); + mShowing = false; + } + } catch (IllegalStateException e) { + // WM might thrown an ISE when removing the mContentView; this should never + // happen - since show() and hide() are always called in the UIThread - but if it + // does, it should not crash the system. + Slog.e(TAG, "Exception hiding window ", e); + } + } + + @Override + public int getAnimationStyle() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public Drawable getBackground() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public View getContentView() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public float getElevation() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public Transition getEnterTransition() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public Transition getExitTransition() { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public void setBackgroundDrawable(Drawable background) { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public void setContentView(View contentView) { + if (contentView != null) { + throw new IllegalStateException("You can't call this!"); + } + } + + @Override + public void setElevation(float elevation) { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public void setEnterTransition(Transition enterTransition) { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public void setExitTransition(Transition exitTransition) { + throw new IllegalStateException("You can't call this!"); + } + + @Override + public void setTouchInterceptor(View.OnTouchListener l) { + throw new IllegalStateException("You can't call this!"); + } + + /** + * Dumps status + */ + public void dump(@NonNull PrintWriter pw, @Nullable String prefix) { + + pw.print(prefix); + + if (mContentContainer != null) { + pw.print(prefix); pw.print("Window: "); + final String prefix2 = prefix + " "; + pw.println(); + pw.print(prefix2); pw.print("showing: "); pw.println(mShowing); + pw.print(prefix2); pw.print("view: "); pw.println(mContentContainer); + if (mWindowLayoutParams != null) { + pw.print(prefix2); pw.print("params: "); pw.println(mWindowLayoutParams); + } + pw.print(prefix2); pw.print("screen coordinates: "); + if (mContentContainer == null) { + pw.println("N/A"); + } else { + final int[] coordinates = mContentContainer.getLocationOnScreen(); + pw.print(coordinates[0]); pw.print("x"); pw.println(coordinates[1]); + } + } + } +} diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java index b15d07b0f1a94..9d4c9ebbf5922 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -16,10 +16,13 @@ package com.android.server.autofill.ui; +import static android.view.inputmethod.InlineSuggestionInfo.TYPE_SUGGESTION; + import static com.android.server.autofill.Helper.sDebug; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.IntentSender; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; import android.service.autofill.InlinePresentation; @@ -49,6 +52,9 @@ final class InlineSuggestionFactory { InlineSuggestionInfo.TYPE_ACTION, () -> uiCallback.authenticate(requestId, AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED), mergedInlinePresentation(inlineFillUiInfo.mInlineRequest, 0, inlineAuthentication), + createInlineSuggestionTooltip(inlineFillUiInfo.mInlineRequest, + inlineFillUiInfo, InlineSuggestionInfo.SOURCE_AUTOFILL, + response.getInlineTooltipPresentation()), uiCallback); } @@ -66,6 +72,8 @@ final class InlineSuggestionFactory { final InlineSuggestionsRequest request = inlineFillUiInfo.mInlineRequest; SparseArray> response = new SparseArray<>(datasets.size()); + + boolean hasTooltip = false; for (int datasetIndex = 0; datasetIndex < datasets.size(); datasetIndex++) { final Dataset dataset = datasets.get(datasetIndex); final int fieldIndex = dataset.getFieldIds().indexOf(inlineFillUiInfo.mFocusedId); @@ -82,14 +90,25 @@ final class InlineSuggestionFactory { } final String suggestionType = - dataset.getAuthentication() == null ? InlineSuggestionInfo.TYPE_SUGGESTION + dataset.getAuthentication() == null ? TYPE_SUGGESTION : InlineSuggestionInfo.TYPE_ACTION; final int index = datasetIndex; + InlineSuggestion inlineSuggestionTooltip = null; + if (!hasTooltip) { + // Only available for first one inline suggestion tooltip. + inlineSuggestionTooltip = createInlineSuggestionTooltip(request, + inlineFillUiInfo, suggestionSource, + dataset.getFieldInlineTooltipPresentation(fieldIndex)); + if (inlineSuggestionTooltip != null) { + hasTooltip = true; + } + } InlineSuggestion inlineSuggestion = createInlineSuggestion( inlineFillUiInfo, suggestionSource, suggestionType, () -> uiCallback.autofill(dataset, index), mergedInlinePresentation(request, datasetIndex, inlinePresentation), + inlineSuggestionTooltip, uiCallback); response.append(datasetIndex, Pair.create(dataset, inlineSuggestion)); } @@ -103,11 +122,13 @@ final class InlineSuggestionFactory { @NonNull @InlineSuggestionInfo.Type String suggestionType, @NonNull Runnable onClickAction, @NonNull InlinePresentation inlinePresentation, + @Nullable InlineSuggestion tooltip, @NonNull InlineFillUi.InlineSuggestionUiCallback uiCallback) { + final InlineSuggestionInfo inlineSuggestionInfo = new InlineSuggestionInfo( inlinePresentation.getInlinePresentationSpec(), suggestionSource, inlinePresentation.getAutofillHints(), suggestionType, - inlinePresentation.isPinned()); + inlinePresentation.isPinned(), tooltip); return new InlineSuggestion(inlineSuggestionInfo, createInlineContentProvider(inlineFillUiInfo, inlinePresentation, @@ -135,6 +156,60 @@ final class InlineSuggestionFactory { inlinePresentation.isPinned()); } + // TODO(182306770): creates new class instead of the InlineSuggestion. + private static InlineSuggestion createInlineSuggestionTooltip( + @NonNull InlineSuggestionsRequest request, + @NonNull InlineFillUi.InlineFillUiInfo inlineFillUiInfo, + String suggestionSource, + @NonNull InlinePresentation tooltipPresentation) { + if (tooltipPresentation == null) { + return null; + } + + final InlinePresentationSpec spec = request.getInlineTooltipPresentationSpec(); + InlinePresentationSpec mergedSpec; + if (spec == null) { + mergedSpec = tooltipPresentation.getInlinePresentationSpec(); + } else { + mergedSpec = new InlinePresentationSpec.Builder( + tooltipPresentation.getInlinePresentationSpec().getMinSize(), + tooltipPresentation.getInlinePresentationSpec().getMaxSize()).setStyle( + spec.getStyle()).build(); + } + + InlineFillUi.InlineSuggestionUiCallback uiCallback = + new InlineFillUi.InlineSuggestionUiCallback() { + @Override + public void autofill(Dataset dataset, int datasetIndex) { + /* nothing */ + } + + @Override + public void authenticate(int requestId, int datasetIndex) { + /* nothing */ + } + + @Override + public void startIntentSender(IntentSender intentSender) { + /* nothing */ + } + + @Override + public void onError() { + Slog.w(TAG, "An error happened on the tooltip"); + } + }; + + InlinePresentation tooltipInline = new InlinePresentation(tooltipPresentation.getSlice(), + mergedSpec, false); + IInlineContentProvider tooltipContentProvider = createInlineContentProvider( + inlineFillUiInfo, tooltipInline, () -> { /* no operation */ }, uiCallback); + final InlineSuggestionInfo tooltipInlineSuggestionInfo = new InlineSuggestionInfo( + mergedSpec, suggestionSource, /* autofillHints */ null, TYPE_SUGGESTION, + /* pinned */ false, /* tooltip */ null); + return new InlineSuggestion(tooltipInlineSuggestionInfo, tooltipContentProvider); + } + private static IInlineContentProvider createInlineContentProvider( @NonNull InlineFillUi.InlineFillUiInfo inlineFillUiInfo, @NonNull InlinePresentation inlinePresentation, @Nullable Runnable onClickAction,