Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package android.service.autofill; |
| 17 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 18 | import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 19 | import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE; |
| 20 | import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE; |
| 21 | import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH; |
| 22 | import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH; |
| 23 | import static android.view.autofill.Helper.sDebug; |
| 24 | |
| 25 | import android.annotation.NonNull; |
| 26 | import android.annotation.Nullable; |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 27 | import android.annotation.TestApi; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 28 | import android.app.ActivityThread; |
| 29 | import android.content.ContentResolver; |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 30 | import android.os.Bundle; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 31 | import android.os.Parcel; |
| 32 | import android.os.Parcelable; |
| 33 | import android.provider.Settings; |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 34 | import android.service.autofill.FieldClassification.Match; |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 35 | import android.text.TextUtils; |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 36 | import android.util.ArrayMap; |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 37 | import android.util.ArraySet; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 38 | import android.util.Log; |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 39 | import android.view.autofill.AutofillManager; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 40 | import android.view.autofill.Helper; |
| 41 | |
| 42 | import com.android.internal.util.Preconditions; |
| 43 | |
| 44 | import java.io.PrintWriter; |
| 45 | import java.util.ArrayList; |
| 46 | |
| 47 | /** |
Felipe Leme | 78172e7 | 2017-12-08 17:01:15 -0800 | [diff] [blame] | 48 | * Defines the user data used for |
| 49 | * <a href="AutofillService.html#FieldClassification">field classification</a>. |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 50 | */ |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 51 | public final class UserData implements FieldClassificationUserData, Parcelable { |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 52 | |
| 53 | private static final String TAG = "UserData"; |
| 54 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 55 | private static final int DEFAULT_MAX_USER_DATA_SIZE = 50; |
| 56 | private static final int DEFAULT_MAX_CATEGORY_COUNT = 10; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 57 | private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10; |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 58 | private static final int DEFAULT_MIN_VALUE_LENGTH = 3; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 59 | private static final int DEFAULT_MAX_VALUE_LENGTH = 100; |
| 60 | |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 61 | private final String mId; |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 62 | private final String[] mCategoryIds; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 63 | private final String[] mValues; |
| 64 | |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 65 | private final String mDefaultAlgorithm; |
| 66 | private final Bundle mDefaultArgs; |
| 67 | private final ArrayMap<String, String> mCategoryAlgorithms; |
| 68 | private final ArrayMap<String, Bundle> mCategoryArgs; |
| 69 | |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 70 | private UserData(Builder builder) { |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 71 | mId = builder.mId; |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 72 | mCategoryIds = new String[builder.mCategoryIds.size()]; |
| 73 | builder.mCategoryIds.toArray(mCategoryIds); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 74 | mValues = new String[builder.mValues.size()]; |
| 75 | builder.mValues.toArray(mValues); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 76 | builder.mValues.toArray(mValues); |
| 77 | |
| 78 | mDefaultAlgorithm = builder.mDefaultAlgorithm; |
| 79 | mDefaultArgs = builder.mDefaultArgs; |
| 80 | mCategoryAlgorithms = builder.mCategoryAlgorithms; |
| 81 | mCategoryArgs = builder.mCategoryArgs; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 82 | } |
| 83 | |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 84 | /** |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 85 | * Gets the name of the default algorithm that is used to calculate |
| 86 | * {@link Match#getScore()} match scores}. |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 87 | */ |
| 88 | @Nullable |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 89 | @Override |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 90 | public String getFieldClassificationAlgorithm() { |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 91 | return mDefaultAlgorithm; |
| 92 | } |
| 93 | |
| 94 | /** @hide */ |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 95 | @Override |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 96 | public Bundle getDefaultFieldClassificationArgs() { |
| 97 | return mDefaultArgs; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Gets the name of the algorithm corresponding to the specific autofill category |
| 102 | * that is used to calculate {@link Match#getScore() match scores} |
| 103 | * |
| 104 | * @param categoryId autofill field category |
| 105 | * |
| 106 | * @return String name of algorithm, null if none found. |
| 107 | */ |
| 108 | @Nullable |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 109 | @Override |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 110 | public String getFieldClassificationAlgorithmForCategory(@NonNull String categoryId) { |
| 111 | Preconditions.checkNotNull(categoryId); |
| 112 | if (mCategoryAlgorithms == null || !mCategoryAlgorithms.containsKey(categoryId)) { |
| 113 | return null; |
| 114 | } |
| 115 | return mCategoryAlgorithms.get(categoryId); |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 116 | } |
| 117 | |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 118 | /** |
| 119 | * Gets the id. |
| 120 | */ |
| 121 | public String getId() { |
| 122 | return mId; |
| 123 | } |
| 124 | |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 125 | /** @hide */ |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 126 | @Override |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 127 | public String[] getCategoryIds() { |
| 128 | return mCategoryIds; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 129 | } |
| 130 | |
| 131 | /** @hide */ |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 132 | @Override |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 133 | public String[] getValues() { |
| 134 | return mValues; |
| 135 | } |
| 136 | |
| 137 | /** @hide */ |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 138 | @TestApi |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 139 | @Override |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 140 | public ArrayMap<String, String> getFieldClassificationAlgorithms() { |
| 141 | return mCategoryAlgorithms; |
| 142 | } |
| 143 | |
| 144 | /** @hide */ |
Adam He | 1cb6f80 | 2018-12-10 15:15:49 -0800 | [diff] [blame] | 145 | @Override |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 146 | public ArrayMap<String, Bundle> getFieldClassificationArgs() { |
| 147 | return mCategoryArgs; |
| 148 | } |
| 149 | |
| 150 | /** @hide */ |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 151 | public void dump(String prefix, PrintWriter pw) { |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 152 | pw.print(prefix); pw.print("id: "); pw.print(mId); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 153 | pw.print(prefix); pw.print("Default Algorithm: "); pw.print(mDefaultAlgorithm); |
| 154 | pw.print(prefix); pw.print("Default Args"); pw.print(mDefaultArgs); |
| 155 | if (mCategoryAlgorithms != null && mCategoryAlgorithms.size() > 0) { |
| 156 | pw.print(prefix); pw.print("Algorithms per category: "); |
| 157 | for (int i = 0; i < mCategoryAlgorithms.size(); i++) { |
| 158 | pw.print(prefix); pw.print(prefix); pw.print(mCategoryAlgorithms.keyAt(i)); |
| 159 | pw.print(": "); pw.println(Helper.getRedacted(mCategoryAlgorithms.valueAt(i))); |
| 160 | pw.print("args="); pw.print(mCategoryArgs.get(mCategoryAlgorithms.keyAt(i))); |
| 161 | } |
| 162 | } |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 163 | // Cannot disclose field ids or values because they could contain PII |
| 164 | pw.print(prefix); pw.print("Field ids size: "); pw.println(mCategoryIds.length); |
| 165 | for (int i = 0; i < mCategoryIds.length; i++) { |
Felipe Leme | 5672def | 2017-12-04 14:57:09 -0800 | [diff] [blame] | 166 | pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 167 | pw.println(Helper.getRedacted(mCategoryIds[i])); |
Felipe Leme | 5672def | 2017-12-04 14:57:09 -0800 | [diff] [blame] | 168 | } |
| 169 | pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 170 | for (int i = 0; i < mValues.length; i++) { |
Felipe Leme | 5672def | 2017-12-04 14:57:09 -0800 | [diff] [blame] | 171 | pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); |
| 172 | pw.println(Helper.getRedacted(mValues[i])); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 173 | } |
| 174 | } |
| 175 | |
| 176 | /** @hide */ |
| 177 | public static void dumpConstraints(String prefix, PrintWriter pw) { |
| 178 | pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize()); |
| 179 | pw.print(prefix); pw.print("maxFieldClassificationIdsSize: "); |
| 180 | pw.println(getMaxFieldClassificationIdsSize()); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 181 | pw.print(prefix); pw.print("maxCategoryCount: "); pw.println(getMaxCategoryCount()); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 182 | pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength()); |
| 183 | pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength()); |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * A builder for {@link UserData} objects. |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 188 | */ |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 189 | public static final class Builder { |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 190 | private final String mId; |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 191 | private final ArrayList<String> mCategoryIds; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 192 | private final ArrayList<String> mValues; |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 193 | private String mDefaultAlgorithm; |
| 194 | private Bundle mDefaultArgs; |
| 195 | |
| 196 | // Map of autofill field categories to fleid classification algorithms and args |
| 197 | private ArrayMap<String, String> mCategoryAlgorithms; |
| 198 | private ArrayMap<String, Bundle> mCategoryArgs; |
| 199 | |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 200 | private boolean mDestroyed; |
| 201 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 202 | // Non-persistent array used to limit the number of unique ids. |
| 203 | private final ArraySet<String> mUniqueCategoryIds; |
Felipe Leme | 725bc91 | 2018-08-17 12:48:05 -0700 | [diff] [blame] | 204 | // Non-persistent array used to ignore duplaicated value/category pairs. |
| 205 | private final ArraySet<String> mUniqueValueCategoryPairs; |
| 206 | |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 207 | /** |
Felipe Leme | 78172e7 | 2017-12-08 17:01:15 -0800 | [diff] [blame] | 208 | * Creates a new builder for the user data used for <a href="#FieldClassification">field |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 209 | * classification</a>. |
| 210 | * |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 211 | * <p>The user data must contain at least one pair of {@code value} -> {@code categoryId}, |
| 212 | * and more pairs can be added through the {@link #add(String, String)} method. For example: |
| 213 | * |
| 214 | * <pre class="prettyprint"> |
| 215 | * new UserData.Builder("v1", "Bart Simpson", "name") |
| 216 | * .add("bart.simpson@example.com", "email") |
| 217 | * .add("el_barto@example.com", "email") |
| 218 | * .build(); |
| 219 | * </pre> |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 220 | * |
| 221 | * @param id id used to identify the whole {@link UserData} object. This id is also returned |
| 222 | * by {@link AutofillManager#getUserDataId()}, which can be used to check if the |
| 223 | * {@link UserData} is up-to-date without fetching the whole object (through |
| 224 | * {@link AutofillManager#getUserData()}). |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 225 | * |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 226 | * @param value value of the user data. |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 227 | * @param categoryId autofill field category. |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 228 | * |
Felipe Leme | 329d040 | 2017-12-06 09:22:43 -0800 | [diff] [blame] | 229 | * @throws IllegalArgumentException if any of the following occurs: |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 230 | * <ul> |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 231 | * <li>{@code id} is empty</li> |
| 232 | * <li>{@code categoryId} is empty</li> |
| 233 | * <li>{@code value} is empty</li> |
| 234 | * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li> |
| 235 | * <li>the length of {@code value} is higher than |
| 236 | * {@link UserData#getMaxValueLength()}</li> |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 237 | * </ul> |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 238 | */ |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 239 | public Builder(@NonNull String id, @NonNull String value, @NonNull String categoryId) { |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 240 | mId = checkNotEmpty("id", id); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 241 | checkNotEmpty("categoryId", categoryId); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 242 | checkValidValue(value); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 243 | final int maxUserDataSize = getMaxUserDataSize(); |
| 244 | mCategoryIds = new ArrayList<>(maxUserDataSize); |
| 245 | mValues = new ArrayList<>(maxUserDataSize); |
Felipe Leme | 725bc91 | 2018-08-17 12:48:05 -0700 | [diff] [blame] | 246 | mUniqueValueCategoryPairs = new ArraySet<>(maxUserDataSize); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 247 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 248 | mUniqueCategoryIds = new ArraySet<>(getMaxCategoryCount()); |
| 249 | |
| 250 | addMapping(value, categoryId); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 251 | } |
| 252 | |
| 253 | /** |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 254 | * Sets the default algorithm used for |
| 255 | * <a href="#FieldClassification">field classification</a>. |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 256 | * |
| 257 | * <p>The currently available algorithms can be retrieve through |
| 258 | * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}. |
| 259 | * |
Felipe Leme | d11a662 | 2018-01-18 13:38:24 -0800 | [diff] [blame] | 260 | * <p>If not set, the |
| 261 | * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is |
| 262 | * used instead. |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 263 | * |
| 264 | * @param name name of the algorithm or {@code null} to used default. |
| 265 | * @param args optional arguments to the algorithm. |
| 266 | * |
| 267 | * @return this builder |
| 268 | */ |
| 269 | public Builder setFieldClassificationAlgorithm(@Nullable String name, |
| 270 | @Nullable Bundle args) { |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 271 | throwIfDestroyed(); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 272 | mDefaultAlgorithm = name; |
| 273 | mDefaultArgs = args; |
| 274 | return this; |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Sets the algorithm used for <a href="#FieldClassification">field classification</a> |
| 279 | * for the specified category. |
| 280 | * |
| 281 | * <p>The currently available algorithms can be retrieved through |
| 282 | * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}. |
| 283 | * |
| 284 | * <p>If not set, the |
| 285 | * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is |
| 286 | * used instead. |
| 287 | * |
| 288 | * @param categoryId autofill field category. |
| 289 | * @param name name of the algorithm or {@code null} to used default. |
| 290 | * @param args optional arguments to the algorithm. |
| 291 | * |
| 292 | * @return this builder |
| 293 | */ |
| 294 | public Builder setFieldClassificationAlgorithmForCategory(@NonNull String categoryId, |
| 295 | @Nullable String name, @Nullable Bundle args) { |
| 296 | throwIfDestroyed(); |
| 297 | Preconditions.checkNotNull(categoryId); |
| 298 | if (mCategoryAlgorithms == null) { |
| 299 | mCategoryAlgorithms = new ArrayMap<>(getMaxCategoryCount()); |
| 300 | } |
| 301 | if (mCategoryArgs == null) { |
| 302 | mCategoryArgs = new ArrayMap<>(getMaxCategoryCount()); |
| 303 | } |
| 304 | mCategoryAlgorithms.put(categoryId, name); |
| 305 | mCategoryArgs.put(categoryId, args); |
Felipe Leme | 27f4573 | 2017-12-22 09:05:22 -0800 | [diff] [blame] | 306 | return this; |
| 307 | } |
| 308 | |
| 309 | /** |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 310 | * Adds a new value for user data. |
| 311 | * |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 312 | * @param value value of the user data. |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 313 | * @param categoryId string used to identify the category the value is associated with. |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 314 | * |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 315 | * @throws IllegalStateException if: |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 316 | * <ul> |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 317 | * <li>{@link #build()} already called</li> |
Felipe Leme | 725bc91 | 2018-08-17 12:48:05 -0700 | [diff] [blame] | 318 | * <li>the {@code value} has already been added (<b>Note: </b> this restriction was |
| 319 | * lifted on Android {@link android.os.Build.VERSION_CODES#Q} and later)</li> |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 320 | * <li>the number of unique {@code categoryId} values added so far is more than |
| 321 | * {@link UserData#getMaxCategoryCount()}</li> |
| 322 | * <li>the number of {@code values} added so far is is more than |
| 323 | * {@link UserData#getMaxUserDataSize()}</li> |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 324 | * </ul> |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 325 | * |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 326 | * @throws IllegalArgumentException if any of the following occurs: |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 327 | * <ul> |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 328 | * <li>{@code id} is empty</li> |
| 329 | * <li>{@code categoryId} is empty</li> |
| 330 | * <li>{@code value} is empty</li> |
| 331 | * <li>the length of {@code value} is lower than {@link UserData#getMinValueLength()}</li> |
| 332 | * <li>the length of {@code value} is higher than |
| 333 | * {@link UserData#getMaxValueLength()}</li> |
Felipe Leme | 4cd1ae0 | 2018-03-15 15:55:05 -0700 | [diff] [blame] | 334 | * </ul> |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 335 | */ |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 336 | public Builder add(@NonNull String value, @NonNull String categoryId) { |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 337 | throwIfDestroyed(); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 338 | checkNotEmpty("categoryId", categoryId); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 339 | checkValidValue(value); |
| 340 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 341 | if (!mUniqueCategoryIds.contains(categoryId)) { |
| 342 | // New category - check size |
| 343 | Preconditions.checkState(mUniqueCategoryIds.size() < getMaxCategoryCount(), |
| 344 | "already added " + mUniqueCategoryIds.size() + " unique category ids"); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 345 | } |
| 346 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 347 | Preconditions.checkState(mValues.size() < getMaxUserDataSize(), |
| 348 | "already added " + mValues.size() + " elements"); |
| 349 | addMapping(value, categoryId); |
Felipe Leme | 5672def | 2017-12-04 14:57:09 -0800 | [diff] [blame] | 350 | |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 351 | return this; |
| 352 | } |
| 353 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 354 | private void addMapping(@NonNull String value, @NonNull String categoryId) { |
Felipe Leme | 725bc91 | 2018-08-17 12:48:05 -0700 | [diff] [blame] | 355 | final String pair = value + ":" + categoryId; |
| 356 | if (mUniqueValueCategoryPairs.contains(pair)) { |
| 357 | // Don't include value on message because it could contain PII |
| 358 | Log.w(TAG, "Ignoring entry with same value / category"); |
| 359 | return; |
| 360 | } |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 361 | mCategoryIds.add(categoryId); |
| 362 | mValues.add(value); |
| 363 | mUniqueCategoryIds.add(categoryId); |
Felipe Leme | 725bc91 | 2018-08-17 12:48:05 -0700 | [diff] [blame] | 364 | mUniqueValueCategoryPairs.add(pair); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 365 | } |
| 366 | |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 367 | private String checkNotEmpty(@NonNull String name, @Nullable String value) { |
| 368 | Preconditions.checkNotNull(value); |
| 369 | Preconditions.checkArgument(!TextUtils.isEmpty(value), "%s cannot be empty", name); |
| 370 | return value; |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 371 | } |
| 372 | |
| 373 | private void checkValidValue(@Nullable String value) { |
| 374 | Preconditions.checkNotNull(value); |
| 375 | final int length = value.length(); |
| 376 | Preconditions.checkArgumentInRange(length, getMinValueLength(), |
| 377 | getMaxValueLength(), "value length (" + length + ")"); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * Creates a new {@link UserData} instance. |
| 382 | * |
| 383 | * <p>You should not interact with this builder once this method is called. |
| 384 | * |
| 385 | * @throws IllegalStateException if {@link #build()} was already called. |
| 386 | * |
| 387 | * @return The built dataset. |
| 388 | */ |
| 389 | public UserData build() { |
| 390 | throwIfDestroyed(); |
| 391 | mDestroyed = true; |
| 392 | return new UserData(this); |
| 393 | } |
| 394 | |
| 395 | private void throwIfDestroyed() { |
| 396 | if (mDestroyed) { |
| 397 | throw new IllegalStateException("Already called #build()"); |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | ///////////////////////////////////// |
| 403 | // Object "contract" methods. // |
| 404 | ///////////////////////////////////// |
| 405 | @Override |
| 406 | public String toString() { |
| 407 | if (!sDebug) return super.toString(); |
| 408 | |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 409 | final StringBuilder builder = new StringBuilder("UserData: [id=").append(mId); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 410 | // Cannot disclose category ids or values because they could contain PII |
| 411 | builder.append(", categoryIds="); |
| 412 | Helper.appendRedacted(builder, mCategoryIds); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 413 | builder.append(", values="); |
| 414 | Helper.appendRedacted(builder, mValues); |
| 415 | return builder.append("]").toString(); |
| 416 | } |
| 417 | |
| 418 | ///////////////////////////////////// |
| 419 | // Parcelable "contract" methods. // |
| 420 | ///////////////////////////////////// |
| 421 | |
| 422 | @Override |
| 423 | public int describeContents() { |
| 424 | return 0; |
| 425 | } |
| 426 | |
| 427 | @Override |
| 428 | public void writeToParcel(Parcel parcel, int flags) { |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 429 | parcel.writeString(mId); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 430 | parcel.writeStringArray(mCategoryIds); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 431 | parcel.writeStringArray(mValues); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 432 | parcel.writeString(mDefaultAlgorithm); |
| 433 | parcel.writeBundle(mDefaultArgs); |
| 434 | parcel.writeMap(mCategoryAlgorithms); |
| 435 | parcel.writeMap(mCategoryArgs); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 436 | } |
| 437 | |
| 438 | public static final Parcelable.Creator<UserData> CREATOR = |
| 439 | new Parcelable.Creator<UserData>() { |
| 440 | @Override |
| 441 | public UserData createFromParcel(Parcel parcel) { |
| 442 | // Always go through the builder to ensure the data ingested by |
| 443 | // the system obeys the contract of the builder to avoid attacks |
| 444 | // using specially crafted parcels. |
Felipe Leme | f0baef74 | 2018-01-26 14:39:39 -0800 | [diff] [blame] | 445 | final String id = parcel.readString(); |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 446 | final String[] categoryIds = parcel.readStringArray(); |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 447 | final String[] values = parcel.readStringArray(); |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 448 | final String defaultAlgorithm = parcel.readString(); |
| 449 | final Bundle defaultArgs = parcel.readBundle(); |
| 450 | final ArrayMap<String, String> categoryAlgorithms = new ArrayMap<>(); |
| 451 | parcel.readMap(categoryAlgorithms, String.class.getClassLoader()); |
| 452 | final ArrayMap<String, Bundle> categoryArgs = new ArrayMap<>(); |
| 453 | parcel.readMap(categoryArgs, Bundle.class.getClassLoader()); |
| 454 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 455 | final Builder builder = new Builder(id, values[0], categoryIds[0]) |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 456 | .setFieldClassificationAlgorithm(defaultAlgorithm, defaultArgs); |
| 457 | |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 458 | for (int i = 1; i < categoryIds.length; i++) { |
Adam He | 71d53e0 | 2018-11-06 14:03:26 -0800 | [diff] [blame] | 459 | String categoryId = categoryIds[i]; |
| 460 | builder.add(values[i], categoryId); |
| 461 | } |
| 462 | |
| 463 | final int size = categoryAlgorithms.size(); |
| 464 | if (size > 0) { |
| 465 | for (int i = 0; i < size; i++) { |
| 466 | final String categoryId = categoryAlgorithms.keyAt(i); |
| 467 | builder.setFieldClassificationAlgorithmForCategory(categoryId, |
| 468 | categoryAlgorithms.valueAt(i), categoryArgs.get(categoryId)); |
| 469 | } |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 470 | } |
| 471 | return builder.build(); |
| 472 | } |
| 473 | |
| 474 | @Override |
| 475 | public UserData[] newArray(int size) { |
| 476 | return new UserData[size]; |
| 477 | } |
| 478 | }; |
| 479 | |
| 480 | /** |
| 481 | * Gets the maximum number of values that can be added to a {@link UserData}. |
| 482 | */ |
| 483 | public static int getMaxUserDataSize() { |
| 484 | return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE); |
| 485 | } |
| 486 | |
| 487 | /** |
| 488 | * Gets the maximum number of ids that can be passed to {@link |
| 489 | * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}. |
| 490 | */ |
| 491 | public static int getMaxFieldClassificationIdsSize() { |
| 492 | return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, |
| 493 | DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE); |
| 494 | } |
| 495 | |
| 496 | /** |
Felipe Leme | febb733 | 2018-02-12 18:12:55 -0800 | [diff] [blame] | 497 | * Gets the maximum number of unique category ids that can be passed to |
| 498 | * the builder's constructor and {@link Builder#add(String, String)}. |
| 499 | */ |
| 500 | public static int getMaxCategoryCount() { |
| 501 | return getInt(AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, DEFAULT_MAX_CATEGORY_COUNT); |
| 502 | } |
| 503 | |
| 504 | /** |
Felipe Leme | 78172e7 | 2017-12-08 17:01:15 -0800 | [diff] [blame] | 505 | * Gets the minimum length of values passed to the builder's constructor or |
| 506 | * or {@link Builder#add(String, String)}. |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 507 | */ |
| 508 | public static int getMinValueLength() { |
| 509 | return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH); |
| 510 | } |
| 511 | |
| 512 | /** |
Felipe Leme | 78172e7 | 2017-12-08 17:01:15 -0800 | [diff] [blame] | 513 | * Gets the maximum length of values passed to the builder's constructor or |
| 514 | * or {@link Builder#add(String, String)}. |
Felipe Leme | 452886a | 2017-11-27 13:09:13 -0800 | [diff] [blame] | 515 | */ |
| 516 | public static int getMaxValueLength() { |
| 517 | return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH); |
| 518 | } |
| 519 | |
| 520 | private static int getInt(String settings, int defaultValue) { |
| 521 | ContentResolver cr = null; |
| 522 | final ActivityThread at = ActivityThread.currentActivityThread(); |
| 523 | if (at != null) { |
| 524 | cr = at.getApplication().getContentResolver(); |
| 525 | } |
| 526 | |
| 527 | if (cr == null) { |
| 528 | Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue); |
| 529 | return defaultValue; |
| 530 | } |
| 531 | return Settings.Secure.getInt(cr, settings, defaultValue); |
| 532 | } |
| 533 | } |