1 /* 2 * Copyright (C) 2023 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.compatibility.common.util; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 import static android.view.Display.INVALID_DISPLAY; 21 22 import android.app.ActivityOptions; 23 import android.content.Context; 24 import android.os.Build; 25 import android.os.UserHandle; 26 import android.os.UserManager; 27 import android.util.Log; 28 import android.view.InputEvent; 29 import android.view.KeyEvent; 30 import android.view.MotionEvent; 31 32 import androidx.annotation.Nullable; 33 import androidx.test.platform.app.InstrumentationRegistry; 34 35 import java.util.Objects; 36 import java.util.function.Function; 37 38 /** 39 * Helper class providing methods to interact with the user under test. 40 * 41 * <p>For example, it knows if the user was {@link 42 * android.app.ActivityManager#startUserInBackgroundVisibleOnDisplay(int, int) started visible in a 43 * display} and provide methods (like {@link #injectDisplayIdIfNeeded(ActivityOptions)}) to help 44 * tests support such behavior. 45 */ 46 // TODO(b/271153404): move logic to bedstead and/or rename it to UserVisibilityHelper 47 public final class UserHelper { 48 49 private static final String TAG = "CtsUserHelper"; 50 51 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 52 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 53 54 private final boolean mVisibleBackgroundUsersSupported; 55 private final UserHandle mUser; 56 private final boolean mIsVisibleBackgroundUser; 57 private final int mDisplayId; 58 59 /** 60 * Creates a helper using the application context from the target context (falling back to the 61 * target context itself when it doesn't have an application context). 62 */ UserHelper()63 public UserHelper() { 64 this(getApplicationContext()); 65 } 66 67 /** 68 * Creates a helper for the given context. 69 */ UserHelper(Context context)70 public UserHelper(Context context) { 71 mUser = Objects.requireNonNull(context, "Context on UserHelper constructor cannot be null") 72 .getUser(); 73 UserManager userManager = context.getSystemService(UserManager.class); 74 75 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 76 mVisibleBackgroundUsersSupported = false; 77 if (DEBUG) { 78 Log.d(TAG, "Pre-UDC constructor (mUser=" + mUser + "): setting " 79 + "mVisibleBackgroundUsersSupported as false"); 80 } 81 } else { 82 mVisibleBackgroundUsersSupported = userManager.isVisibleBackgroundUsersSupported(); 83 } 84 if (!mVisibleBackgroundUsersSupported) { 85 if (DEBUG) { 86 Log.d(TAG, "Device doesn't support visible background users; setting mDisplayId as" 87 + " DEFAULT_DISPLAY and mIsVisibleBackgroundUser as false"); 88 } 89 mIsVisibleBackgroundUser = false; 90 mDisplayId = DEFAULT_DISPLAY; 91 return; 92 } 93 94 boolean isForeground = userManager.isUserForeground(); 95 boolean isProfile = userManager.isProfile(); 96 int displayId = DEFAULT_DISPLAY; 97 try { 98 // NOTE: getMainDisplayIdAssignedToUser() was added on UDC, but it's a @TestApi, so it 99 // will throw a NoSuchMethodError if the test is not configured to allow it 100 displayId = userManager.getMainDisplayIdAssignedToUser(); 101 } catch (NoSuchMethodError e) { 102 Log.wtf(TAG, "test is not configured to access @TestApi; setting mDisplayId as" 103 + " DEFAULT_DISPLAY", e); 104 } 105 mDisplayId = displayId; 106 boolean isVisible = userManager.isUserVisible(); 107 if (DEBUG) { 108 Log.d(TAG, "Constructor: mUser=" + mUser + ", visible=" + isVisible 109 + ", isForeground=" + isForeground + ", isProfile=" + isProfile 110 + ", mDisplayId=" + mDisplayId + ", mVisibleBackgroundUsersSupported=" 111 + mVisibleBackgroundUsersSupported); 112 } 113 // TODO(b/271153404): use TestApis.users().instrument() to set mIsVisibleBackgroundUser 114 if (isVisible && !isForeground && !isProfile) { 115 if (mDisplayId == INVALID_DISPLAY) { 116 throw new IllegalStateException("UserManager returned INVALID_DISPLAY for " 117 + "visible background user " + mUser); 118 } 119 mIsVisibleBackgroundUser = true; 120 Log.i(TAG, "Test user " + mUser + " is running on background, visible on display " 121 + mDisplayId); 122 } else { 123 mIsVisibleBackgroundUser = false; 124 if (DEBUG) { 125 Log.d(TAG, "Test user " + mUser + " is not running visible on background"); 126 } 127 } 128 } 129 130 /** 131 * Checks if the user is a full user (i.e, not a {@link UserManager#isProfile() profile}) and 132 * is {@link UserManager#isVisibleBackgroundUsersEnabled() running in the background but 133 * visible in a display}; if it's not, then it's either the 134 * {@link android.app.ActivityManager#getCurrentUser() current foreground user}, a profile, or a 135 * full user running in background but not {@link UserManager#isUserVisible() visible}. 136 */ isVisibleBackgroundUser()137 public boolean isVisibleBackgroundUser() { 138 return mIsVisibleBackgroundUser; 139 } 140 141 /** 142 * Convenience method to return {@link UserManager#isVisibleBackgroundUsersSupported()}. 143 */ isVisibleBackgroundUserSupported()144 public boolean isVisibleBackgroundUserSupported() { 145 return mVisibleBackgroundUsersSupported; 146 } 147 148 /** 149 * Convenience method to get the user running this test. 150 */ getUser()151 public UserHandle getUser() { 152 return mUser; 153 } 154 155 /** 156 * Convenience method to get the id of the {@link #getUser() user running this test}. 157 */ getUserId()158 public int getUserId() { 159 return mUser.getIdentifier(); 160 } 161 162 /** 163 * Gets the display id the {@link #getUser() user} {@link #isVisibleBackgroundUser() is 164 * running visible on}. 165 * 166 * <p>Notice that this id is not necessarily the same as the id returned by 167 * {@link Context#getDisplayId()}, as that method returns {@link INVALID_DISPLAY} on contexts 168 * that are not associated with a {@link Context#isUiContext() UI}. 169 */ getMainDisplayId()170 public int getMainDisplayId() { 171 return mDisplayId; 172 } 173 174 /** 175 * Gets an {@link ActivityOptions} that can be used to launch an activity in the display under 176 * test. 177 */ getActivityOptions()178 public ActivityOptions getActivityOptions() { 179 return injectDisplayIdIfNeeded((ActivityOptions) null); 180 } 181 182 /** 183 * Get the proper {@code cmd appops} with the user id set, including the trailing space. 184 */ getAppopsCmd(String command)185 public String getAppopsCmd(String command) { 186 return "cmd appops " + command + " --user " + getUserId() + " "; 187 } 188 189 /** 190 * Get a {@code cmd input} for the given {@code source}, setting the proper display (if needed). 191 */ getInputCmd(String source)192 public String getInputCmd(String source) { 193 StringBuilder cmd = new StringBuilder("cmd input ").append(source); 194 if (mIsVisibleBackgroundUser) { 195 cmd.append(" -d ").append(mDisplayId); 196 } 197 198 return cmd.toString(); 199 } 200 201 /** 202 * Augments a existing {@link ActivityOptions} (or create a new one), injecting the 203 * {{@link #getMainDisplayId()} if needed. 204 */ injectDisplayIdIfNeeded(@ullable ActivityOptions options)205 public ActivityOptions injectDisplayIdIfNeeded(@Nullable ActivityOptions options) { 206 ActivityOptions augmentedOptions = options != null ? options : ActivityOptions.makeBasic(); 207 if (mIsVisibleBackgroundUser) { 208 augmentedOptions.setLaunchDisplayId(mDisplayId); 209 } 210 Log.v(TAG, "injectDisplayIdIfNeeded(): returning " + augmentedOptions); 211 return augmentedOptions; 212 } 213 214 /** 215 * Sets the display id of the event if the test is running in a visible background user. 216 */ injectDisplayIdIfNeeded(MotionEvent event)217 public MotionEvent injectDisplayIdIfNeeded(MotionEvent event) { 218 return injectDisplayIdIfNeeded(event, MotionEvent.class, 219 (e) -> MotionEvent.actionToString(event.getAction())); 220 } 221 222 /** 223 * Sets the display id of the event if the test is running in a visible background user. 224 */ injectDisplayIdIfNeeded(KeyEvent event)225 public KeyEvent injectDisplayIdIfNeeded(KeyEvent event) { 226 return injectDisplayIdIfNeeded(event, KeyEvent.class, 227 (e) -> KeyEvent.actionToString(event.getAction())); 228 } 229 injectDisplayIdIfNeeded(T event, Class<T> clazz, Function<T, String> liteStringGenerator)230 private <T extends InputEvent> T injectDisplayIdIfNeeded(T event, Class<T> clazz, 231 Function<T, String> liteStringGenerator) { 232 if (!isVisibleBackgroundUserSupported()) { 233 return event; 234 } 235 int eventDisplayId = event.getDisplayId(); 236 if (!mIsVisibleBackgroundUser) { 237 if (DEBUG) { 238 Log.d(TAG, "Not replacing display id (" + eventDisplayId + "->" + mDisplayId 239 + ") as user is not running visible on background"); 240 } 241 return event; 242 } 243 event.setDisplayId(mDisplayId); 244 if (VERBOSE) { 245 Log.v(TAG, "Replaced displayId (" + eventDisplayId + "->" + mDisplayId + ") on " 246 + event); 247 } else if (DEBUG) { 248 Log.d(TAG, "Replaced displayId (" + eventDisplayId + "->" + mDisplayId + ") on " 249 + liteStringGenerator.apply(event)); 250 } 251 return event; 252 } 253 254 @Override toString()255 public String toString() { 256 return getClass().getSimpleName() + "[user=" + mUser + ", displayId=" + mDisplayId 257 + ", isVisibleBackgroundUser=" + mIsVisibleBackgroundUser 258 + ", isVisibleBackgroundUsersSupported" + mVisibleBackgroundUsersSupported 259 + "]"; 260 } 261 getApplicationContext()262 private static Context getApplicationContext() { 263 Context targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 264 Context appContext = targetContext.getApplicationContext(); 265 if (appContext == null) { 266 Log.w(TAG, "getApplicationContext(): target context (" + targetContext 267 + ") doesn't have an application context; returning it instead"); 268 return targetContext; 269 } 270 return appContext; 271 } 272 } 273