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