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