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
17package android.server.cts;
18
19import com.android.ddmlib.Log.LogLevel;
20import com.android.tradefed.device.CollectingOutputReceiver;
21import com.android.tradefed.device.DeviceNotAvailableException;
22import com.android.tradefed.device.ITestDevice;
23import com.android.tradefed.log.LogUtil.CLog;
24import com.android.tradefed.testtype.DeviceTestCase;
25
26import java.lang.Exception;
27import java.lang.Integer;
28import java.lang.String;
29import java.util.HashSet;
30import java.util.regex.Matcher;
31import java.util.regex.Pattern;
32
33import static android.server.cts.StateLogger.log;
34
35public abstract class ActivityManagerTestBase extends DeviceTestCase {
36    private static final boolean PRETEND_DEVICE_SUPPORTS_PIP = false;
37    private static final boolean PRETEND_DEVICE_SUPPORTS_FREEFORM = false;
38
39    // Constants copied from ActivityManager.StackId. If they are changed there, these must be
40    // updated.
41    /** First static stack ID. */
42    public static final int FIRST_STATIC_STACK_ID = 0;
43
44    /** Home activity stack ID. */
45    public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;
46
47    /** ID of stack where fullscreen activities are normally launched into. */
48    public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
49
50    /** ID of stack where freeform/resized activities are normally launched into. */
51    public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;
52
53    /** ID of stack that occupies a dedicated region of the screen. */
54    public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;
55
56    /** ID of stack that always on top (always visible) when it exist. */
57    public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;
58
59    private static final String TASK_ID_PREFIX = "taskId";
60
61    private static final String AM_STACK_LIST = "am stack list";
62
63    private static final String AM_FORCE_STOP_TEST_PACKAGE = "am force-stop android.server.app";
64
65    private static final String AM_REMOVE_STACK = "am stack remove ";
66
67    protected static final String AM_START_HOME_ACTIVITY_COMMAND =
68            "am start -a android.intent.action.MAIN -c android.intent.category.HOME";
69
70    protected static final String AM_MOVE_TOP_ACTIVITY_TO_PINNED_STACK_COMMAND =
71            "am stack move-top-activity-to-pinned-stack 1 0 0 500 500";
72
73    private static final String AM_RESIZE_DOCKED_STACK = "am stack resize-docked-stack ";
74
75    private static final String AM_MOVE_TASK = "am stack movetask ";
76
77    /** A reference to the device under test. */
78    protected ITestDevice mDevice;
79
80    private HashSet<String> mAvailableFeatures;
81
82    protected static String getAmStartCmd(final String activityName) {
83        return "am start -n " + getActivityComponentName(activityName);
84    }
85
86    protected static String getAmStartCmdOverHome(final String activityName) {
87        return "am start --activity-task-on-home -n " + getActivityComponentName(activityName);
88    }
89
90    static String getActivityComponentName(final String activityName) {
91        return "android.server.app/." + activityName;
92    }
93
94    static String getWindowName(final String activityName) {
95        return "android.server.app/android.server.app." + activityName;
96    }
97
98    protected ActivityAndWindowManagersState mAmWmState = new ActivityAndWindowManagersState();
99
100    private int mInitialAccelerometerRotation;
101    private int mUserRotation;
102
103    @Override
104    protected void setUp() throws Exception {
105        super.setUp();
106
107        // Get the device, this gives a handle to run commands and install APKs.
108        mDevice = getDevice();
109        unlockDevice();
110        // Remove special stacks.
111        executeShellCommand(AM_REMOVE_STACK + PINNED_STACK_ID);
112        executeShellCommand(AM_REMOVE_STACK + DOCKED_STACK_ID);
113        executeShellCommand(AM_REMOVE_STACK + FREEFORM_WORKSPACE_STACK_ID);
114        // Store rotation settings.
115        mInitialAccelerometerRotation = getAccelerometerRotation();
116        mUserRotation = getUserRotation();
117    }
118
119    @Override
120    protected void tearDown() throws Exception {
121        super.tearDown();
122        try {
123            unlockDevice();
124            executeShellCommand(AM_FORCE_STOP_TEST_PACKAGE);
125            // Restore rotation settings to the state they were before test.
126            setAccelerometerRotation(mInitialAccelerometerRotation);
127            setUserRotation(mUserRotation);
128            // Remove special stacks.
129            executeShellCommand(AM_REMOVE_STACK + PINNED_STACK_ID);
130            executeShellCommand(AM_REMOVE_STACK + DOCKED_STACK_ID);
131            executeShellCommand(AM_REMOVE_STACK + FREEFORM_WORKSPACE_STACK_ID);
132        } catch (DeviceNotAvailableException e) {
133        }
134    }
135
136    protected String executeShellCommand(String command) throws DeviceNotAvailableException {
137        log("adb shell " + command);
138        return mDevice.executeShellCommand(command);
139    }
140
141    protected void executeShellCommand(String command, CollectingOutputReceiver outputReceiver)
142            throws DeviceNotAvailableException {
143        log("adb shell " + command);
144        mDevice.executeShellCommand(command, outputReceiver);
145    }
146
147    protected void launchActivityInStack(String activityName, int stackId) throws Exception {
148        executeShellCommand(getAmStartCmd(activityName) + " --stack " + stackId);
149    }
150
151    protected void launchActivityInDockStack(String activityName) throws Exception {
152        executeShellCommand(getAmStartCmd(activityName));
153        moveActivityToDockStack(activityName);
154    }
155
156    protected void moveActivityToDockStack(String activityName) throws Exception {
157        moveActivityToStack(activityName, DOCKED_STACK_ID);
158    }
159
160    protected void moveActivityToStack(String activityName, int stackId) throws Exception {
161        final int taskId = getActivityTaskId(activityName);
162        final String cmd = AM_MOVE_TASK + taskId + " " + stackId + " true";
163        executeShellCommand(cmd);
164    }
165
166    protected void resizeActivityTask(String activityName, int left, int top, int right, int bottom)
167            throws Exception {
168        final int taskId = getActivityTaskId(activityName);
169        final String cmd = "am task resize "
170                + taskId + " " + left + " " + top + " " + right + " " + bottom;
171        executeShellCommand(cmd);
172    }
173
174    protected void resizeDockedStack(
175            int stackWidth, int stackHeight, int taskWidth, int taskHeight)
176                    throws DeviceNotAvailableException {
177        executeShellCommand(AM_RESIZE_DOCKED_STACK
178                + "0 0 " + stackWidth + " " + stackHeight
179                + " 0 0 " + taskWidth + " " + taskHeight);
180    }
181
182    // Utility method for debugging, not used directly here, but useful, so kept around.
183    protected void printStacksAndTasks() throws DeviceNotAvailableException {
184        CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
185        executeShellCommand(AM_STACK_LIST, outputReceiver);
186        String output = outputReceiver.getOutput();
187        for (String line : output.split("\\n")) {
188            CLog.logAndDisplay(LogLevel.INFO, line);
189        }
190    }
191
192    protected int getActivityTaskId(String name) throws DeviceNotAvailableException {
193        CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
194        executeShellCommand(AM_STACK_LIST, outputReceiver);
195        final String output = outputReceiver.getOutput();
196        final Pattern activityPattern = Pattern.compile("(.*) " + getWindowName(name) + " (.*)");
197        for (String line : output.split("\\n")) {
198            Matcher matcher = activityPattern.matcher(line);
199            if (matcher.matches()) {
200                for (String word : line.split("\\s+")) {
201                    if (word.startsWith(TASK_ID_PREFIX)) {
202                        final String withColon = word.split("=")[1];
203                        return Integer.parseInt(withColon.substring(0, withColon.length() - 1));
204                    }
205                }
206            }
207        }
208        return -1;
209    }
210
211    protected boolean supportsPip() throws DeviceNotAvailableException {
212        return hasDeviceFeature("android.software.picture_in_picture")
213                || PRETEND_DEVICE_SUPPORTS_PIP;
214    }
215
216    protected boolean supportsFreeform() throws DeviceNotAvailableException {
217        return hasDeviceFeature("android.software.freeform_window_management")
218                || PRETEND_DEVICE_SUPPORTS_FREEFORM;
219    }
220
221    protected boolean hasDeviceFeature(String requiredFeature) throws DeviceNotAvailableException {
222        if (mAvailableFeatures == null) {
223            // TODO: Move this logic to ITestDevice.
224            final String output = runCommandAndPrintOutput("pm list features");
225
226            // Extract the id of the new user.
227            mAvailableFeatures = new HashSet<>();
228            for (String feature: output.split("\\s+")) {
229                // Each line in the output of the command has the format "feature:{FEATURE_VALUE}".
230                String[] tokens = feature.split(":");
231                assertTrue("\"" + feature + "\" expected to have format feature:{FEATURE_VALUE}",
232                        tokens.length > 1);
233                assertEquals(feature, "feature", tokens[0]);
234                mAvailableFeatures.add(tokens[1]);
235            }
236        }
237        boolean result = mAvailableFeatures.contains(requiredFeature);
238        if (!result) {
239            CLog.logAndDisplay(LogLevel.INFO, "Device doesn't support " + requiredFeature);
240        }
241        return result;
242    }
243
244    private boolean isDisplayOn() throws DeviceNotAvailableException {
245        final CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver();
246        mDevice.executeShellCommand("dumpsys power", outputReceiver);
247
248        for (String line : outputReceiver.getOutput().split("\\n")) {
249            line = line.trim();
250
251            final Matcher matcher = sDisplayStatePattern.matcher(line);
252            if (matcher.matches()) {
253                final String state = matcher.group(1);
254                log("power state=" + state);
255                return "ON".equals(state);
256            }
257        }
258        log("power state :(");
259        return false;
260    }
261
262    protected void lockDevice() throws DeviceNotAvailableException {
263        int retriesLeft = 5;
264        runCommandAndPrintOutput("input keyevent 26");
265        do {
266            if (isDisplayOn()) {
267                log("***Waiting for display to turn off...");
268                try {
269                    Thread.sleep(1000);
270                } catch (InterruptedException e) {
271                    log(e.toString());
272                    // Well I guess we are not waiting...
273                }
274            } else {
275                break;
276            }
277        } while (retriesLeft-- > 0);
278    }
279
280    protected void unlockDevice() throws DeviceNotAvailableException {
281        if (!isDisplayOn()) {
282            runCommandAndPrintOutput("input keyevent 224");
283            runCommandAndPrintOutput("input keyevent 82");
284        }
285    }
286
287    protected void setDeviceRotation(int rotation) throws DeviceNotAvailableException {
288        setAccelerometerRotation(0);
289        setUserRotation(rotation);
290    }
291
292    private int getAccelerometerRotation() throws DeviceNotAvailableException {
293        final String rotation =
294                runCommandAndPrintOutput("settings get system accelerometer_rotation");
295        return Integer.parseInt(rotation.trim());
296    }
297
298    private void setAccelerometerRotation(int rotation) throws DeviceNotAvailableException {
299        runCommandAndPrintOutput(
300                "settings put system accelerometer_rotation " + rotation);
301    }
302
303    private int getUserRotation() throws DeviceNotAvailableException {
304        final String rotation =
305                runCommandAndPrintOutput("settings get system user_rotation").trim();
306        if ("null".equals(rotation)) {
307            return -1;
308        }
309        return Integer.parseInt(rotation);
310    }
311
312    private void setUserRotation(int rotation) throws DeviceNotAvailableException {
313        if (rotation == -1) {
314            runCommandAndPrintOutput(
315                    "settings delete system user_rotation");
316        } else {
317            runCommandAndPrintOutput(
318                    "settings put system user_rotation " + rotation);
319        }
320    }
321
322    protected String runCommandAndPrintOutput(String command) throws DeviceNotAvailableException {
323        final String output = executeShellCommand(command);
324        log(output);
325        return output;
326    }
327
328    protected void clearLogcat() throws DeviceNotAvailableException {
329        mDevice.executeAdbCommand("logcat", "-c");
330    }
331
332    protected void assertActivityLifecycle(String activityName, boolean relaunched)
333            throws DeviceNotAvailableException {
334        final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName);
335
336        if (relaunched) {
337            if (lifecycleCounts.mDestroyCount < 1) {
338                fail(activityName + " must have been destroyed. mDestroyCount="
339                        + lifecycleCounts.mDestroyCount);
340            }
341            if (lifecycleCounts.mCreateCount < 1) {
342                fail(activityName + " must have been (re)created. mCreateCount="
343                        + lifecycleCounts.mCreateCount);
344            }
345        } else {
346            if (lifecycleCounts.mDestroyCount > 0) {
347                fail(activityName + " must *NOT* have been destroyed. mDestroyCount="
348                        + lifecycleCounts.mDestroyCount);
349            }
350            if (lifecycleCounts.mCreateCount > 0) {
351                fail(activityName + " must *NOT* have been (re)created. mCreateCount="
352                        + lifecycleCounts.mCreateCount);
353            }
354            if (lifecycleCounts.mConfigurationChangedCount < 1) {
355                fail(activityName + " must have received configuration changed. "
356                        + "mConfigurationChangedCount="
357                        + lifecycleCounts.mConfigurationChangedCount);
358            }
359        }
360    }
361
362    private String[] getDeviceLogsForActivity(String activityName)
363            throws DeviceNotAvailableException {
364        return mDevice.executeAdbCommand("logcat", "-v", "brief", "-d", activityName + ":I", "*:S")
365                .split("\\n");
366    }
367
368    private static final Pattern sCreatePattern = Pattern.compile("(.+): onCreate");
369    private static final Pattern sConfigurationChangedPattern =
370            Pattern.compile("(.+): onConfigurationChanged");
371    private static final Pattern sDestroyPattern = Pattern.compile("(.+): onDestroy");
372    private static final Pattern sNewConfigPattern = Pattern.compile(
373            "(.+): config size=\\((\\d+),(\\d+)\\) displaySize=\\((\\d+),(\\d+)\\)" +
374            " metricsSize=\\((\\d+),(\\d+)\\)");
375    private static final Pattern sDisplayStatePattern =
376            Pattern.compile("Display Power: state=(.+)");
377
378    protected class ReportedSizes {
379        int widthDp;
380        int heightDp;
381        int displayWidth;
382        int displayHeight;
383        int metricsWidth;
384        int metricsHeight;
385    }
386
387    protected ReportedSizes getLastReportedSizesForActivity(String activityName)
388            throws DeviceNotAvailableException {
389        final String[] lines = getDeviceLogsForActivity(activityName);
390        for (int i = lines.length - 1; i >= 0; i--) {
391            final String line = lines[i].trim();
392            final Matcher matcher = sNewConfigPattern.matcher(line);
393            if (matcher.matches()) {
394                ReportedSizes details = new ReportedSizes();
395                details.widthDp = Integer.parseInt(matcher.group(2));
396                details.heightDp = Integer.parseInt(matcher.group(3));
397                details.displayWidth = Integer.parseInt(matcher.group(4));
398                details.displayHeight = Integer.parseInt(matcher.group(5));
399                details.metricsWidth = Integer.parseInt(matcher.group(6));
400                details.metricsHeight = Integer.parseInt(matcher.group(7));
401                return details;
402            }
403        }
404        return null;
405    }
406
407    private class ActivityLifecycleCounts {
408        int mCreateCount;
409        int mConfigurationChangedCount;
410        int mDestroyCount;
411
412        public ActivityLifecycleCounts(String activityName) throws DeviceNotAvailableException {
413            for (String line : getDeviceLogsForActivity(activityName)) {
414                line = line.trim();
415
416                Matcher matcher = sCreatePattern.matcher(line);
417                if (matcher.matches()) {
418                    mCreateCount++;
419                    continue;
420                }
421
422                matcher = sConfigurationChangedPattern.matcher(line);
423                if (matcher.matches()) {
424                    mConfigurationChangedCount++;
425                    continue;
426                }
427
428                matcher = sDestroyPattern.matcher(line);
429                if (matcher.matches()) {
430                    mDestroyCount++;
431                    continue;
432                }
433            }
434        }
435    }
436}
437