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