• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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