diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-10-10 15:20:13 -0400 |
commit | 93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6 (patch) | |
tree | 49f76f879a89c256a4f65b674086be50760bdffb /android/service | |
parent | bc81c7ada5aab3806dd0b17498f5c9672c9b33c4 (diff) | |
download | android-28-93b7ee4fce01df52a6607f0b1965cbafdfeaf1a6.tar.gz |
Import Android SDK Platform P [4386628]
/google/data/ro/projects/android/fetch_artifact \
--bid 4386628 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4386628.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: I9b8400ac92116cae4f033d173f7a5682b26ccba9
Diffstat (limited to 'android/service')
17 files changed, 1047 insertions, 84 deletions
diff --git a/android/service/autofill/AutofillService.java b/android/service/autofill/AutofillService.java index 3e08dcf2..2e59f6c5 100644 --- a/android/service/autofill/AutofillService.java +++ b/android/service/autofill/AutofillService.java @@ -187,7 +187,7 @@ import com.android.internal.os.SomeArgs; * protect a dataset that contains sensitive information by requiring dataset authentication * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include * info about the "primary" field of the partition in the custom presentation for "secondary" - * fields — that would prevent a malicious app from getting the "primary" fields without the + * fields—that would prevent a malicious app from getting the "primary" fields without the * user realizing they're being released (for example, a malicious app could have fields for a * credit card number, verification code, and expiration date crafted in a way that just the latter * is visible; by explicitly indicating the expiration date is related to a given credit card @@ -305,7 +305,7 @@ import com.android.internal.os.SomeArgs; * <li>Use the {@link android.app.assist.AssistStructure.ViewNode#getWebDomain()} to get the * source of the document. * <li>Get the canonical domain using the - * <a href="https://publicsuffix.org/>Public Suffix List</a> (see example below). + * <a href="https://publicsuffix.org/">Public Suffix List</a> (see example below). * <li>Use <a href="https://developers.google.com/digital-asset-links/">Digital Asset Links</a> * to obtain the package name and certificate fingerprint of the package corresponding to * the canonical domain. @@ -503,13 +503,19 @@ public abstract class AutofillService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull FillCallback callback); /** - * Called when user requests service to save the fields of a screen. + * Called when the user requests the service to save the contents of a screen. * * <p>Service must call one of the {@link SaveCallback} methods (like * {@link SaveCallback#onSuccess()} or {@link SaveCallback#onFailure(CharSequence)}) - * to notify the result of the request. + * to notify the Android System of the result of the request. + * + * <p>If the service could not handle the request right away—for example, because it must + * launch an activity asking the user to authenticate first or because the network is + * down—the service could keep the {@link SaveRequest request} and reuse it later, + * but the service must call {@link SaveCallback#onSuccess()} right away. * - * <p><b>Note:</b> To retrieve the actual value of the field, the service should call + * <p><b>Note:</b> To retrieve the actual value of fields input by the user, the service + * should call * {@link android.app.assist.AssistStructure.ViewNode#getAutofillValue()}; if it calls * {@link android.app.assist.AssistStructure.ViewNode#getText()} or other methods, there is no * guarantee such method will return the most recent value of the field. diff --git a/android/service/autofill/AutofillServiceInfo.java b/android/service/autofill/AutofillServiceInfo.java index f1474006..5c7388f7 100644 --- a/android/service/autofill/AutofillServiceInfo.java +++ b/android/service/autofill/AutofillServiceInfo.java @@ -146,4 +146,9 @@ public final class AutofillServiceInfo { public String getSettingsActivity() { return mSettingsActivity; } + + @Override + public String toString() { + return mServiceInfo == null ? "null" : mServiceInfo.toString(); + } } diff --git a/android/service/autofill/Dataset.java b/android/service/autofill/Dataset.java index cb341b1d..ef9598aa 100644 --- a/android/service/autofill/Dataset.java +++ b/android/service/autofill/Dataset.java @@ -29,32 +29,77 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; +import java.io.Serializable; import java.util.ArrayList; +import java.util.regex.Pattern; /** - * A dataset object represents a group of key/value pairs used to autofill parts of a screen. + * A dataset object represents a group of fields (key / value pairs) used to autofill parts of a + * screen. * - * <p>In its simplest form, a dataset contains one or more key / value pairs (comprised of - * {@link AutofillId} and {@link AutofillValue} respectively); and one or more - * {@link RemoteViews presentation} for these pairs (a pair could have its own - * {@link RemoteViews presentation}, or use the default {@link RemoteViews presentation} associated - * with the whole dataset). When an autofill service returns datasets in a {@link FillResponse} + * <a name="BasicUsage"></a> + * <h3>Basic usage</h3> + * + * <p>In its simplest form, a dataset contains one or more fields (comprised of + * an {@link AutofillId id}, a {@link AutofillValue value}, and an optional filter + * {@link Pattern regex}); and one or more {@link RemoteViews presentations} for these fields + * (each field could have its own {@link RemoteViews presentation}, or use the default + * {@link RemoteViews presentation} associated with the whole dataset). + * + * <p>When an autofill service returns datasets in a {@link FillResponse} * and the screen input is focused in a view that is present in at least one of these datasets, - * the Android System displays a UI affordance containing the {@link RemoteViews presentation} of + * the Android System displays a UI containing the {@link RemoteViews presentation} of * all datasets pairs that have that view's {@link AutofillId}. Then, when the user selects a - * dataset from the affordance, all views in that dataset are autofilled. + * dataset from the UI, all views in that dataset are autofilled. + * + * <a name="Authentication"></a> + * <h3>Dataset authentication</h3> + * + * <p>In a more sophisticated form, the dataset values can be protected until the user authenticates + * the dataset—in that case, when a dataset is selected by the user, the Android System + * launches an intent set by the service to "unlock" the dataset. + * + * <p>For example, when a data set contains credit card information (such as number, + * expiration date, and verification code), you could provide a dataset presentation saying + * "Tap to authenticate". Then when the user taps that option, you would launch an activity asking + * the user to enter the credit card code, and if the user enters a valid code, you could then + * "unlock" the dataset. + * + * <p>You can also use authenticated datasets to offer an interactive UI for the user. For example, + * if the activity being autofilled is an account creation screen, you could use an authenticated + * dataset to automatically generate a random password for the user. * - * <p>In a more sophisticated form, the dataset value can be protected until the user authenticates - * the dataset - see {@link Dataset.Builder#setAuthentication(IntentSender)}. + * <p>See {@link Dataset.Builder#setAuthentication(IntentSender)} for more details about the dataset + * authentication mechanism. * - * @see android.service.autofill.AutofillService for more information and examples about the - * role of datasets in the autofill workflow. + * <a name="Filtering"></a> + * <h3>Filtering</h3> + * <p>The autofill UI automatically changes which values are shown based on value of the view + * anchoring it, following the rules below: + * <ol> + * <li>If the view's {@link android.view.View#getAutofillValue() autofill value} is not + * {@link AutofillValue#isText() text} or is empty, all datasets are shown. + * <li>Datasets that have a filter regex (set through + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern)} or + * {@link Dataset.Builder#setValue(AutofillId, AutofillValue, Pattern, RemoteViews)}) and whose + * regex matches the view's text value converted to lower case are shown. + * <li>Datasets that do not require authentication, have a field value that is + * {@link AutofillValue#isText() text} and whose {@link AutofillValue#getTextValue() value} starts + * with the lower case value of the view's text are shown. + * <li>All other datasets are hidden. + * </ol> + * + * <a name="MoreInfo"></a> + * <h3>More information</h3> + * <p>See {@link android.service.autofill.AutofillService} for more information and examples about + * the role of datasets in the autofill workflow. */ public final class Dataset implements Parcelable { private final ArrayList<AutofillId> mFieldIds; private final ArrayList<AutofillValue> mFieldValues; private final ArrayList<RemoteViews> mFieldPresentations; + private final ArrayList<Pattern> mFieldFilters; private final RemoteViews mPresentation; private final IntentSender mAuthentication; @Nullable String mId; @@ -63,6 +108,7 @@ public final class Dataset implements Parcelable { mFieldIds = builder.mFieldIds; mFieldValues = builder.mFieldValues; mFieldPresentations = builder.mFieldPresentations; + mFieldFilters = builder.mFieldFilters; mPresentation = builder.mPresentation; mAuthentication = builder.mAuthentication; mId = builder.mId; @@ -85,6 +131,12 @@ public final class Dataset implements Parcelable { } /** @hide */ + @Nullable + public Pattern getFilter(int index) { + return mFieldFilters.get(index); + } + + /** @hide */ public @Nullable IntentSender getAuthentication() { return mAuthentication; } @@ -103,6 +155,8 @@ public final class Dataset implements Parcelable { .append(", fieldValues=").append(mFieldValues) .append(", fieldPresentations=") .append(mFieldPresentations == null ? 0 : mFieldPresentations.size()) + .append(", fieldFilters=") + .append(mFieldFilters == null ? 0 : mFieldFilters.size()) .append(", hasPresentation=").append(mPresentation != null) .append(", hasAuthentication=").append(mAuthentication != null) .append(']').toString(); @@ -127,6 +181,7 @@ public final class Dataset implements Parcelable { private ArrayList<AutofillId> mFieldIds; private ArrayList<AutofillValue> mFieldValues; private ArrayList<RemoteViews> mFieldPresentations; + private ArrayList<Pattern> mFieldFilters; private RemoteViews mPresentation; private IntentSender mAuthentication; private boolean mDestroyed; @@ -182,12 +237,12 @@ public final class Dataset implements Parcelable { * credit card information without the CVV for the data set in the {@link FillResponse * response} then the returned data set should contain the CVV entry. * - * <p><b>NOTE:</b> Do not make the provided pending intent + * <p><b>Note:</b> Do not make the provided pending intent * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the * platform needs to fill in the authentication arguments. * * @param authentication Intent to an activity with your authentication flow. - * @return This builder. + * @return this builder. * * @see android.app.PendingIntent */ @@ -214,11 +269,10 @@ public final class Dataset implements Parcelable { * * @param id id for this dataset or {@code null} to unset. * - * @return This builder. + * @return this builder. */ public @NonNull Builder setId(@Nullable String id) { throwIfDestroyed(); - mId = id; return this; } @@ -230,17 +284,16 @@ public final class Dataset implements Parcelable { * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. * @param value 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 an authentication and you have no access to the value. - * @return This builder. + * the dataset needs authentication and you have no access to the value. + * @return this builder. * @throws IllegalStateException if the builder was constructed without a * {@link RemoteViews presentation}. */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) { throwIfDestroyed(); - if (mPresentation == null) { - throw new IllegalStateException("Dataset presentation not set on constructor"); - } - setValueAndPresentation(id, value, null); + Preconditions.checkState(mPresentation != null, + "Dataset presentation not set on constructor"); + setLifeTheUniverseAndEverything(id, value, null, null); return this; } @@ -250,23 +303,81 @@ public final class Dataset implements Parcelable { * * @param id id returned by {@link * android.app.assist.AssistStructure.ViewNode#getAutofillId()}. - * @param value value to be auto filled. Pass {@code null} if you do not have the value + * @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 an authentication and you have no access to the value. - * Filtering matches any user typed string to {@code null} values. - * @param presentation The presentation used to visualize this field. - * @return This builder. + * the dataset needs authentication and you have no access to the value. + * @param presentation the presentation used to visualize this field. + * @return this builder. + * */ public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, @NonNull RemoteViews presentation) { throwIfDestroyed(); Preconditions.checkNotNull(presentation, "presentation cannot be null"); - setValueAndPresentation(id, value, presentation); + setLifeTheUniverseAndEverything(id, value, presentation, null); + return this; + } + + /** + * Sets the value of a field using an <a href="#Filtering">explicit filter</a>. + * + * <p>This method is typically used when the dataset is not authenticated and the field + * value is not {@link AutofillValue#isText() text} but the service still wants to allow + * the user to filter it out. + * + * @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. + * + * @return this builder. + * @throws IllegalStateException if the builder was constructed without a + * {@link RemoteViews presentation}. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkState(mPresentation != null, + "Dataset presentation not set on constructor"); + setLifeTheUniverseAndEverything(id, value, null, filter); + return this; + } + + /** + * Sets the value of a field, using a custom {@link RemoteViews presentation} to + * visualize it and a <a href="#Filtering">explicit filter</a>. + * + * <p>Typically used to allow filtering on + * {@link Dataset.Builder#setAuthentication(IntentSender) authenticated datasets}. For + * example, if the dataset represents a credit card number and the service does not want to + * show the "Tap to authenticate" message until the user tapped 4 digits, in which case + * the filter would be {@code Pattern.compile("\\d.{4,}")}. + * + * @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 filter regex used to determine if the dataset should be shown in the autofill UI. + * + * @return this builder. + */ + public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value, + @NonNull Pattern filter, @NonNull RemoteViews presentation) { + throwIfDestroyed(); + Preconditions.checkNotNull(filter, "filter cannot be null"); + Preconditions.checkNotNull(presentation, "presentation cannot be null"); + setLifeTheUniverseAndEverything(id, value, presentation, filter); return this; } - private void setValueAndPresentation(AutofillId id, AutofillValue value, - RemoteViews presentation) { + private void setLifeTheUniverseAndEverything(@NonNull AutofillId id, + @Nullable AutofillValue value, @Nullable RemoteViews presentation, + @Nullable Pattern filter) { Preconditions.checkNotNull(id, "id cannot be null"); if (mFieldIds != null) { final int existingIdx = mFieldIds.indexOf(id); @@ -279,10 +390,12 @@ public final class Dataset implements Parcelable { mFieldIds = new ArrayList<>(); mFieldValues = new ArrayList<>(); mFieldPresentations = new ArrayList<>(); + mFieldFilters = new ArrayList<>(); } mFieldIds.add(id); mFieldValues.add(value); mFieldPresentations.add(presentation); + mFieldFilters.add(filter); } /** @@ -290,8 +403,9 @@ public final class Dataset implements Parcelable { * * <p>You should not interact with this builder once this method is called. * - * <p>It is required that you specify at least one field before calling this method. It's - * also mandatory to provide a presentation view to visualize the data set in the UI. + * @throws IllegalStateException if no field was set (through + * {@link #setValue(AutofillId, AutofillValue)} or + * {@link #setValue(AutofillId, AutofillValue, RemoteViews)}). * * @return The built dataset. */ @@ -299,7 +413,7 @@ public final class Dataset implements Parcelable { throwIfDestroyed(); mDestroyed = true; if (mFieldIds == null) { - throw new IllegalArgumentException("at least one value must be set"); + throw new IllegalStateException("at least one value must be set"); } return new Dataset(this); } @@ -323,9 +437,10 @@ public final class Dataset implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeParcelable(mPresentation, flags); - parcel.writeTypedArrayList(mFieldIds, flags); - parcel.writeTypedArrayList(mFieldValues, flags); + parcel.writeTypedList(mFieldIds, flags); + parcel.writeTypedList(mFieldValues, flags); parcel.writeParcelableList(mFieldPresentations, flags); + parcel.writeSerializable(mFieldFilters); parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); } @@ -340,10 +455,14 @@ public final class Dataset implements Parcelable { final Builder builder = (presentation == null) ? new Builder() : new Builder(presentation); - final ArrayList<AutofillId> ids = parcel.readTypedArrayList(null); - final ArrayList<AutofillValue> values = parcel.readTypedArrayList(null); + final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR); + final ArrayList<AutofillValue> values = + parcel.createTypedArrayList(AutofillValue.CREATOR); final ArrayList<RemoteViews> presentations = new ArrayList<>(); parcel.readParcelableList(presentations, null); + @SuppressWarnings("unchecked") + final ArrayList<Serializable> filters = + (ArrayList<Serializable>) parcel.readSerializable(); final int idCount = (ids != null) ? ids.size() : 0; final int valueCount = (values != null) ? values.size() : 0; for (int i = 0; i < idCount; i++) { @@ -351,7 +470,8 @@ public final class Dataset implements Parcelable { final AutofillValue value = (valueCount > i) ? values.get(i) : null; final RemoteViews fieldPresentation = presentations.isEmpty() ? null : presentations.get(i); - builder.setValueAndPresentation(id, value, fieldPresentation); + final Pattern filter = (Pattern) filters.get(i); + builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter); } builder.setAuthentication(parcel.readParcelable(null)); builder.setId(parcel.readString()); diff --git a/android/service/autofill/ImageTransformation.java b/android/service/autofill/ImageTransformation.java index 2151f74f..4afda249 100644 --- a/android/service/autofill/ImageTransformation.java +++ b/android/service/autofill/ImageTransformation.java @@ -20,11 +20,12 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.DrawableRes; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.TestApi; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.autofill.AutofillId; import android.widget.ImageView; import android.widget.RemoteViews; @@ -43,9 +44,9 @@ import java.util.regex.Pattern; * * <pre class="prettyprint"> * new ImageTransformation.Builder(ccNumberId, Pattern.compile("^4815.*$"), - * R.drawable.ic_credit_card_logo1) - * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2) - * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3) + * R.drawable.ic_credit_card_logo1, "Brand 1") + * .addOption(Pattern.compile("^1623.*$"), R.drawable.ic_credit_card_logo2, "Brand 2") + * .addOption(Pattern.compile("^42.*$"), R.drawable.ic_credit_card_logo3, "Brand 3") * .build(); * </pre> * @@ -59,7 +60,7 @@ public final class ImageTransformation extends InternalTransformation implements private static final String TAG = "ImageTransformation"; private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions; + private final ArrayList<Option> mOptions; private ImageTransformation(Builder builder) { mId = builder.mId; @@ -82,17 +83,21 @@ public final class ImageTransformation extends InternalTransformation implements } for (int i = 0; i < size; i++) { - final Pair<Pattern, Integer> option = mOptions.get(i); + final Option option = mOptions.get(i); try { - if (option.first.matcher(value).matches()) { + if (option.pattern.matcher(value).matches()) { Log.d(TAG, "Found match at " + i + ": " + option); - parentTemplate.setImageViewResource(childViewId, option.second); + parentTemplate.setImageViewResource(childViewId, option.resId); + if (option.contentDescription != null) { + parentTemplate.setContentDescription(childViewId, + option.contentDescription); + } return; } } catch (Exception e) { // Do not log full exception to avoid PII leaking - Log.w(TAG, "Error matching regex #" + i + "(" + option.first.pattern() + ") on id " - + option.second + ": " + e.getClass()); + Log.w(TAG, "Error matching regex #" + i + "(" + option.pattern + ") on id " + + option.resId + ": " + e.getClass()); throw e; } @@ -105,25 +110,44 @@ public final class ImageTransformation extends InternalTransformation implements */ public static class Builder { private final AutofillId mId; - private final ArrayList<Pair<Pattern, Integer>> mOptions = new ArrayList<>(); + private final ArrayList<Option> mOptions = new ArrayList<>(); private boolean mDestroyed; /** - * Create a new builder for a autofill id and add a first option. + * Creates a new builder for a autofill id and add a first option. * * @param id id of the screen field that will be used to evaluate whether the image should * be used. * @param regex regular expression defining what should be matched to use this image. * @param resId resource id of the image (in the autofill service's package). The * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * + * @deprecated use + * {@link #ImageTransformation.Builder(AutofillId, Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId) { mId = Preconditions.checkNotNull(id); - addOption(regex, resId); } /** + * Creates a new builder for a autofill id and add a first option. + * + * @param id id of the screen field that will be used to evaluate whether the image should + * be used. + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + */ + public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + mId = Preconditions.checkNotNull(id); + addOption(regex, resId, contentDescription); + } + + /** * Adds an option to replace the child view with a different image when the regex matches. * * @param regex regular expression defining what should be matched to use this image. @@ -131,17 +155,43 @@ public final class ImageTransformation extends InternalTransformation implements * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. * * @return this build + * + * @deprecated use {@link #addOption(Pattern, int, CharSequence)} instead. */ + @Deprecated public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId) { + addOptionInternal(regex, resId, null); + return this; + } + + /** + * Adds an option to replace the child view with a different image and content description + * when the regex matches. + * + * @param regex regular expression defining what should be matched to use this image. + * @param resId resource id of the image (in the autofill service's package). The + * {@link RemoteViews presentation} must contain a {@link ImageView} child with that id. + * @param contentDescription content description to be applied in the child view. + * + * @return this build + */ + public Builder addOption(@NonNull Pattern regex, @DrawableRes int resId, + @NonNull CharSequence contentDescription) { + addOptionInternal(regex, resId, Preconditions.checkNotNull(contentDescription)); + return this; + } + + private void addOptionInternal(@NonNull Pattern regex, @DrawableRes int resId, + @Nullable CharSequence contentDescription) { throwIfDestroyed(); Preconditions.checkNotNull(regex); Preconditions.checkArgument(resId != 0); - mOptions.add(new Pair<>(regex, resId)); - return this; + mOptions.add(new Option(regex, resId, contentDescription)); } + /** * Creates a new {@link ImageTransformation} instance. */ @@ -178,15 +228,18 @@ public final class ImageTransformation extends InternalTransformation implements parcel.writeParcelable(mId, flags); final int size = mOptions.size(); - final Pattern[] regexs = new Pattern[size]; + final Pattern[] patterns = new Pattern[size]; final int[] resIds = new int[size]; + final CharSequence[] contentDescriptions = new String[size]; for (int i = 0; i < size; i++) { - Pair<Pattern, Integer> regex = mOptions.get(i); - regexs[i] = regex.first; - resIds[i] = regex.second; + final Option option = mOptions.get(i); + patterns[i] = option.pattern; + resIds[i] = option.resId; + contentDescriptions[i] = option.contentDescription; } - parcel.writeSerializable(regexs); + parcel.writeSerializable(patterns); parcel.writeIntArray(resIds); + parcel.writeCharSequenceArray(contentDescriptions); } public static final Parcelable.Creator<ImageTransformation> CREATOR = @@ -197,15 +250,22 @@ public final class ImageTransformation extends InternalTransformation implements final Pattern[] regexs = (Pattern[]) parcel.readSerializable(); final int[] resIds = parcel.createIntArray(); + final CharSequence[] contentDescriptions = parcel.readCharSequenceArray(); // Always go through the builder to ensure the data ingested by the system obeys the // contract of the builder to avoid attacks using specially crafted parcels. - final ImageTransformation.Builder builder = new ImageTransformation.Builder(id, - regexs[0], resIds[0]); + final CharSequence contentDescription = contentDescriptions[0]; + final ImageTransformation.Builder builder = (contentDescription != null) + ? new ImageTransformation.Builder(id, regexs[0], resIds[0], contentDescription) + : new ImageTransformation.Builder(id, regexs[0], resIds[0]); final int size = regexs.length; for (int i = 1; i < size; i++) { - builder.addOption(regexs[i], resIds[i]); + if (contentDescriptions[i] != null) { + builder.addOption(regexs[i], resIds[i], contentDescriptions[i]); + } else { + builder.addOption(regexs[i], resIds[i]); + } } return builder.build(); @@ -216,4 +276,16 @@ public final class ImageTransformation extends InternalTransformation implements return new ImageTransformation[size]; } }; + + private static final class Option { + public final Pattern pattern; + public final int resId; + public final CharSequence contentDescription; + + Option(Pattern pattern, int resId, CharSequence contentDescription) { + this.pattern = pattern; + this.resId = resId; + this.contentDescription = TextUtils.trimNoCopySpans(contentDescription); + } + } } diff --git a/android/service/autofill/InternalSanitizer.java b/android/service/autofill/InternalSanitizer.java new file mode 100644 index 00000000..95d2f660 --- /dev/null +++ b/android/service/autofill/InternalSanitizer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all sanitizers the system understands. As this is not public all public subclasses + * have to implement {@link Sanitizer} again. + * + * @hide + */ +@TestApi +public abstract class InternalSanitizer implements Sanitizer, Parcelable { + + /** + * Sanitizes an {@link AutofillValue}. + * + * @hide + */ + public abstract AutofillValue sanitize(@NonNull AutofillValue value); +} diff --git a/android/service/autofill/Sanitizer.java b/android/service/autofill/Sanitizer.java new file mode 100644 index 00000000..38757ac7 --- /dev/null +++ b/android/service/autofill/Sanitizer.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill; + +/** + * Helper class used to sanitize user input before using it in a save request. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted + * by the app—for example, if the autofill service sends a credit card number + * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108". + */ +public interface Sanitizer { +} diff --git a/android/service/autofill/SaveCallback.java b/android/service/autofill/SaveCallback.java index 3a701384..7207f1df 100644 --- a/android/service/autofill/SaveCallback.java +++ b/android/service/autofill/SaveCallback.java @@ -34,9 +34,13 @@ public final class SaveCallback { /** * Notifies the Android System that an - * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully fulfilled + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} was successfully handled * by the service. * + * <p>If the service could not handle the request right away—for example, because it must + * launch an activity asking the user to authenticate first or because the network is + * down—it should still call {@link #onSuccess()}. + * * @throws RuntimeException if an error occurred while calling the Android System. */ public void onSuccess() { @@ -51,9 +55,16 @@ public final class SaveCallback { /** * Notifies the Android System that an - * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be fulfilled + * {@link AutofillService#onSaveRequest(SaveRequest, SaveCallback)} could not be handled * by the service. * + * <p>This method should only be called when the service could not handle the request right away + * and could not recover or retry it. If the service could retry or recover, it could keep + * the {@link SaveRequest} and call {@link #onSuccess()} instead. + * + * <p><b>Note:</b> The Android System displays an UI with the supplied error message; if + * you prefer to show your own message, call {@link #onSuccess()} instead. + * * @param message error message to be displayed to the user. * * @throws RuntimeException if an error occurred while calling the Android System. diff --git a/android/service/autofill/SaveInfo.java b/android/service/autofill/SaveInfo.java index e0a07305..1b9240cc 100644 --- a/android/service/autofill/SaveInfo.java +++ b/android/service/autofill/SaveInfo.java @@ -25,6 +25,8 @@ import android.app.Activity; import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -232,6 +234,8 @@ public final class SaveInfo implements Parcelable { private final int mFlags; private final CustomDescription mCustomDescription; private final InternalValidator mValidator; + private final InternalSanitizer[] mSanitizerKeys; + private final AutofillId[][] mSanitizerValues; private SaveInfo(Builder builder) { mType = builder.mType; @@ -243,6 +247,18 @@ public final class SaveInfo implements Parcelable { mFlags = builder.mFlags; mCustomDescription = builder.mCustomDescription; mValidator = builder.mValidator; + if (builder.mSanitizers == null) { + mSanitizerKeys = null; + mSanitizerValues = null; + } else { + final int size = builder.mSanitizers.size(); + mSanitizerKeys = new InternalSanitizer[size]; + mSanitizerValues = new AutofillId[size][]; + for (int i = 0; i < size; i++) { + mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); + mSanitizerValues[i] = builder.mSanitizers.valueAt(i); + } + } } /** @hide */ @@ -292,6 +308,18 @@ public final class SaveInfo implements Parcelable { return mValidator; } + /** @hide */ + @Nullable + public InternalSanitizer[] getSanitizerKeys() { + return mSanitizerKeys; + } + + /** @hide */ + @Nullable + public AutofillId[][] getSanitizerValues() { + return mSanitizerValues; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -307,6 +335,9 @@ public final class SaveInfo implements Parcelable { private int mFlags; private CustomDescription mCustomDescription; private InternalValidator mValidator; + private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; + // Set used to validate against duplicate ids. + private ArraySet<AutofillId> mSanitizerIds; /** * Creates a new builder. @@ -530,6 +561,61 @@ public final class SaveInfo implements Parcelable { } /** + * Adds a sanitizer for one or more field. + * + * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the + * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but + * reformattedby the app. For example, to remove spaces between every 4 digits of a + * credit card number: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), + * "$1$2$3$4"), ccNumberId); + * </pre> + * + * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim + * both the username and password fields: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), + * usernameId, passwordId); + * </pre> + * + * @param sanitizer an implementation provided by the Android System. + * @param ids id of fields whose value will be sanitized. + * @return this builder. + * + * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already + * been added or if {@code ids} is empty. + */ + public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, + @NonNull AutofillId... ids) { + throwIfDestroyed(); + Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); + Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), + "not provided by Android System: " + sanitizer); + + if (mSanitizers == null) { + mSanitizers = new ArrayMap<>(); + mSanitizerIds = new ArraySet<>(ids.length); + } + + // Check for duplicates first. + for (AutofillId id : ids) { + Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); + mSanitizerIds.add(id); + } + + mSanitizers.put((InternalSanitizer) sanitizer, ids); + + return this; + } + + /** * Builds a new {@link SaveInfo} instance. * * @throws IllegalStateException if no @@ -569,6 +655,10 @@ public final class SaveInfo implements Parcelable { .append(", mFlags=").append(mFlags) .append(", mCustomDescription=").append(mCustomDescription) .append(", validation=").append(mValidator) + .append(", sanitizerKeys=") + .append(mSanitizerKeys == null ? "N/A:" : mSanitizerKeys.length) + .append(", sanitizerValues=") + .append(mSanitizerValues == null ? "N/A:" : mSanitizerValues.length) .append("]").toString(); } @@ -591,6 +681,12 @@ public final class SaveInfo implements Parcelable { parcel.writeCharSequence(mDescription); parcel.writeParcelable(mCustomDescription, flags); parcel.writeParcelable(mValidator, flags); + parcel.writeParcelableArray(mSanitizerKeys, flags); + if (mSanitizerKeys != null) { + for (int i = 0; i < mSanitizerValues.length; i++) { + parcel.writeParcelableArray(mSanitizerValues[i], flags); + } + } parcel.writeInt(mFlags); } @@ -621,6 +717,16 @@ public final class SaveInfo implements Parcelable { if (validator != null) { builder.setValidator(validator); } + final InternalSanitizer[] sanitizers = + parcel.readParcelableArray(null, InternalSanitizer.class); + if (sanitizers != null) { + final int size = sanitizers.length; + for (int i = 0; i < size; i++) { + final AutofillId[] autofillIds = + parcel.readParcelableArray(null, AutofillId.class); + builder.addSanitizer(sanitizers[i], autofillIds); + } + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/android/service/autofill/SaveRequest.java b/android/service/autofill/SaveRequest.java index 1a6c5b0b..65fdb5c4 100644 --- a/android/service/autofill/SaveRequest.java +++ b/android/service/autofill/SaveRequest.java @@ -48,7 +48,8 @@ public final class SaveRequest implements Parcelable { } private SaveRequest(@NonNull Parcel parcel) { - this(parcel.readTypedArrayList(null), parcel.readBundle(), parcel.createStringArrayList()); + this(parcel.createTypedArrayList(FillContext.CREATOR), + parcel.readBundle(), parcel.createStringArrayList()); } /** @@ -84,7 +85,7 @@ public final class SaveRequest implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - parcel.writeTypedArrayList(mFillContexts, flags); + parcel.writeTypedList(mFillContexts, flags); parcel.writeBundle(mClientState); parcel.writeStringList(mDatasetIds); } diff --git a/android/service/autofill/TextValueSanitizer.java b/android/service/autofill/TextValueSanitizer.java new file mode 100644 index 00000000..12e85b1d --- /dev/null +++ b/android/service/autofill/TextValueSanitizer.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Slog; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution. + * + * <p>For example, to remove spaces from groups of 4-digits in a credit card: + * + * <pre class="prettyprint"> + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), "$1$2$3$4") + * </pre> + */ +public final class TextValueSanitizer extends InternalSanitizer implements + Sanitizer, Parcelable { + private static final String TAG = "TextValueSanitizer"; + + private final Pattern mRegex; + private final String mSubst; + + /** + * Default constructor. + * + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + */ + public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) { + mRegex = Preconditions.checkNotNull(regex); + mSubst = Preconditions.checkNotNull(subst); + } + + /** @hide */ + @Override + @TestApi + public AutofillValue sanitize(@NonNull AutofillValue value) { + if (value == null) { + Slog.w(TAG, "sanitize() called with null value"); + return null; + } + if (!value.isText()) return value; + + final CharSequence text = value.getTextValue(); + + try { + final Matcher matcher = mRegex.matcher(text); + if (!matcher.matches()) return value; + + final CharSequence sanitized = matcher.replaceAll(mSubst); + return AutofillValue.forText(sanitized); + } catch (Exception e) { + Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e); + return value; + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeSerializable(mRegex); + parcel.writeString(mSubst); + } + + public static final Parcelable.Creator<TextValueSanitizer> CREATOR = + new Parcelable.Creator<TextValueSanitizer>() { + @Override + public TextValueSanitizer createFromParcel(Parcel parcel) { + return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString()); + } + + @Override + public TextValueSanitizer[] newArray(int size) { + return new TextValueSanitizer[size]; + } + }; +} diff --git a/android/service/carrier/CarrierService.java b/android/service/carrier/CarrierService.java index 813acc23..2707f146 100644 --- a/android/service/carrier/CarrierService.java +++ b/android/service/carrier/CarrierService.java @@ -17,10 +17,13 @@ package android.service.carrier; import android.annotation.CallSuper; import android.app.Service; import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; +import android.util.Log; import com.android.internal.telephony.ITelephonyRegistry; @@ -48,6 +51,8 @@ import com.android.internal.telephony.ITelephonyRegistry; */ public abstract class CarrierService extends Service { + private static final String LOG_TAG = "CarrierService"; + public static final String CARRIER_SERVICE_INTERFACE = "android.service.carrier.CarrierService"; private static ITelephonyRegistry sRegistry; @@ -133,11 +138,26 @@ public abstract class CarrierService extends Service { /** * A wrapper around ICarrierService that forwards calls to implementations of * {@link CarrierService}. + * @hide */ - private class ICarrierServiceWrapper extends ICarrierService.Stub { + public class ICarrierServiceWrapper extends ICarrierService.Stub { + /** @hide */ + public static final int RESULT_OK = 0; + /** @hide */ + public static final int RESULT_ERROR = 1; + /** @hide */ + public static final String KEY_CONFIG_BUNDLE = "config_bundle"; + @Override - public PersistableBundle getCarrierConfig(CarrierIdentifier id) { - return CarrierService.this.onLoadConfig(id); + public void getCarrierConfig(CarrierIdentifier id, ResultReceiver result) { + try { + Bundle data = new Bundle(); + data.putParcelable(KEY_CONFIG_BUNDLE, CarrierService.this.onLoadConfig(id)); + result.send(RESULT_OK, data); + } catch (Exception e) { + Log.e(LOG_TAG, "Error in onLoadConfig: " + e.getMessage(), e); + result.send(RESULT_ERROR, null); + } } } } diff --git a/android/service/notification/Adjustment.java b/android/service/notification/Adjustment.java index ce678fc8..7348cf68 100644 --- a/android/service/notification/Adjustment.java +++ b/android/service/notification/Adjustment.java @@ -56,6 +56,15 @@ public final class Adjustment implements Parcelable { public static final String KEY_GROUP_KEY = "key_group_key"; /** + * Data type: int, one of {@link NotificationListenerService.Ranking#USER_SENTIMENT_POSITIVE}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEUTRAL}, + * {@link NotificationListenerService.Ranking#USER_SENTIMENT_NEGATIVE}. Used to express how + * a user feels about notifications in the same {@link android.app.NotificationChannel} as + * the notification represented by {@link #getKey()}. + */ + public static final String KEY_USER_SENTIMENT = "key_user_sentiment"; + + /** * Create a notification adjustment. * * @param pkg The package of the notification. diff --git a/android/service/notification/NotificationAssistantService.java b/android/service/notification/NotificationAssistantService.java index d94017cd..8e52bfa8 100644 --- a/android/service/notification/NotificationAssistantService.java +++ b/android/service/notification/NotificationAssistantService.java @@ -16,12 +16,9 @@ package android.service.notification; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.NotificationChannel; import android.content.Context; import android.content.Intent; import android.os.Handler; @@ -30,9 +27,9 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; + import com.android.internal.os.SomeArgs; -import java.util.ArrayList; import java.util.List; /** @@ -79,7 +76,7 @@ public abstract class NotificationAssistantService extends NotificationListenerS String snoozeCriterionId); /** - * A notification was posted by an app. Called before alert. + * A notification was posted by an app. Called before post. * * @param sbn the new notification * @return an adjustment or null to take no action, within 100ms. @@ -87,6 +84,34 @@ public abstract class NotificationAssistantService extends NotificationListenerS abstract public Adjustment onNotificationEnqueued(StatusBarNotification sbn); /** + * Implement this method to learn when notifications are removed, how they were interacted with + * before removal, and why they were removed. + * <p> + * This might occur because the user has dismissed the notification using system UI (or another + * notification listener) or because the app has withdrawn the notification. + * <p> + * NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the + * result from {@link StatusBarNotification#getNotification} may be missing some heavyweight + * fields such as {@link android.app.Notification#contentView} and + * {@link android.app.Notification#largeIcon}. However, all other fields on + * {@link StatusBarNotification}, sufficient to match this call with a prior call to + * {@link #onNotificationPosted(StatusBarNotification)}, will be intact. + * + ** @param sbn A data structure encapsulating at least the original information (tag and id) + * and source (package name) used to post the {@link android.app.Notification} that + * was just removed. + * @param rankingMap The current ranking map that can be used to retrieve ranking information + * for active notifications. + * @param stats Stats about how the user interacted with the notification before it was removed. + * @param reason see {@link #REASON_LISTENER_CANCEL}, etc. + */ + @Override + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Updates a notification. N.B. this won’t cause * an existing notification to alert, but might allow a future update to * this notification to alert. diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java index a5223fd8..08d3118b 100644 --- a/android/service/notification/NotificationListenerService.java +++ b/android/service/notification/NotificationListenerService.java @@ -20,6 +20,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; +import android.annotation.TestApi; import android.app.INotificationManager; import android.app.Notification; import android.app.Notification.Builder; @@ -265,7 +266,10 @@ public abstract class NotificationListenerService extends Service { @GuardedBy("mLock") private RankingMap mRankingMap; - private INotificationManager mNoMan; + /** + * @hide + */ + protected INotificationManager mNoMan; /** * Only valid after a successful call to (@link registerAsService}. @@ -389,6 +393,18 @@ public abstract class NotificationListenerService extends Service { } /** + * NotificationStats are not populated for notification listeners, so fall back to + * {@link #onNotificationRemoved(StatusBarNotification, RankingMap, int)}. + * + * @hide + */ + @TestApi + public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, + NotificationStats stats, int reason) { + onNotificationRemoved(sbn, rankingMap, reason); + } + + /** * Implement this method to learn about when the listener is enabled and connected to * the notification manager. You are safe to call {@link #getActiveNotifications()} * at this time. @@ -1200,7 +1216,7 @@ public abstract class NotificationListenerService extends Service { @Override public void onNotificationRemoved(IStatusBarNotificationHolder sbnHolder, - NotificationRankingUpdate update, int reason) { + NotificationRankingUpdate update, NotificationStats stats, int reason) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); @@ -1215,6 +1231,7 @@ public abstract class NotificationListenerService extends Service { args.arg1 = sbn; args.arg2 = mRankingMap; args.arg3 = reason; + args.arg4 = stats; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_REMOVED, args).sendToTarget(); } @@ -1324,6 +1341,26 @@ public abstract class NotificationListenerService extends Service { * @hide */ public static final int VISIBILITY_NO_OVERRIDE = NotificationManager.VISIBILITY_NO_OVERRIDE; + /** + * The user is likely to have a negative reaction to this notification. + */ + public static final int USER_SENTIMENT_NEGATIVE = -1; + /** + * It is not known how the user will react to this notification. + */ + public static final int USER_SENTIMENT_NEUTRAL = 0; + /** + * The user is likely to have a positive reaction to this notification. + */ + public static final int USER_SENTIMENT_POSITIVE = 1; + + /** @hide */ + @IntDef(prefix = { "USER_SENTIMENT_" }, value = { + USER_SENTIMENT_NEGATIVE, USER_SENTIMENT_NEUTRAL, USER_SENTIMENT_POSITIVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserSentiment {} + private String mKey; private int mRank = -1; private boolean mIsAmbient; @@ -1341,6 +1378,7 @@ public abstract class NotificationListenerService extends Service { // Notification assistant snooze criteria. private ArrayList<SnoozeCriterion> mSnoozeCriteria; private boolean mShowBadge; + private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL; public Ranking() {} @@ -1436,6 +1474,17 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns how the system thinks the user feels about notifications from the + * channel provided by {@link #getChannel()}. You can use this information to expose + * controls to help the user block this channel's notifications, if the sentiment is + * {@link #USER_SENTIMENT_NEGATIVE}, or emphasize this notification if the sentiment is + * {@link #USER_SENTIMENT_POSITIVE}. + */ + public int getUserSentiment() { + return mUserSentiment; + } + + /** * If the {@link NotificationAssistantService} has added people to this notification, then * this will be non-null. * @hide @@ -1471,7 +1520,8 @@ public abstract class NotificationListenerService extends Service { int visibilityOverride, int suppressedVisualEffects, int importance, CharSequence explanation, String overrideGroupKey, NotificationChannel channel, ArrayList<String> overridePeople, - ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge) { + ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge, + int userSentiment) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -1485,6 +1535,7 @@ public abstract class NotificationListenerService extends Service { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } /** @@ -1532,6 +1583,7 @@ public abstract class NotificationListenerService extends Service { private ArrayMap<String, ArrayList<String>> mOverridePeople; private ArrayMap<String, ArrayList<SnoozeCriterion>> mSnoozeCriteria; private ArrayMap<String, Boolean> mShowBadge; + private ArrayMap<String, Integer> mUserSentiment; private RankingMap(NotificationRankingUpdate rankingUpdate) { mRankingUpdate = rankingUpdate; @@ -1560,7 +1612,7 @@ public abstract class NotificationListenerService extends Service { getVisibilityOverride(key), getSuppressedVisualEffects(key), getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key), getChannel(key), getOverridePeople(key), getSnoozeCriteria(key), - getShowBadge(key)); + getShowBadge(key), getUserSentiment(key)); return rank >= 0; } @@ -1677,6 +1729,17 @@ public abstract class NotificationListenerService extends Service { return showBadge == null ? false : showBadge.booleanValue(); } + private int getUserSentiment(String key) { + synchronized (this) { + if (mUserSentiment == null) { + buildUserSentimentLocked(); + } + } + Integer userSentiment = mUserSentiment.get(key); + return userSentiment == null + ? Ranking.USER_SENTIMENT_NEUTRAL : userSentiment.intValue(); + } + // Locked by 'this' private void buildRanksLocked() { String[] orderedKeys = mRankingUpdate.getOrderedKeys(); @@ -1776,6 +1839,15 @@ public abstract class NotificationListenerService extends Service { } } + // Locked by 'this' + private void buildUserSentimentLocked() { + Bundle userSentiment = mRankingUpdate.getUserSentiment(); + mUserSentiment = new ArrayMap<>(userSentiment.size()); + for (String key : userSentiment.keySet()) { + mUserSentiment.put(key, userSentiment.getInt(key)); + } + } + // ----------- Parcelable @Override @@ -1835,8 +1907,9 @@ public abstract class NotificationListenerService extends Service { StatusBarNotification sbn = (StatusBarNotification) args.arg1; RankingMap rankingMap = (RankingMap) args.arg2; int reason = (int) args.arg3; + NotificationStats stats = (NotificationStats) args.arg4; args.recycle(); - onNotificationRemoved(sbn, rankingMap, reason); + onNotificationRemoved(sbn, rankingMap, stats, reason); } break; case MSG_ON_LISTENER_CONNECTED: { diff --git a/android/service/notification/NotificationRankingUpdate.java b/android/service/notification/NotificationRankingUpdate.java index 326b212a..6d51db09 100644 --- a/android/service/notification/NotificationRankingUpdate.java +++ b/android/service/notification/NotificationRankingUpdate.java @@ -35,12 +35,13 @@ public class NotificationRankingUpdate implements Parcelable { private final Bundle mOverridePeople; private final Bundle mSnoozeCriteria; private final Bundle mShowBadge; + private final Bundle mUserSentiment; public NotificationRankingUpdate(String[] keys, String[] interceptedKeys, Bundle visibilityOverrides, Bundle suppressedVisualEffects, int[] importance, Bundle explanation, Bundle overrideGroupKeys, Bundle channels, Bundle overridePeople, Bundle snoozeCriteria, - Bundle showBadge) { + Bundle showBadge, Bundle userSentiment) { mKeys = keys; mInterceptedKeys = interceptedKeys; mVisibilityOverrides = visibilityOverrides; @@ -52,6 +53,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = overridePeople; mSnoozeCriteria = snoozeCriteria; mShowBadge = showBadge; + mUserSentiment = userSentiment; } public NotificationRankingUpdate(Parcel in) { @@ -67,6 +69,7 @@ public class NotificationRankingUpdate implements Parcelable { mOverridePeople = in.readBundle(); mSnoozeCriteria = in.readBundle(); mShowBadge = in.readBundle(); + mUserSentiment = in.readBundle(); } @Override @@ -87,6 +90,7 @@ public class NotificationRankingUpdate implements Parcelable { out.writeBundle(mOverridePeople); out.writeBundle(mSnoozeCriteria); out.writeBundle(mShowBadge); + out.writeBundle(mUserSentiment); } public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR @@ -143,4 +147,8 @@ public class NotificationRankingUpdate implements Parcelable { public Bundle getShowBadge() { return mShowBadge; } + + public Bundle getUserSentiment() { + return mUserSentiment; + } } diff --git a/android/service/notification/NotificationStats.java b/android/service/notification/NotificationStats.java new file mode 100644 index 00000000..76d5328d --- /dev/null +++ b/android/service/notification/NotificationStats.java @@ -0,0 +1,256 @@ +/** + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.notification; + +import android.annotation.IntDef; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.RemoteInput; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +@TestApi +@SystemApi +public final class NotificationStats implements Parcelable { + + private boolean mSeen; + private boolean mExpanded; + private boolean mDirectReplied; + private boolean mSnoozed; + private boolean mViewedSettings; + private boolean mInteracted; + + /** @hide */ + @IntDef(prefix = { "DISMISSAL_SURFACE_" }, value = { + DISMISSAL_NOT_DISMISSED, DISMISSAL_OTHER, DISMISSAL_PEEK, DISMISSAL_AOD, DISMISSAL_SHADE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DismissalSurface {} + + + private @DismissalSurface int mDismissalSurface = DISMISSAL_NOT_DISMISSED; + + /** + * Notification has not been dismissed yet. + */ + public static final int DISMISSAL_NOT_DISMISSED = -1; + /** + * Notification has been dismissed from a {@link NotificationListenerService} or the app + * itself. + */ + public static final int DISMISSAL_OTHER = 0; + /** + * Notification has been dismissed while peeking. + */ + public static final int DISMISSAL_PEEK = 1; + /** + * Notification has been dismissed from always on display. + */ + public static final int DISMISSAL_AOD = 2; + /** + * Notification has been dismissed from the notification shade. + */ + public static final int DISMISSAL_SHADE = 3; + + public NotificationStats() { + } + + protected NotificationStats(Parcel in) { + mSeen = in.readByte() != 0; + mExpanded = in.readByte() != 0; + mDirectReplied = in.readByte() != 0; + mSnoozed = in.readByte() != 0; + mViewedSettings = in.readByte() != 0; + mInteracted = in.readByte() != 0; + mDismissalSurface = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mSeen ? 1 : 0)); + dest.writeByte((byte) (mExpanded ? 1 : 0)); + dest.writeByte((byte) (mDirectReplied ? 1 : 0)); + dest.writeByte((byte) (mSnoozed ? 1 : 0)); + dest.writeByte((byte) (mViewedSettings ? 1 : 0)); + dest.writeByte((byte) (mInteracted ? 1 : 0)); + dest.writeInt(mDismissalSurface); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<NotificationStats> CREATOR = new Creator<NotificationStats>() { + @Override + public NotificationStats createFromParcel(Parcel in) { + return new NotificationStats(in); + } + + @Override + public NotificationStats[] newArray(int size) { + return new NotificationStats[size]; + } + }; + + /** + * Returns whether the user has seen this notification at least once. + */ + public boolean hasSeen() { + return mSeen; + } + + /** + * Records that the user as seen this notification at least once. + */ + public void setSeen() { + mSeen = true; + } + + /** + * Returns whether the user has expanded this notification at least once. + */ + public boolean hasExpanded() { + return mExpanded; + } + + /** + * Records that the user has expanded this notification at least once. + */ + public void setExpanded() { + mExpanded = true; + mInteracted = true; + } + + /** + * Returns whether the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} at + * least once. + */ + public boolean hasDirectReplied() { + return mDirectReplied; + } + + /** + * Records that the user has replied to a notification that has a + * {@link android.app.Notification.Action.Builder#addRemoteInput(RemoteInput) direct reply} + * at least once. + */ + public void setDirectReplied() { + mDirectReplied = true; + mInteracted = true; + } + + /** + * Returns whether the user has snoozed this notification at least once. + */ + public boolean hasSnoozed() { + return mSnoozed; + } + + /** + * Records that the user has snoozed this notification at least once. + */ + public void setSnoozed() { + mSnoozed = true; + mInteracted = true; + } + + /** + * Returns whether the user has viewed the in-shade settings for this notification at least + * once. + */ + public boolean hasViewedSettings() { + return mViewedSettings; + } + + /** + * Records that the user has viewed the in-shade settings for this notification at least once. + */ + public void setViewedSettings() { + mViewedSettings = true; + mInteracted = true; + } + + /** + * Returns whether the user has interacted with this notification beyond having viewed it. + */ + public boolean hasInteracted() { + return mInteracted; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public @DismissalSurface int getDismissalSurface() { + return mDismissalSurface; + } + + /** + * Returns from which surface the notification was dismissed. + */ + public void setDismissalSurface(@DismissalSurface int dismissalSurface) { + mDismissalSurface = dismissalSurface; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NotificationStats that = (NotificationStats) o; + + if (mSeen != that.mSeen) return false; + if (mExpanded != that.mExpanded) return false; + if (mDirectReplied != that.mDirectReplied) return false; + if (mSnoozed != that.mSnoozed) return false; + if (mViewedSettings != that.mViewedSettings) return false; + if (mInteracted != that.mInteracted) return false; + return mDismissalSurface == that.mDismissalSurface; + } + + @Override + public int hashCode() { + int result = (mSeen ? 1 : 0); + result = 31 * result + (mExpanded ? 1 : 0); + result = 31 * result + (mDirectReplied ? 1 : 0); + result = 31 * result + (mSnoozed ? 1 : 0); + result = 31 * result + (mViewedSettings ? 1 : 0); + result = 31 * result + (mInteracted ? 1 : 0); + result = 31 * result + mDismissalSurface; + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("NotificationStats{"); + sb.append("mSeen=").append(mSeen); + sb.append(", mExpanded=").append(mExpanded); + sb.append(", mDirectReplied=").append(mDirectReplied); + sb.append(", mSnoozed=").append(mSnoozed); + sb.append(", mViewedSettings=").append(mViewedSettings); + sb.append(", mInteracted=").append(mInteracted); + sb.append(", mDismissalSurface=").append(mDismissalSurface); + sb.append('}'); + return sb.toString(); + } +} diff --git a/android/service/settings/suggestions/Suggestion.java b/android/service/settings/suggestions/Suggestion.java index f27cc2eb..cfeb7fce 100644 --- a/android/service/settings/suggestions/Suggestion.java +++ b/android/service/settings/suggestions/Suggestion.java @@ -16,12 +16,17 @@ package android.service.settings.suggestions; +import android.annotation.IntDef; import android.annotation.SystemApi; import android.app.PendingIntent; +import android.graphics.drawable.Icon; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Data object that has information about a device suggestion. * @@ -30,9 +35,27 @@ import android.text.TextUtils; @SystemApi public final class Suggestion implements Parcelable { + /** + * @hide + */ + @IntDef(flag = true, value = { + FLAG_HAS_BUTTON, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Flags { + } + + /** + * Flag for suggestion type with a single button + */ + public static final int FLAG_HAS_BUTTON = 1 << 0; + private final String mId; private final CharSequence mTitle; private final CharSequence mSummary; + private final Icon mIcon; + @Flags + private final int mFlags; private final PendingIntent mPendingIntent; /** @@ -57,6 +80,22 @@ public final class Suggestion implements Parcelable { } /** + * Optional icon for this suggestion. + */ + public Icon getIcon() { + return mIcon; + } + + /** + * Optional flags for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + @Flags + public int getFlags() { + return mFlags; + } + + /** * The Intent to launch when the suggestion is activated. */ public PendingIntent getPendingIntent() { @@ -67,6 +106,8 @@ public final class Suggestion implements Parcelable { mId = builder.mId; mTitle = builder.mTitle; mSummary = builder.mSummary; + mIcon = builder.mIcon; + mFlags = builder.mFlags; mPendingIntent = builder.mPendingIntent; } @@ -74,6 +115,8 @@ public final class Suggestion implements Parcelable { mId = in.readString(); mTitle = in.readCharSequence(); mSummary = in.readCharSequence(); + mIcon = in.readParcelable(Icon.class.getClassLoader()); + mFlags = in.readInt(); mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader()); } @@ -99,6 +142,8 @@ public final class Suggestion implements Parcelable { dest.writeString(mId); dest.writeCharSequence(mTitle); dest.writeCharSequence(mSummary); + dest.writeParcelable(mIcon, flags); + dest.writeInt(mFlags); dest.writeParcelable(mPendingIntent, flags); } @@ -109,6 +154,9 @@ public final class Suggestion implements Parcelable { private final String mId; private CharSequence mTitle; private CharSequence mSummary; + private Icon mIcon; + @Flags + private int mFlags; private PendingIntent mPendingIntent; public Builder(String id) { @@ -135,6 +183,23 @@ public final class Suggestion implements Parcelable { } /** + * Sets icon for the suggestion. + */ + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets a UI type for this suggestion. This will influence UI when rendering suggestion in + * different style. + */ + public Builder setFlags(@Flags int flags) { + mFlags = flags; + return this; + } + + /** * Sets suggestion intent */ public Builder setPendingIntent(PendingIntent pendingIntent) { |