• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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