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