1 /*
2  * Copyright (C) 2024 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.cts.mockime;
18 
19 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
20 
21 import android.Manifest;
22 import android.app.ActivityManager;
23 import android.app.ApplicationExitInfo;
24 import android.app.UiAutomation;
25 import android.content.ContentProviderClient;
26 import android.content.Context;
27 import android.content.pm.PackageManager;
28 import android.os.Bundle;
29 import android.os.UserHandle;
30 import android.util.ArraySet;
31 import android.view.inputmethod.InputMethodInfo;
32 import android.view.inputmethod.InputMethodManager;
33 
34 import androidx.annotation.IntRange;
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 
38 import com.android.compatibility.common.util.SystemUtil;
39 import com.android.compatibility.common.util.ThrowingSupplier;
40 
41 import java.io.IOException;
42 import java.util.List;
43 import java.util.Objects;
44 
45 /**
46  * Utility methods for testing multi-user scenarios.
47  *
48  * <p>TODO(b/323251870): Consider creating a new utility class to host logic like this.</p>
49  */
50 final class MultiUserUtils {
51     /**
52      * Not intended to be instantiated.
53      */
MultiUserUtils()54     private MultiUserUtils() {
55     }
56 
57     @Nullable
runWithShellPermissionIdentity(@onNull UiAutomation uiAutomation, @NonNull ThrowingSupplier<T> supplier, String... permissions)58     private static <T> T runWithShellPermissionIdentity(@NonNull UiAutomation uiAutomation,
59             @NonNull ThrowingSupplier<T> supplier, String... permissions) {
60         Object[] placeholder = new Object[1];
61         SystemUtil.runWithShellPermissionIdentity(uiAutomation, () -> {
62             placeholder[0] = supplier.get();
63         }, permissions);
64         return (T) placeholder[0];
65     }
66 
67     @NonNull
runShellCommandOrThrow(@onNull UiAutomation uiAutomation, @NonNull String cmd)68     private static String runShellCommandOrThrow(@NonNull UiAutomation uiAutomation,
69             @NonNull String cmd) {
70         try {
71             return runShellCommand(uiAutomation, cmd);
72         } catch (IOException e) {
73             throw new RuntimeException(e);
74         }
75     }
76 
77     @Nullable
runWithShellPermissionIdentity(@onNull UiAutomation uiAutomation, @NonNull ThrowingSupplier<T> supplier)78     private static <T> T runWithShellPermissionIdentity(@NonNull UiAutomation uiAutomation,
79             @NonNull ThrowingSupplier<T> supplier) {
80         return runWithShellPermissionIdentity(uiAutomation, supplier,
81                 (String[]) null /* permissions */);
82     }
83 
84     @NonNull
callContentProvider(@onNull Context context, @NonNull UiAutomation uiAutomation, @NonNull String authority, @NonNull String method, @Nullable String arg, @Nullable Bundle extras, @NonNull UserHandle user)85     static Bundle callContentProvider(@NonNull Context context, @NonNull UiAutomation uiAutomation,
86             @NonNull String authority, @NonNull String method, @Nullable String arg,
87             @Nullable Bundle extras, @NonNull UserHandle user) {
88         return Objects.requireNonNull(runWithShellPermissionIdentity(uiAutomation, () -> {
89             final Context userAwareContext;
90             try {
91                 userAwareContext = context.createPackageContextAsUser("android", 0, user);
92             } catch (PackageManager.NameNotFoundException e) {
93                 throw new RuntimeException(e);
94             }
95             return userAwareContext.getContentResolver().call(authority, method, arg, extras);
96         }));
97     }
98 
99     @Nullable
getCurrentInputMethodInfoAsUser(@onNull Context context, @NonNull UiAutomation uiAutomation, @NonNull UserHandle user)100     static InputMethodInfo getCurrentInputMethodInfoAsUser(@NonNull Context context,
101             @NonNull UiAutomation uiAutomation, @NonNull UserHandle user) {
102         Objects.requireNonNull(user);
103         final InputMethodManager imm = Objects.requireNonNull(
104                 context.getSystemService(InputMethodManager.class));
105         return runWithShellPermissionIdentity(uiAutomation, () ->
106                 imm.getCurrentInputMethodInfoAsUser(user),
107                 Manifest.permission.INTERACT_ACROSS_USERS_FULL);
108     }
109 
110     @NonNull
getHistoricalProcessExitReasons(@onNull Context context, @NonNull UiAutomation uiAutomation, @Nullable String packageName, @IntRange(from = 0) int pid, @IntRange(from = 0) int maxNum, @NonNull UserHandle user)111     static List<ApplicationExitInfo> getHistoricalProcessExitReasons(@NonNull Context context,
112             @NonNull UiAutomation uiAutomation, @Nullable String packageName,
113             @IntRange(from = 0) int pid, @IntRange(from = 0) int maxNum, @NonNull UserHandle user) {
114         return Objects.requireNonNull(runWithShellPermissionIdentity(uiAutomation, () -> {
115             final Context userAwareContext;
116             try {
117                 userAwareContext = context.createPackageContextAsUser("android", 0, user);
118             } catch (PackageManager.NameNotFoundException e) {
119                 throw new RuntimeException(e);
120             }
121             return Objects.requireNonNull(userAwareContext.getSystemService(ActivityManager.class))
122                     .getHistoricalProcessExitReasons(packageName, pid, maxNum);
123         }, Manifest.permission.INTERACT_ACROSS_USERS_FULL, Manifest.permission.DUMP));
124     }
125 
126     @NonNull
127     static List<InputMethodInfo> getInputMethodListAsUser(@NonNull Context context,
128             @NonNull UiAutomation uiAutomation, @NonNull UserHandle user) {
129         final InputMethodManager imm = Objects.requireNonNull(
130                 context.getSystemService(InputMethodManager.class));
131         return Objects.requireNonNull(runWithShellPermissionIdentity(uiAutomation,
132                 () -> imm.getInputMethodListAsUser(user.getIdentifier()),
133                 Manifest.permission.INTERACT_ACROSS_USERS_FULL,
134                 Manifest.permission.QUERY_ALL_PACKAGES));
135     }
136 
137     @NonNull
138     static List<InputMethodInfo> getEnabledInputMethodListAsUser(@NonNull Context context,
139             @NonNull UiAutomation uiAutomation, @NonNull UserHandle user) {
140         final InputMethodManager imm = Objects.requireNonNull(
141                 context.getSystemService(InputMethodManager.class));
142         final List<InputMethodInfo> result = runWithShellPermissionIdentity(uiAutomation, () -> {
143             try {
144                 return imm.getEnabledInputMethodListAsUser(user);
145             } catch (NoSuchMethodError unused) {
146                 return null;
147             }
148         }, Manifest.permission.INTERACT_ACROSS_USERS_FULL, Manifest.permission.QUERY_ALL_PACKAGES);
149         if (result != null) {
150             return result;
151         }
152 
153         // Use the shell command as a fallback.
154         final String command = "ime list -s --user " + user.getIdentifier();
155         final var enabledImes = new ArraySet<>(
156                 runShellCommandOrThrow(uiAutomation, command).split("\n"));
157         final List<InputMethodInfo> imes = getInputMethodListAsUser(context, uiAutomation, user);
158         imes.removeIf(imi -> !enabledImes.contains(imi.getId()));
159         return imes;
160     }
161 
162     @NonNull
163     static AutoCloseable acquireUnstableContentProviderClientSession(@NonNull Context context,
164             @NonNull UiAutomation uiAutomation, @NonNull String name, @NonNull UserHandle user) {
165         return Objects.requireNonNull(runWithShellPermissionIdentity(uiAutomation, () -> {
166             final Context userAwareContext;
167             try {
168                 userAwareContext = context.createPackageContextAsUser("android", 0, user);
169             } catch (PackageManager.NameNotFoundException e) {
170                 throw new RuntimeException(e);
171             }
172             final ContentProviderClient client = Objects.requireNonNull(
173                     userAwareContext.getContentResolver()
174                             .acquireUnstableContentProviderClient(name));
175             return () -> SystemUtil.runWithShellPermissionIdentity(uiAutomation, client::close);
176         }));
177     }
178 }
179