1 /*
2  * Copyright (C) 2016 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 android.server.am;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
23 import static android.server.am.ComponentNameUtils.getActivityName;
24 import static android.server.am.ComponentNameUtils.getWindowName;
25 import static android.server.am.StateLogger.log;
26 import static android.server.am.StateLogger.logAlways;
27 import static android.server.am.StateLogger.logE;
28 import static android.server.am.UiDeviceUtils.pressBackButton;
29 import static android.server.am.UiDeviceUtils.pressEnterButton;
30 import static android.server.am.UiDeviceUtils.pressHomeButton;
31 import static android.server.am.UiDeviceUtils.pressSleepButton;
32 import static android.server.am.UiDeviceUtils.pressUnlockButton;
33 import static android.server.am.UiDeviceUtils.pressWakeupButton;
34 import static android.server.am.UiDeviceUtils.waitForDeviceIdle;
35 
36 import android.accessibilityservice.AccessibilityService;
37 import android.app.ActivityManager;
38 import android.app.ActivityTaskManager;
39 import android.content.ComponentName;
40 import android.content.Context;
41 import android.os.SystemClock;
42 
43 import androidx.test.InstrumentationRegistry;
44 
45 import com.android.compatibility.common.util.SystemUtil;
46 
47 import org.junit.After;
48 import org.junit.Before;
49 
50 import java.io.IOException;
51 import java.util.UUID;
52 import java.util.concurrent.TimeUnit;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 public abstract class ActivityManagerTestBase {
57     protected static final int[] ALL_ACTIVITY_TYPE_BUT_HOME = {
58             ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS,
59             ACTIVITY_TYPE_UNDEFINED
60     };
61 
62     private static final String TASK_ID_PREFIX = "taskId";
63 
64     private static final String AM_STACK_LIST = "am stack list";
65 
66     private static final String AM_FORCE_STOP_TEST_PACKAGE = "am force-stop android.server.am";
67     private static final String AM_FORCE_STOP_SECOND_TEST_PACKAGE
68             = "am force-stop android.server.am.second";
69     private static final String AM_FORCE_STOP_THIRD_TEST_PACKAGE
70             = "am force-stop android.server.am.third";
71 
72     protected static final String AM_START_HOME_ACTIVITY_COMMAND =
73             "am start -a android.intent.action.MAIN -c android.intent.category.HOME";
74 
75     private static final String LOCK_CREDENTIAL = "1234";
76 
77     private static final int UI_MODE_TYPE_MASK = 0x0f;
78     private static final int UI_MODE_TYPE_VR_HEADSET = 0x07;
79 
80     protected Context mContext;
81     protected ActivityManager mAm;
82     protected ActivityTaskManager mAtm;
83 
84     /**
85      * @return the am command to start the given activity with the following extra key/value pairs.
86      *         {@param keyValuePairs} must be a list of arguments defining each key/value extra.
87      */
88     // TODO: Make this more generic, for instance accepting flags or extras of other types.
getAmStartCmd(final ComponentName activityName, final String... keyValuePairs)89     protected static String getAmStartCmd(final ComponentName activityName,
90             final String... keyValuePairs) {
91         return getAmStartCmdInternal(getActivityName(activityName), keyValuePairs);
92     }
93 
getAmStartCmdInternal(final String activityName, final String... keyValuePairs)94     private static String getAmStartCmdInternal(final String activityName,
95             final String... keyValuePairs) {
96         return appendKeyValuePairs(
97                 new StringBuilder("am start -n ").append(activityName),
98                 keyValuePairs);
99     }
100 
appendKeyValuePairs( final StringBuilder cmd, final String... keyValuePairs)101     private static String appendKeyValuePairs(
102             final StringBuilder cmd, final String... keyValuePairs) {
103         if (keyValuePairs.length % 2 != 0) {
104             throw new RuntimeException("keyValuePairs must be pairs of key/value arguments");
105         }
106         for (int i = 0; i < keyValuePairs.length; i += 2) {
107             final String key = keyValuePairs[i];
108             final String value = keyValuePairs[i + 1];
109             cmd.append(" --es ")
110                     .append(key)
111                     .append(" ")
112                     .append(value);
113         }
114         return cmd.toString();
115     }
116 
117     protected ActivityAndWindowManagersState mAmWmState = new ActivityAndWindowManagersState();
118 
119     @Before
setUp()120     public void setUp() throws Exception {
121         mContext = InstrumentationRegistry.getContext();
122         mAm = mContext.getSystemService(ActivityManager.class);
123         mAtm = mContext.getSystemService(ActivityTaskManager.class);
124 
125         pressWakeupButton();
126         pressUnlockButton();
127         pressHomeButton();
128     }
129 
130     @After
tearDown()131     public void tearDown() throws Exception {
132         // Synchronous execution of removeStacksWithActivityTypes() ensures that all activities but
133         // home are cleaned up from the stack at the end of each test. Am force stop shell commands
134         // might be asynchronous and could interrupt the stack cleanup process if executed first.
135         executeShellCommand(AM_FORCE_STOP_TEST_PACKAGE);
136         executeShellCommand(AM_FORCE_STOP_SECOND_TEST_PACKAGE);
137         executeShellCommand(AM_FORCE_STOP_THIRD_TEST_PACKAGE);
138         pressHomeButton();
139     }
140 
executeShellCommand(String command)141     public static String executeShellCommand(String command) {
142         log("Shell command: " + command);
143         try {
144             return SystemUtil
145                     .runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
146         } catch (IOException e) {
147             //bubble it up
148             logE("Error running shell command: " + command);
149             throw new RuntimeException(e);
150         }
151     }
152 
launchActivity(final ComponentName activityName, final String... keyValuePairs)153     protected void launchActivity(final ComponentName activityName, final String... keyValuePairs) {
154         executeShellCommand(getAmStartCmd(activityName, keyValuePairs));
155         mAmWmState.waitForValidState(new WaitForValidActivityState(activityName));
156     }
157 
waitForIdle()158     private static void waitForIdle() {
159         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
160     }
161 
launchHomeActivity()162     protected void launchHomeActivity() {
163         executeShellCommand(AM_START_HOME_ACTIVITY_COMMAND);
164         mAmWmState.waitForHomeActivityVisible();
165     }
166 
launchActivity(ComponentName activityName, int windowingMode, final String... keyValuePairs)167     protected void launchActivity(ComponentName activityName, int windowingMode,
168             final String... keyValuePairs) {
169         executeShellCommand(getAmStartCmd(activityName, keyValuePairs)
170                 + " --windowingMode " + windowingMode);
171         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
172                 .setWindowingMode(windowingMode)
173                 .build());
174     }
175 
setActivityTaskWindowingMode(ComponentName activityName, int windowingMode)176     protected void setActivityTaskWindowingMode(ComponentName activityName, int windowingMode) {
177         final int taskId = getActivityTaskId(activityName);
178         mAtm.setTaskWindowingMode(taskId, windowingMode, true /* toTop */);
179         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
180                 .setActivityType(ACTIVITY_TYPE_STANDARD)
181                 .setWindowingMode(windowingMode)
182                 .build());
183     }
184 
185     @Deprecated
getActivityTaskId(final ComponentName activityName)186     protected int getActivityTaskId(final ComponentName activityName) {
187         final String windowName = getWindowName(activityName);
188         final String output = executeShellCommand(AM_STACK_LIST);
189         final Pattern activityPattern = Pattern.compile("(.*) " + windowName + " (.*)");
190         for (final String line : output.split("\\n")) {
191             final Matcher matcher = activityPattern.matcher(line);
192             if (matcher.matches()) {
193                 for (String word : line.split("\\s+")) {
194                     if (word.startsWith(TASK_ID_PREFIX)) {
195                         final String withColon = word.split("=")[1];
196                         return Integer.parseInt(withColon.substring(0, withColon.length() - 1));
197                     }
198                 }
199             }
200         }
201         return -1;
202     }
203 
isTablet()204     protected boolean isTablet() {
205         // Larger than approx 7" tablets
206         return mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600;
207     }
208 
209     // TODO: Switch to using a feature flag, when available.
isUiModeLockedToVrHeadset()210     protected boolean isUiModeLockedToVrHeadset() {
211         final String output = runCommandAndPrintOutput("dumpsys uimode");
212 
213         Integer curUiMode = null;
214         Boolean uiModeLocked = null;
215         for (String line : output.split("\\n")) {
216             line = line.trim();
217             Matcher matcher = sCurrentUiModePattern.matcher(line);
218             if (matcher.find()) {
219                 curUiMode = Integer.parseInt(matcher.group(1), 16);
220             }
221             matcher = sUiModeLockedPattern.matcher(line);
222             if (matcher.find()) {
223                 uiModeLocked = matcher.group(1).equals("true");
224             }
225         }
226 
227         boolean uiModeLockedToVrHeadset = (curUiMode != null) && (uiModeLocked != null)
228                 && ((curUiMode & UI_MODE_TYPE_MASK) == UI_MODE_TYPE_VR_HEADSET) && uiModeLocked;
229 
230         if (uiModeLockedToVrHeadset) {
231             log("UI mode is locked to VR headset");
232         }
233 
234         return uiModeLockedToVrHeadset;
235     }
236 
supportsSplitScreenMultiWindow()237     protected boolean supportsSplitScreenMultiWindow() {
238         return ActivityTaskManager.supportsSplitScreenMultiWindow(mContext);
239     }
240 
hasDeviceFeature(final String requiredFeature)241     protected boolean hasDeviceFeature(final String requiredFeature) {
242         return InstrumentationRegistry.getContext()
243                 .getPackageManager()
244                 .hasSystemFeature(requiredFeature);
245     }
246 
isDisplayOn()247     protected boolean isDisplayOn() {
248         final String output = executeShellCommand("dumpsys power");
249         final Matcher matcher = sDisplayStatePattern.matcher(output);
250         if (matcher.find()) {
251             final String state = matcher.group(1);
252             log("power state=" + state);
253             return "ON".equals(state);
254         }
255         logAlways("power state :(");
256         return false;
257     }
258 
259     protected class LockScreenSession implements AutoCloseable {
260         private static final boolean DEBUG = false;
261 
262         private final boolean mIsLockDisabled;
263         private boolean mLockCredentialSet;
264 
LockScreenSession()265         public LockScreenSession() {
266             mIsLockDisabled = isLockDisabled();
267             mLockCredentialSet = false;
268             // Enable lock screen (swipe) by default.
269             setLockDisabled(false);
270         }
271 
setLockCredential()272         public LockScreenSession setLockCredential() {
273             mLockCredentialSet = true;
274             runCommandAndPrintOutput("locksettings set-pin " + LOCK_CREDENTIAL);
275             return this;
276         }
277 
enterAndConfirmLockCredential()278         public LockScreenSession enterAndConfirmLockCredential() {
279             waitForDeviceIdle(3000);
280 
281             runCommandAndPrintOutput("input text " + LOCK_CREDENTIAL);
282             pressEnterButton();
283             return this;
284         }
285 
removeLockCredential()286         private void removeLockCredential() {
287             runCommandAndPrintOutput("locksettings clear --old " + LOCK_CREDENTIAL);
288             mLockCredentialSet = false;
289         }
290 
disableLockScreen()291         LockScreenSession disableLockScreen() {
292             setLockDisabled(true);
293             return this;
294         }
295 
sleepDevice()296         public LockScreenSession sleepDevice() {
297             pressSleepButton();
298             // Not all device variants lock when we go to sleep, so we need to explicitly lock the
299             // device. Note that pressSleepButton() above is redundant because the action also
300             // puts the device to sleep, but kept around for clarity.
301             InstrumentationRegistry.getInstrumentation().getUiAutomation().performGlobalAction(
302                     AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN);
303             waitForDisplayStateWithRetry(false /* displayOn */ );
304             return this;
305         }
306 
wakeUpDevice()307         protected LockScreenSession wakeUpDevice() {
308             pressWakeupButton();
309             waitForDisplayStateWithRetry(true /* displayOn */ );
310             return this;
311         }
312 
313         /**
314          * If the device has a PIN or password set, this doesn't immediately unlock the device.
315          * Instead, it shows the keyguard and brings the entry field into focus, ready for a
316          * call to enterAndConfirmLockCredential().
317          */
unlockDevice()318         protected LockScreenSession unlockDevice() {
319             pressUnlockButton();
320             SystemClock.sleep(200);
321             return this;
322         }
323 
gotoKeyguard()324         public LockScreenSession gotoKeyguard() {
325             if (DEBUG && isLockDisabled()) {
326                 logE("LockScreenSession.gotoKeyguard() is called without lock enabled.");
327             }
328             sleepDevice();
329             wakeUpDevice();
330             unlockDevice();
331             mAmWmState.waitForKeyguardShowingAndNotOccluded();
332             return this;
333         }
334 
335         @Override
close()336         public void close() throws Exception {
337             setLockDisabled(mIsLockDisabled);
338             if (mLockCredentialSet) {
339                 removeLockCredential();
340             }
341 
342             // Dismiss active keyguard after credential is cleared, so keyguard doesn't ask for
343             // the stale credential.
344             pressBackButton();
345             sleepDevice();
346             wakeUpDevice();
347             unlockDevice();
348         }
349 
350         /**
351          * Returns whether the lock screen is disabled.
352          *
353          * @return true if the lock screen is disabled, false otherwise.
354          */
isLockDisabled()355         private boolean isLockDisabled() {
356             final String isLockDisabled = runCommandAndPrintOutput(
357                     "locksettings get-disabled").trim();
358             return !"null".equals(isLockDisabled) && Boolean.parseBoolean(isLockDisabled);
359         }
360 
361         /**
362          * Disable the lock screen.
363          *
364          * @param lockDisabled true if should disable, false otherwise.
365          */
setLockDisabled(boolean lockDisabled)366         protected void setLockDisabled(boolean lockDisabled) {
367             runCommandAndPrintOutput("locksettings set-disabled " + lockDisabled);
368         }
369 
waitForDisplayStateWithRetry(boolean displayOn)370         protected void waitForDisplayStateWithRetry(boolean displayOn) {
371             for (int retry = 1; isDisplayOn() != displayOn && retry <= 5; retry++) {
372                 logAlways("***Waiting for display state... retry " + retry);
373                 SystemClock.sleep(TimeUnit.SECONDS.toMillis(1));
374             }
375         }
376     }
377 
runCommandAndPrintOutput(String command)378     protected static String runCommandAndPrintOutput(String command) {
379         final String output = executeShellCommand(command);
380         log(output);
381         return output;
382     }
383 
384     protected static class LogSeparator {
385         private final String mUniqueString;
386 
LogSeparator()387         private LogSeparator() {
388             mUniqueString = UUID.randomUUID().toString();
389         }
390 
391         @Override
toString()392         public String toString() {
393             return mUniqueString;
394         }
395     }
396 
397     // TODO: Now that our test are device side, we can convert these to a more direct communication
398     // channel vs. depending on logs.
399     private static final Pattern sDisplayStatePattern =
400             Pattern.compile("Display Power: state=(.+)");
401     private static final Pattern sCurrentUiModePattern = Pattern.compile("mCurUiMode=0x(\\d+)");
402     private static final Pattern sUiModeLockedPattern =
403             Pattern.compile("mUiModeLocked=(true|false)");
404 
stopTestPackage(final ComponentName activityName)405     protected void stopTestPackage(final ComponentName activityName) {
406         executeShellCommand("am force-stop " + activityName.getPackageName());
407     }
408 }
409