diff options
| -rw-r--r-- | api/current.txt | 16 | ||||
| -rw-r--r-- | api/test-current.txt | 13 | ||||
| -rw-r--r-- | core/java/android/service/autofill/CustomDescription.java | 101 | ||||
| -rw-r--r-- | core/java/android/service/autofill/InternalOnClickAction.java | 36 | ||||
| -rw-r--r-- | core/java/android/service/autofill/InternalSanitizer.java | 2 | ||||
| -rw-r--r-- | core/java/android/service/autofill/InternalTransformation.java | 4 | ||||
| -rw-r--r-- | core/java/android/service/autofill/InternalValidator.java | 3 | ||||
| -rw-r--r-- | core/java/android/service/autofill/OnClickAction.java | 26 | ||||
| -rw-r--r-- | core/java/android/service/autofill/VisibilitySetterAction.java | 173 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/ui/SaveUi.java | 33 |
10 files changed, 393 insertions, 14 deletions
diff --git a/api/current.txt b/api/current.txt index 300ab3af79c4..fc595ac49a2c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -39005,6 +39005,7 @@ package android.service.autofill { public static class CustomDescription.Builder { ctor public CustomDescription.Builder(android.widget.RemoteViews); method public android.service.autofill.CustomDescription.Builder addChild(int, android.service.autofill.Transformation); + method public android.service.autofill.CustomDescription.Builder addOnClickAction(int, android.service.autofill.OnClickAction); method public android.service.autofill.CustomDescription.Builder batchUpdate(android.service.autofill.Validator, android.service.autofill.BatchUpdates); method public android.service.autofill.CustomDescription build(); } @@ -39143,6 +39144,9 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.LuhnChecksumValidator> CREATOR; } + public abstract interface OnClickAction { + } + public final class RegexValidator implements android.os.Parcelable android.service.autofill.Validator { ctor public RegexValidator(android.view.autofill.AutofillId, java.util.regex.Pattern); method public int describeContents(); @@ -39238,6 +39242,18 @@ package android.service.autofill { method public static android.service.autofill.Validator or(android.service.autofill.Validator...); } + public final class VisibilitySetterAction implements android.service.autofill.OnClickAction android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.VisibilitySetterAction> CREATOR; + } + + public static class VisibilitySetterAction.Builder { + ctor public VisibilitySetterAction.Builder(int, int); + method public android.service.autofill.VisibilitySetterAction build(); + method public android.service.autofill.VisibilitySetterAction.Builder setVisibility(int, int); + } + } package android.service.carrier { diff --git a/api/test-current.txt b/api/test-current.txt index 35c6c4802439..1657de5624f3 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -998,6 +998,10 @@ package android.service.autofill { method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception; } + public final class CustomDescription implements android.os.Parcelable { + method public android.util.SparseArray<android.service.autofill.InternalOnClickAction> getActions(); + } + public final class DateTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation { method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception; } @@ -1014,6 +1018,11 @@ package android.service.autofill { method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception; } + public abstract class InternalOnClickAction implements android.service.autofill.OnClickAction android.os.Parcelable { + ctor public InternalOnClickAction(); + method public abstract void onClick(android.view.ViewGroup); + } + public abstract class InternalSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer { ctor public InternalSanitizer(); } @@ -1044,6 +1053,10 @@ package android.service.autofill { method public abstract android.view.autofill.AutofillValue findRawValueByAutofillId(android.view.autofill.AutofillId); } + public final class VisibilitySetterAction extends android.service.autofill.InternalOnClickAction implements android.service.autofill.OnClickAction android.os.Parcelable { + method public void onClick(android.view.ViewGroup); + } + } package android.service.notification { diff --git a/core/java/android/service/autofill/CustomDescription.java b/core/java/android/service/autofill/CustomDescription.java index fb468a8dad6f..b1ae7a5b5d33 100644 --- a/core/java/android/service/autofill/CustomDescription.java +++ b/core/java/android/service/autofill/CustomDescription.java @@ -20,11 +20,13 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.Activity; import android.app.PendingIntent; import android.os.Parcel; import android.os.Parcelable; import android.util.Pair; +import android.util.SparseArray; import android.widget.RemoteViews; import com.android.internal.util.Preconditions; @@ -90,11 +92,13 @@ public final class CustomDescription implements Parcelable { private final RemoteViews mPresentation; private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations; private final ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; + private final SparseArray<InternalOnClickAction> mActions; private CustomDescription(Builder builder) { mPresentation = builder.mPresentation; mTransformations = builder.mTransformations; mUpdates = builder.mUpdates; + mActions = builder.mActions; } /** @hide */ @@ -115,6 +119,13 @@ public final class CustomDescription implements Parcelable { return mUpdates; } + /** @hide */ + @Nullable + @TestApi + public SparseArray<InternalOnClickAction> getActions() { + return mActions; + } + /** * Builder for {@link CustomDescription} objects. */ @@ -124,6 +135,7 @@ public final class CustomDescription implements Parcelable { private boolean mDestroyed; private ArrayList<Pair<Integer, InternalTransformation>> mTransformations; private ArrayList<Pair<InternalValidator, BatchUpdates>> mUpdates; + private SparseArray<InternalOnClickAction> mActions; /** * Default constructor. @@ -157,9 +169,12 @@ public final class CustomDescription implements Parcelable { * * @param id view id of the children view. * @param transformation an implementation provided by the Android System. + * * @return this builder. + * * @throws IllegalArgumentException if {@code transformation} is not a class provided * by the Android System. + * @throws IllegalStateException if {@link #build()} was already called. */ public Builder addChild(int id, @NonNull Transformation transformation) { throwIfDestroyed(); @@ -250,8 +265,10 @@ public final class CustomDescription implements Parcelable { * is satisfied. * * @return this builder + * * @throws IllegalArgumentException if {@code condition} is not a class provided * by the Android System. + * @throws IllegalStateException if {@link #build()} was already called. */ public Builder batchUpdate(@NonNull Validator condition, @NonNull BatchUpdates updates) { throwIfDestroyed(); @@ -266,6 +283,58 @@ public final class CustomDescription implements Parcelable { } /** + * Sets an action to be applied to the {@link RemoteViews presentation template} when the + * child view with the given {@code id} is clicked. + * + * <p>Typically used when the presentation uses a masked field (like {@code ****}) for + * sensitive fields like passwords or credit cards numbers, but offers a an icon that the + * user can tap to show the value for that field. + * + * <p>Example: + * + * <pre class="prettyprint"> + * customDescriptionBuilder + * .addChild(R.id.password_plain, new CharSequenceTransformation + * .Builder(passwordId, Pattern.compile("^(.*)$"), "$1").build()) + * .addOnClickAction(R.id.showIcon, new VisibilitySetterAction + * .Builder(R.id.hideIcon, View.VISIBLE) + * .setVisibility(R.id.showIcon, View.GONE) + * .setVisibility(R.id.password_plain, View.VISIBLE) + * .setVisibility(R.id.password_masked, View.GONE) + * .build()) + * .addOnClickAction(R.id.hideIcon, new VisibilitySetterAction + * .Builder(R.id.showIcon, View.VISIBLE) + * .setVisibility(R.id.hideIcon, View.GONE) + * .setVisibility(R.id.password_masked, View.VISIBLE) + * .setVisibility(R.id.password_plain, View.GONE) + * .build()); + * </pre> + * + * <p><b>Note:</b> Currently only one action can be applied to a child; if this method + * is called multiple times passing the same {@code id}, only the last call will be used. + * + * @param id resource id of the child view. + * @param action action to be performed. + * + * @return this builder + * + * @throws IllegalArgumentException if {@code action} is not a class provided + * by the Android System. + * @throws IllegalStateException if {@link #build()} was already called. + */ + public Builder addOnClickAction(int id, @NonNull OnClickAction action) { + throwIfDestroyed(); + Preconditions.checkArgument((action instanceof InternalOnClickAction), + "not provided by Android System: " + action); + if (mActions == null) { + mActions = new SparseArray<InternalOnClickAction>(); + } + mActions.put(id, (InternalOnClickAction) action); + + return this; + } + + /** * Creates a new {@link CustomDescription} instance. */ public CustomDescription build() { @@ -294,6 +363,8 @@ public final class CustomDescription implements Parcelable { .append(mTransformations == null ? "N/A" : mTransformations.size()) .append(", updates=") .append(mUpdates == null ? "N/A" : mUpdates.size()) + .append(", actions=") + .append(mActions == null ? "N/A" : mActions.size()) .append("]").toString(); } @@ -339,6 +410,19 @@ public final class CustomDescription implements Parcelable { dest.writeParcelableArray(conditions, flags); dest.writeParcelableArray(updates, flags); } + if (mActions == null) { + dest.writeIntArray(null); + } else { + final int size = mActions.size(); + final int[] ids = new int[size]; + final InternalOnClickAction[] values = new InternalOnClickAction[size]; + for (int i = 0; i < size; i++) { + ids[i] = mActions.keyAt(i); + values[i] = mActions.valueAt(i); + } + dest.writeIntArray(ids); + dest.writeParcelableArray(values, flags); + } } public static final Parcelable.Creator<CustomDescription> CREATOR = new Parcelable.Creator<CustomDescription>() { @@ -351,13 +435,13 @@ public final class CustomDescription implements Parcelable { if (parentPresentation == null) return null; final Builder builder = new Builder(parentPresentation); - final int[] ids = parcel.createIntArray(); - if (ids != null) { + final int[] transformationIds = parcel.createIntArray(); + if (transformationIds != null) { final InternalTransformation[] values = parcel.readParcelableArray(null, InternalTransformation.class); - final int size = ids.length; + final int size = transformationIds.length; for (int i = 0; i < size; i++) { - builder.addChild(ids[i], values[i]); + builder.addChild(transformationIds[i], values[i]); } } final InternalValidator[] conditions = @@ -369,6 +453,15 @@ public final class CustomDescription implements Parcelable { builder.batchUpdate(conditions[i], updates[i]); } } + final int[] actionIds = parcel.createIntArray(); + if (actionIds != null) { + final InternalOnClickAction[] values = + parcel.readParcelableArray(null, InternalOnClickAction.class); + final int size = actionIds.length; + for (int i = 0; i < size; i++) { + builder.addOnClickAction(actionIds[i], values[i]); + } + } return builder.build(); } diff --git a/core/java/android/service/autofill/InternalOnClickAction.java b/core/java/android/service/autofill/InternalOnClickAction.java new file mode 100644 index 000000000000..6dc49b627369 --- /dev/null +++ b/core/java/android/service/autofill/InternalOnClickAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.ViewGroup; + +/** + * Superclass of all {@link OnClickAction} the system understands. As this is not public, all public + * subclasses have to implement {@link OnClickAction} again. + * + * @hide + */ +@TestApi +public abstract class InternalOnClickAction implements OnClickAction, Parcelable { + + /** + * Applies the action to the children of the {@rootView} when clicked. + */ + public abstract void onClick(@NonNull ViewGroup rootView); +} diff --git a/core/java/android/service/autofill/InternalSanitizer.java b/core/java/android/service/autofill/InternalSanitizer.java index d77e41e3f022..ccffc70381df 100644 --- a/core/java/android/service/autofill/InternalSanitizer.java +++ b/core/java/android/service/autofill/InternalSanitizer.java @@ -35,8 +35,6 @@ public abstract class InternalSanitizer implements Sanitizer, Parcelable { * * @return sanitized value or {@code null} if value could not be sanitized (for example: didn't * match regex, it's an invalid type, regex failed, etc). - * - * @hide */ @Nullable public abstract AutofillValue sanitize(@NonNull AutofillValue value); diff --git a/core/java/android/service/autofill/InternalTransformation.java b/core/java/android/service/autofill/InternalTransformation.java index c9864a0e5711..0dba2b9bb9a6 100644 --- a/core/java/android/service/autofill/InternalTransformation.java +++ b/core/java/android/service/autofill/InternalTransformation.java @@ -44,8 +44,6 @@ public abstract class InternalTransformation implements Transformation, Parcelab * @param finder object used to find the value of a field in the screen. * @param template the {@link RemoteViews presentation template}. * @param childViewId resource id of the child view inside the template. - * - * @hide */ abstract void apply(@NonNull ValueFinder finder, @NonNull RemoteViews template, int childViewId) throws Exception; @@ -58,8 +56,6 @@ public abstract class InternalTransformation implements Transformation, Parcelab * @param template the {@link RemoteViews presentation template}. * @param transformations map of resource id of the child view inside the template to * transformation. - * - * @hide */ public static boolean batchApply(@NonNull ValueFinder finder, @NonNull RemoteViews template, @NonNull ArrayList<Pair<Integer, InternalTransformation>> transformations) { diff --git a/core/java/android/service/autofill/InternalValidator.java b/core/java/android/service/autofill/InternalValidator.java index e08bb6c1a2e0..4bea98d0d8bb 100644 --- a/core/java/android/service/autofill/InternalValidator.java +++ b/core/java/android/service/autofill/InternalValidator.java @@ -33,9 +33,6 @@ public abstract class InternalValidator implements Validator, Parcelable { * * @param finder object used to find the value of a field in the screen. * @return {@code true} if the contents are valid, {@code false} otherwise. - * - * @hide */ - @TestApi public abstract boolean isValid(@NonNull ValueFinder finder); } diff --git a/core/java/android/service/autofill/OnClickAction.java b/core/java/android/service/autofill/OnClickAction.java new file mode 100644 index 000000000000..7439b003c070 --- /dev/null +++ b/core/java/android/service/autofill/OnClickAction.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Class used to define an action to be performed when a child view in a + * {@link android.widget.RemoteViews presentation} is clicked. + * + * <p>Typically used to switch the visibility of other views in a + * {@link CustomDescription custom save UI}. + */ +public interface OnClickAction { +} diff --git a/core/java/android/service/autofill/VisibilitySetterAction.java b/core/java/android/service/autofill/VisibilitySetterAction.java new file mode 100644 index 000000000000..cdd710258ba0 --- /dev/null +++ b/core/java/android/service/autofill/VisibilitySetterAction.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; +import static android.view.autofill.Helper.sVerbose; + +import android.annotation.IdRes; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Slog; +import android.util.SparseIntArray; +import android.view.View; +import android.view.View.Visibility; +import android.view.ViewGroup; +import android.widget.RemoteViews; + +import com.android.internal.util.Preconditions; + +/** + * Action used to change the visibility of other child view in a {@link CustomDescription} + * {@link RemoteViews presentation template}. + * + * <p>See {@link CustomDescription.Builder#addOnClickAction(int, OnClickAction)} for more details. + */ +public final class VisibilitySetterAction extends InternalOnClickAction implements + OnClickAction, Parcelable { + private static final String TAG = "VisibilitySetterAction"; + + @NonNull private final SparseIntArray mVisibilities; + + private VisibilitySetterAction(@NonNull Builder builder) { + mVisibilities = builder.mVisibilities; + } + + /** @hide */ + @Override + public void onClick(@NonNull ViewGroup rootView) { + for (int i = 0; i < mVisibilities.size(); i++) { + final int id = mVisibilities.keyAt(i); + final View child = rootView.findViewById(id); + if (child == null) { + Slog.w(TAG, "Skipping view id " + id + " because it's not found on " + rootView); + continue; + } + final int visibility = mVisibilities.valueAt(i); + if (sVerbose) { + Slog.v(TAG, "Changing visibility of view " + child + " from " + + child.getVisibility() + " to " + visibility); + } + child.setVisibility(visibility); + } + } + + /** + * Builder for {@link VisibilitySetterAction} objects. + */ + public static class Builder { + private final SparseIntArray mVisibilities = new SparseIntArray(); + private boolean mDestroyed; + + /** + * Creates a new builder for an action that change the visibility of one child view. + * + * @param id view resource id of the children view. + * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or + * {@link View#GONE}. + * @throw {@link IllegalArgumentException} if visibility is not one of {@link View#VISIBLE}, + * {@link View#INVISIBLE}, or {@link View#GONE}. + */ + public Builder(@IdRes int id, @Visibility int visibility) { + setVisibility(id, visibility); + } + + /** + * Sets the action to changes the visibility of a child view. + * + * @param id view resource id of the children view. + * @param visibility one of {@link View#VISIBLE}, {@link View#INVISIBLE}, or + * {@link View#GONE}. + * @throw {@link IllegalArgumentException} if visibility is not one of {@link View#VISIBLE}, + * {@link View#INVISIBLE}, or {@link View#GONE}. + */ + public Builder setVisibility(@IdRes int id, @Visibility int visibility) { + throwIfDestroyed(); + switch (visibility) { + case View.VISIBLE: + case View.INVISIBLE: + case View.GONE: + mVisibilities.put(id, visibility); + return this; + } + throw new IllegalArgumentException("Invalid visibility: " + visibility); + } + + /** + * Creates a new {@link VisibilitySetterAction} instance. + */ + public VisibilitySetterAction build() { + throwIfDestroyed(); + mDestroyed = true; + return new VisibilitySetterAction(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already called build()"); + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "VisibilitySetterAction: [" + mVisibilities + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeSparseIntArray(mVisibilities); + } + + public static final Parcelable.Creator<VisibilitySetterAction> CREATOR = + new Parcelable.Creator<VisibilitySetterAction>() { + @Override + public VisibilitySetterAction createFromParcel(Parcel parcel) { + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + final SparseIntArray visibilities = parcel.readSparseIntArray(); + Builder builder = null; + for (int i = 0; i < visibilities.size(); i++) { + final int id = visibilities.keyAt(i); + final int visibility = visibilities.valueAt(i); + if (builder == null) { + builder = new Builder(id, visibility); + } else { + builder.setVisibility(id, visibility); + } + } + return builder == null ? null : builder.build(); + } + + @Override + public VisibilitySetterAction[] newArray(int size) { + return new VisibilitySetterAction[size]; + } + }; +} diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 0812cb992100..760e85e1e77b 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -35,6 +35,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.service.autofill.BatchUpdates; import android.service.autofill.CustomDescription; +import android.service.autofill.InternalOnClickAction; import android.service.autofill.InternalTransformation; import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; @@ -43,6 +44,7 @@ import android.text.Html; import android.util.ArraySet; import android.util.Pair; import android.util.Slog; +import android.util.SparseArray; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; @@ -337,7 +339,7 @@ final class SaveUi { template.setApplyTheme(THEME_ID); final View customSubtitleView = template.apply(context, null, handler); - // And apply batch updates (if any). + // Apply batch updates (if any). final ArrayList<Pair<InternalValidator, BatchUpdates>> updates = customDescription.getUpdates(); if (updates != null) { @@ -376,6 +378,35 @@ final class SaveUi { } } + // Apply click actions (if any). + final SparseArray<InternalOnClickAction> actions = customDescription.getActions(); + if (actions != null) { + final int size = actions.size(); + if (sDebug) Slog.d(TAG, "custom description has " + size + " actions"); + if (!(customSubtitleView instanceof ViewGroup)) { + Slog.w(TAG, "cannot apply actions because custom description root is not a " + + "ViewGroup: " + customSubtitleView); + } else { + final ViewGroup rootView = (ViewGroup) customSubtitleView; + for (int i = 0; i < size; i++) { + final int id = actions.keyAt(i); + final InternalOnClickAction action = actions.valueAt(i); + final View child = rootView.findViewById(id); + if (child == null) { + Slog.w(TAG, "Ignoring action " + action + " for view " + id + + " because it's not on " + rootView); + continue; + } + child.setOnClickListener((v) -> { + if (sVerbose) { + Slog.v(TAG, "Applying " + action + " after " + v + " was clicked"); + } + action.onClick(rootView); + }); + } + } + } + // Finally, add the custom description to the save UI. final ViewGroup subtitleContainer = saveUiView.findViewById(R.id.autofill_save_custom_subtitle); |