1 /* 2 * Copyright (C) 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 17 package com.android.server.autofill; 18 19 import static android.Manifest.permission.INTERACT_ACROSS_USERS; 20 21 import static com.android.server.autofill.Helper.sDebug; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.RequiresPermission; 26 import android.annotation.UserIdInt; 27 import android.app.ActivityManager; 28 import android.app.assist.AssistStructure; 29 import android.app.assist.AssistStructure.ViewNode; 30 import android.app.assist.AssistStructure.WindowNode; 31 import android.app.slice.Slice; 32 import android.app.slice.SliceItem; 33 import android.content.ComponentName; 34 import android.content.Context; 35 import android.graphics.drawable.Icon; 36 import android.hardware.display.DisplayManager; 37 import android.metrics.LogMaker; 38 import android.os.UserHandle; 39 import android.os.UserManager; 40 import android.service.autofill.Dataset; 41 import android.service.autofill.FillResponse; 42 import android.service.autofill.InternalSanitizer; 43 import android.service.autofill.SaveInfo; 44 import android.text.TextUtils; 45 import android.util.ArrayMap; 46 import android.util.ArraySet; 47 import android.util.Slog; 48 import android.util.SparseArray; 49 import android.view.Display; 50 import android.view.View; 51 import android.view.WindowManager; 52 import android.view.autofill.AutofillId; 53 import android.view.autofill.AutofillValue; 54 import android.widget.RemoteViews; 55 56 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 57 import com.android.internal.util.ArrayUtils; 58 import com.android.server.utils.Slogf; 59 60 import java.io.PrintWriter; 61 import java.lang.ref.WeakReference; 62 import java.util.ArrayDeque; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.concurrent.atomic.AtomicBoolean; 66 67 public final class Helper { 68 69 private static final String TAG = "AutofillHelper"; 70 71 // TODO(b/117779333): get rid of sDebug / sVerbose and always use the service variables instead 72 73 /** 74 * Defines a logging flag that can be dynamically changed at runtime using 75 * {@code cmd autofill set log_level debug} or through 76 * {@link android.provider.Settings.Global#AUTOFILL_LOGGING_LEVEL}. 77 */ 78 public static boolean sDebug = true; 79 80 /** 81 * Defines a logging flag that can be dynamically changed at runtime using 82 * {@code cmd autofill set log_level verbose} or through 83 * {@link android.provider.Settings.Global#AUTOFILL_LOGGING_LEVEL}. 84 */ 85 public static boolean sVerbose = false; 86 87 /** 88 * When non-null, overrides whether the UI should be shown on full-screen mode. 89 * 90 * <p>Note: access to this variable is not synchronized because it's "final" on real usage - 91 * it's only set by Shell cmd, for development purposes. 92 */ 93 public static Boolean sFullScreenMode = null; 94 Helper()95 private Helper() { 96 throw new UnsupportedOperationException("contains static members only"); 97 } 98 checkRemoteViewUriPermissions( @serIdInt int userId, @NonNull RemoteViews rView)99 private static boolean checkRemoteViewUriPermissions( 100 @UserIdInt int userId, @NonNull RemoteViews rView) { 101 final AtomicBoolean permissionsOk = new AtomicBoolean(true); 102 103 rView.visitUris( 104 uri -> { 105 int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri, userId); 106 boolean allowed = uriOwnerId == userId; 107 permissionsOk.set(allowed & permissionsOk.get()); 108 }); 109 110 return permissionsOk.get(); 111 } 112 113 /** 114 * Creates the context as the foreground user 115 * 116 * <p>Returns the current context as the current foreground user 117 */ 118 @RequiresPermission(INTERACT_ACROSS_USERS) getUserContext(Context context)119 public static Context getUserContext(Context context) { 120 int userId = ActivityManager.getCurrentUser(); 121 Context c = context.createContextAsUser(UserHandle.of(userId), /* flags= */ 0); 122 if (sDebug) { 123 Slog.d( 124 TAG, 125 "Current User: " 126 + userId 127 + ", context created as: " 128 + c.getContentResolver().getUserId()); 129 } 130 return c; 131 } 132 133 /** 134 * Checks the URI permissions of the remote view, 135 * to see if the current userId is able to access it. 136 * 137 * Returns the RemoteView that is passed if user is able, null otherwise. 138 * 139 * TODO: instead of returning a null remoteview when 140 * the current userId cannot access an URI, 141 * return a new RemoteView with the URI removed. 142 */ sanitizeRemoteView(RemoteViews rView)143 public static @Nullable RemoteViews sanitizeRemoteView(RemoteViews rView) { 144 if (rView == null) return null; 145 146 int userId = ActivityManager.getCurrentUser(); 147 148 boolean ok = checkRemoteViewUriPermissions(userId, rView); 149 if (!ok) { 150 Slog.w(TAG, 151 "sanitizeRemoteView() user: " + userId 152 + " tried accessing resource that does not belong to them"); 153 } 154 return (ok ? rView : null); 155 } 156 157 /** 158 * Checks the URI permissions of the icon in the slice, to see if the current userId is able to 159 * access it. 160 * 161 * <p>Returns null if slice contains user inaccessible icons 162 * 163 * <p>TODO: instead of returning a null Slice when the current userId cannot access an icon, 164 * return a reconstructed Slice without the icons. This is currently non-trivial since there are 165 * no public methods to generically add SliceItems to Slices 166 */ sanitizeSlice(Slice slice)167 public static @Nullable Slice sanitizeSlice(Slice slice) { 168 if (slice == null) { 169 return null; 170 } 171 172 int userId = ActivityManager.getCurrentUser(); 173 174 // Recontruct the Slice, filtering out bad icons 175 for (SliceItem sliceItem : slice.getItems()) { 176 if (!sliceItem.getFormat().equals(SliceItem.FORMAT_IMAGE)) { 177 // Not an image slice 178 continue; 179 } 180 181 Icon icon = sliceItem.getIcon(); 182 if (icon.getType() != Icon.TYPE_URI 183 && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP) { 184 // No URIs to sanitize 185 continue; 186 } 187 188 int iconUriId = android.content.ContentProvider.getUserIdFromUri(icon.getUri(), userId); 189 190 if (iconUriId != userId) { 191 Slog.w(TAG, "sanitizeSlice() user: " + userId + " cannot access icons in Slice"); 192 return null; 193 } 194 } 195 196 return slice; 197 } 198 199 @Nullable toArray(@ullable ArraySet<AutofillId> set)200 static AutofillId[] toArray(@Nullable ArraySet<AutofillId> set) { 201 if (set == null) return null; 202 203 final AutofillId[] array = new AutofillId[set.size()]; 204 for (int i = 0; i < set.size(); i++) { 205 array[i] = set.valueAt(i); 206 } 207 return array; 208 } 209 210 @NonNull paramsToString(@onNull WindowManager.LayoutParams params)211 public static String paramsToString(@NonNull WindowManager.LayoutParams params) { 212 final StringBuilder builder = new StringBuilder(25); 213 params.dumpDimensions(builder); 214 return builder.toString(); 215 } 216 217 @NonNull getFields(@onNull Dataset dataset)218 static ArrayMap<AutofillId, AutofillValue> getFields(@NonNull Dataset dataset) { 219 final ArrayList<AutofillId> ids = dataset.getFieldIds(); 220 final ArrayList<AutofillValue> values = dataset.getFieldValues(); 221 final int size = ids == null ? 0 : ids.size(); 222 final ArrayMap<AutofillId, AutofillValue> fields = new ArrayMap<>(size); 223 for (int i = 0; i < size; i++) { 224 fields.put(ids.get(i), values.get(i)); 225 } 226 return fields; 227 } 228 229 @NonNull newLogMaker(int category, @NonNull String servicePackageName, int sessionId, boolean compatMode)230 private static LogMaker newLogMaker(int category, @NonNull String servicePackageName, 231 int sessionId, boolean compatMode) { 232 final LogMaker log = new LogMaker(category) 233 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName) 234 .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, Integer.toString(sessionId)); 235 if (compatMode) { 236 log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_COMPAT_MODE, 1); 237 } 238 return log; 239 } 240 241 @NonNull newLogMaker(int category, @NonNull String packageName, @NonNull String servicePackageName, int sessionId, boolean compatMode)242 public static LogMaker newLogMaker(int category, @NonNull String packageName, 243 @NonNull String servicePackageName, int sessionId, boolean compatMode) { 244 return newLogMaker(category, servicePackageName, sessionId, compatMode) 245 .setPackageName(packageName); 246 } 247 248 @NonNull newLogMaker(int category, @NonNull ComponentName componentName, @NonNull String servicePackageName, int sessionId, boolean compatMode)249 public static LogMaker newLogMaker(int category, @NonNull ComponentName componentName, 250 @NonNull String servicePackageName, int sessionId, boolean compatMode) { 251 // Remove activity name from logging 252 final ComponentName sanitizedComponentName = 253 new ComponentName(componentName.getPackageName(), ""); 254 return newLogMaker(category, servicePackageName, sessionId, compatMode) 255 .setComponentName(sanitizedComponentName); 256 } 257 printlnRedactedText(@onNull PrintWriter pw, @Nullable CharSequence text)258 public static void printlnRedactedText(@NonNull PrintWriter pw, @Nullable CharSequence text) { 259 if (text == null) { 260 pw.println("null"); 261 } else { 262 pw.print(text.length()); pw.println("_chars"); 263 } 264 } 265 266 /** 267 * Finds the {@link ViewNode} that has the requested {@code autofillId}, if any. 268 */ 269 @Nullable findViewNodeByAutofillId(@onNull AssistStructure structure, @NonNull AutofillId autofillId)270 public static ViewNode findViewNodeByAutofillId(@NonNull AssistStructure structure, 271 @NonNull AutofillId autofillId) { 272 return findViewNode(structure, (node) -> { 273 return autofillId.equals(node.getAutofillId()); 274 }); 275 } 276 findViewNode(@onNull AssistStructure structure, @NonNull ViewNodeFilter filter)277 private static ViewNode findViewNode(@NonNull AssistStructure structure, 278 @NonNull ViewNodeFilter filter) { 279 final ArrayDeque<ViewNode> nodesToProcess = new ArrayDeque<>(); 280 final int numWindowNodes = structure.getWindowNodeCount(); 281 for (int i = 0; i < numWindowNodes; i++) { 282 nodesToProcess.add(structure.getWindowNodeAt(i).getRootViewNode()); 283 } 284 while (!nodesToProcess.isEmpty()) { 285 final ViewNode node = nodesToProcess.removeFirst(); 286 if (filter.matches(node)) { 287 return node; 288 } 289 for (int i = 0; i < node.getChildCount(); i++) { 290 nodesToProcess.addLast(node.getChildAt(i)); 291 } 292 } 293 294 return null; 295 } 296 297 /** 298 * Sanitize the {@code webDomain} property of the URL bar node on compat mode. 299 * 300 * @param structure Assist structure 301 * @param urlBarIds list of ids; only the first id found will be sanitized. 302 * 303 * @return the node containing the URL bar 304 */ 305 @Nullable sanitizeUrlBar(@onNull AssistStructure structure, @NonNull String[] urlBarIds)306 public static ViewNode sanitizeUrlBar(@NonNull AssistStructure structure, 307 @NonNull String[] urlBarIds) { 308 final ViewNode urlBarNode = findViewNode(structure, (node) -> { 309 return ArrayUtils.contains(urlBarIds, node.getIdEntry()); 310 }); 311 if (urlBarNode != null) { 312 final String domain = urlBarNode.getText().toString(); 313 if (domain.isEmpty()) { 314 if (sDebug) Slog.d(TAG, "sanitizeUrlBar(): empty on " + urlBarNode.getIdEntry()); 315 return null; 316 } 317 urlBarNode.setWebDomain(domain); 318 if (sDebug) { 319 Slog.d(TAG, "sanitizeUrlBar(): id=" + urlBarNode.getIdEntry() + ", domain=" 320 + urlBarNode.getWebDomain()); 321 } 322 } 323 return urlBarNode; 324 } 325 326 /** 327 * Gets the value of a metric tag, or {@code 0} if not found or NaN. 328 */ getNumericValue(@onNull LogMaker log, int tag)329 static int getNumericValue(@NonNull LogMaker log, int tag) { 330 final Object value = log.getTaggedData(tag); 331 if (!(value instanceof Number)) { 332 return 0; 333 } else { 334 return ((Number) value).intValue(); 335 } 336 } 337 338 /** 339 * Gets the {@link AutofillId} of the autofillable nodes in the {@code structure}. 340 */ 341 @NonNull getAutofillIds(@onNull AssistStructure structure, boolean autofillableOnly)342 static ArrayList<AutofillId> getAutofillIds(@NonNull AssistStructure structure, 343 boolean autofillableOnly) { 344 final ArrayList<AutofillId> ids = new ArrayList<>(); 345 final int size = structure.getWindowNodeCount(); 346 for (int i = 0; i < size; i++) { 347 final WindowNode node = structure.getWindowNodeAt(i); 348 addAutofillableIds(node.getRootViewNode(), ids, autofillableOnly); 349 } 350 return ids; 351 } 352 addAutofillableIds(@onNull ViewNode node, @NonNull ArrayList<AutofillId> ids, boolean autofillableOnly)353 private static void addAutofillableIds(@NonNull ViewNode node, 354 @NonNull ArrayList<AutofillId> ids, boolean autofillableOnly) { 355 if (!autofillableOnly || node.getAutofillType() != View.AUTOFILL_TYPE_NONE) { 356 AutofillId id = node.getAutofillId(); 357 if (id != null) { 358 ids.add(id); 359 } 360 } 361 final int size = node.getChildCount(); 362 for (int i = 0; i < size; i++) { 363 final ViewNode child = node.getChildAt(i); 364 addAutofillableIds(child, ids, autofillableOnly); 365 } 366 } 367 368 @Nullable createSanitizers(@ullable SaveInfo saveInfo)369 static ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) { 370 if (saveInfo == null) return null; 371 372 final InternalSanitizer[] sanitizerKeys = saveInfo.getSanitizerKeys(); 373 if (sanitizerKeys == null) return null; 374 375 final int size = sanitizerKeys.length; 376 final ArrayMap<AutofillId, InternalSanitizer> sanitizers = new ArrayMap<>(size); 377 if (sDebug) Slog.d(TAG, "Service provided " + size + " sanitizers"); 378 final AutofillId[][] sanitizerValues = saveInfo.getSanitizerValues(); 379 for (int i = 0; i < size; i++) { 380 final InternalSanitizer sanitizer = sanitizerKeys[i]; 381 final AutofillId[] ids = sanitizerValues[i]; 382 if (sDebug) { 383 Slog.d(TAG, "sanitizer #" + i + " (" + sanitizer + ") for ids " 384 + Arrays.toString(ids)); 385 } 386 for (AutofillId id : ids) { 387 sanitizers.put(id, sanitizer); 388 } 389 } 390 return sanitizers; 391 } 392 393 /** 394 * Returns true if {@code s1} contains all characters of {@code s2}, in order. 395 */ containsCharsInOrder(String s1, String s2)396 static boolean containsCharsInOrder(String s1, String s2) { 397 int prevIndex = -1; 398 for (char ch : s2.toCharArray()) { 399 int index = TextUtils.indexOf(s1, ch, prevIndex + 1); 400 if (index == -1) { 401 return false; 402 } 403 prevIndex = index; 404 } 405 return true; 406 } 407 408 /** 409 * Gets a context with the proper display id. 410 * 411 * <p>For most cases it will return the provided context, but on devices that 412 * {@link UserManager#isVisibleBackgroundUsersEnabled() support visible background users}, it 413 * will return a context with the display pased as parameter. 414 */ getDisplayContext(Context context, int displayId)415 static Context getDisplayContext(Context context, int displayId) { 416 if (!UserManager.isVisibleBackgroundUsersEnabled()) { 417 return context; 418 } 419 if (context.getDisplayId() == displayId) { 420 if (sDebug) { 421 Slogf.d(TAG, "getDisplayContext(): context %s already has displayId %d", context, 422 displayId); 423 } 424 return context; 425 } 426 if (sDebug) { 427 Slogf.d(TAG, "Creating context for display %d", displayId); 428 } 429 Display display = context.getSystemService(DisplayManager.class).getDisplay(displayId); 430 if (display == null) { 431 Slogf.wtf(TAG, "Could not get context with displayId %d, Autofill operations will " 432 + "probably fail)", displayId); 433 return context; 434 } 435 436 return context.createDisplayContext(display); 437 } 438 weakDeref(WeakReference<T> weakRef, String tag, String prefix)439 static <T> @Nullable T weakDeref(WeakReference<T> weakRef, String tag, String prefix) { 440 T deref = weakRef.get(); 441 if (deref == null) { 442 Slog.wtf(tag, prefix + "fail to deref " + weakRef); 443 } 444 return deref; 445 } 446 447 private interface ViewNodeFilter { matches(ViewNode node)448 boolean matches(ViewNode node); 449 } 450 451 public static class SaveInfoStats { 452 public int saveInfoCount; 453 public int saveDataTypeCount; 454 SaveInfoStats(int saveInfoCount, int saveDataTypeCount)455 public SaveInfoStats(int saveInfoCount, int saveDataTypeCount) { 456 this.saveInfoCount = saveInfoCount; 457 this.saveDataTypeCount = saveDataTypeCount; 458 } 459 } 460 461 /** 462 * Get statistic information of save info given a sparse array of fill responses. 463 * 464 * Specifically the statistic includes 465 * 1. how many save info the current session has. 466 * 2. How many distinct save data types current session has. 467 * 468 * @return SaveInfoStats returns the above two number in a SaveInfoStats object 469 */ getSaveInfoStatsFromFillResponses( SparseArray<FillResponse> fillResponses)470 public static SaveInfoStats getSaveInfoStatsFromFillResponses( 471 SparseArray<FillResponse> fillResponses) { 472 if (fillResponses == null) { 473 if (sVerbose) { 474 Slog.v(TAG, "getSaveInfoStatsFromFillResponses(): fillResponse sparse array is " 475 + "null"); 476 } 477 return new SaveInfoStats(-1, -1); 478 } 479 int numSaveInfos = 0; 480 int numSaveDataTypes = 0; 481 ArraySet<Integer> saveDataTypeSeen = new ArraySet<>(); 482 final int numResponses = fillResponses.size(); 483 for (int responseNum = 0; responseNum < numResponses; responseNum++) { 484 final FillResponse response = fillResponses.valueAt(responseNum); 485 if (response != null && response.getSaveInfo() != null) { 486 numSaveInfos += 1; 487 int saveDataType = response.getSaveInfo().getType(); 488 if (!saveDataTypeSeen.contains(saveDataType)) { 489 saveDataTypeSeen.add(saveDataType); 490 numSaveDataTypes += 1; 491 } 492 } 493 } 494 return new SaveInfoStats(numSaveInfos, numSaveDataTypes); 495 } 496 } 497