1 /*
2  * Copyright (C) 2022 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.platform.helpers;
18 
19 import static android.platform.helpers.ui.UiAutomatorUtils.getInstrumentation;
20 import static android.platform.helpers.ui.UiAutomatorUtils.getUiDevice;
21 import static android.platform.helpers.ui.UiSearch.search;
22 import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
23 
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static java.lang.String.format;
27 
28 import android.app.Instrumentation;
29 import android.content.ComponentName;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
32 import android.content.res.Configuration;
33 import android.content.res.Resources;
34 import android.graphics.Point;
35 import android.graphics.Rect;
36 import android.os.Bundle;
37 import android.os.ParcelFileDescriptor;
38 import android.os.RemoteException;
39 import android.platform.helpers.features.common.HomeLockscreenPage;
40 import android.platform.test.util.HealthTestingUtils;
41 import android.util.Log;
42 
43 import androidx.test.platform.app.InstrumentationRegistry;
44 import androidx.test.uiautomator.BySelector;
45 import androidx.test.uiautomator.UiObject;
46 import androidx.test.uiautomator.UiObjectNotFoundException;
47 
48 import java.io.FileInputStream;
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.List;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 /**
57  * Helper class for writing System UI ui tests. It consists of common utils required while writing
58  * UI tests.
59  */
60 public class CommonUtils {
61 
62     private static final int LARGE_SCREEN_DP_THRESHOLD = 600;
63     private static final String TAG = "CommonUtils";
64     private static final int SWIPE_STEPS = 100;
65     private static final int DEFAULT_MARGIN = 5;
66     private static final String LIST_ALL_USERS_COMMAND = "cmd user list -v --all";
67     private static final String GET_MAIN_USER_COMMAND = "cmd user get-main-user";
68     private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
69     private static final String SPLIT_SHADE_RES_NAME = "config_use_split_notification_shade";
70 
CommonUtils()71     private CommonUtils() {
72     }
73 
74     /**
75      * Prints a message to standard output during an instrumentation test.
76      *
77      * Message will be printed to terminal if test is run using {@code am instrument}. This is
78      * useful for debugging.
79      */
println(String msg)80     public static void println(String msg) {
81         final Bundle streamResult = new Bundle();
82         streamResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, msg + "\n");
83         InstrumentationRegistry.getInstrumentation().sendStatus(0, streamResult);
84     }
85 
86     /**
87      * This method help you execute you shell command.
88      * Example: adb shell pm list packages -f
89      * Here you just need to provide executeShellCommand("pm list packages -f")
90      *
91      * @param command command need to executed.
92      * @return output in String format.
93      */
executeShellCommand(String command)94     public static String executeShellCommand(String command) {
95         Log.d(TAG, format("Executing Shell Command: %s", command));
96         try {
97             String out = getUiDevice().executeShellCommand(command);
98             return out;
99         } catch (IOException e) {
100             Log.d(TAG, format("IOException Occurred: %s", e));
101             throw new RuntimeException(e);
102         }
103     }
104 
105     /** Returns PIDs of all System UI processes */
getSystemUiPids()106     private static String[] getSystemUiPids() {
107         String output = executeShellCommand("pidof com.android.systemui");
108         if (output.isEmpty()) {
109             // explicit check empty string, and return 0-length array.
110             // "".split("\\s") returns 1-length array [""], which invalidates
111             // allSysUiProcessesRestarted check.
112             return new String[0];
113         }
114         return output.split("\\s");
115     }
116 
allSysUiProcessesRestarted(List<String> initialPidsList)117     private static boolean allSysUiProcessesRestarted(List<String> initialPidsList) {
118         final String[] currentPids = getSystemUiPids();
119         Log.d(TAG, "restartSystemUI: Current PIDs=" + Arrays.toString(currentPids));
120         if (currentPids.length < initialPidsList.size()) {
121             return false; // No all processes restarted.
122         }
123         for (String pid : currentPids) {
124             if (initialPidsList.contains(pid)) {
125                 return false; // Old process still running.
126             }
127         }
128         return true;
129     }
130 
131     /**
132      * Restart System UI by running {@code am crash com.android.systemui}.
133      *
134      * <p>This is sometimes necessary after changing flags, configs, or settings ensure that
135      * systemui is properly initialized with the new changes. This method will wait until the home
136      * screen is visible, then it will optionally dismiss the home screen via swipe.
137      *
138      * @param swipeUp whether to call {@link HomeLockscreenPage#swipeUp()} after restarting System
139      *     UI
140      * @deprecated Use {@link SysuiRestarter} instead. It has been moved out from here to use
141      *     androidx uiautomator version (this class depends on the old version, and there are many
142      *     deps that don't allow to easily switch to the new androidx one)
143      */
144     @Deprecated
restartSystemUI(boolean swipeUp)145     public static void restartSystemUI(boolean swipeUp) {
146         SysuiRestarter.restartSystemUI(swipeUp);
147     }
148 
149     /** Asserts that the screen is on. */
assertScreenOn(String errorMessage)150     public static void assertScreenOn(String errorMessage) {
151         try {
152             assertWithMessage(errorMessage)
153                     .that(getUiDevice().isScreenOn())
154                     .isTrue();
155         } catch (RemoteException e) {
156             throw new RuntimeException(e);
157         }
158     }
159 
160     /**
161      * Helper method to swipe the given object.
162      *
163      * @param gestureType direction which to swipe.
164      * @param obj         object which needs to be swiped.
165      */
swipe(GestureType gestureType, UiObject obj)166     public static void swipe(GestureType gestureType, UiObject obj) {
167         Log.d(TAG, format("Swiping Object[%s] %s", obj.getSelector(), gestureType));
168         try {
169             Rect boundary = obj.getBounds();
170             final int displayHeight = getUiDevice().getDisplayHeight() - DEFAULT_MARGIN;
171             final int displayWidth = getUiDevice().getDisplayWidth() - DEFAULT_MARGIN;
172             final int objHeight = boundary.height();
173             final int objWidth = boundary.width();
174             final int marginHeight = (Math.abs(displayHeight - objHeight)) / 2;
175             final int marginWidth = (Math.abs(displayWidth - objWidth)) / 2;
176             switch (gestureType) {
177                 case DOWN:
178                     getUiDevice().swipe(
179                             marginWidth + (objWidth / 2),
180                             marginHeight,
181                             marginWidth + (objWidth / 2),
182                             displayHeight,
183                             SWIPE_STEPS
184                     );
185                     break;
186                 case UP:
187                     getUiDevice().swipe(
188                             marginWidth + (objWidth / 2),
189                             displayHeight,
190                             marginWidth + (objWidth / 2),
191                             marginHeight,
192                             SWIPE_STEPS
193                     );
194                     break;
195                 case RIGHT:
196                     getUiDevice().swipe(
197                             marginWidth,
198                             marginHeight + (objHeight / 2),
199                             displayWidth,
200                             marginHeight + (objHeight / 2),
201                             SWIPE_STEPS
202                     );
203                     break;
204                 case LEFT:
205                     getUiDevice().swipe(
206                             displayWidth,
207                             marginHeight + (objHeight / 2),
208                             marginWidth,
209                             marginHeight + (objHeight / 2),
210                             SWIPE_STEPS
211                     );
212                     break;
213             }
214         } catch (UiObjectNotFoundException e) {
215             Log.e(TAG,
216                     format("Given object was not found. Hence failed to swipe. Exception %s", e));
217             throw new RuntimeException(e);
218         }
219     }
220 
221     /**
222      * Launching an app with different ways.
223      *
224      * @param launchAppWith                  options used to launching an app.
225      * @param packageActivityOrComponentName required package or activity or component name to
226      *                                       launch the given app
227      * @param appName                        name of the app
228      */
launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName, String appName)229     public static void launchApp(LaunchAppWith launchAppWith, String packageActivityOrComponentName,
230             String appName) {
231         Log.d(TAG, String.format("Opening app %s using their %s [%s]",
232                 appName, launchAppWith, packageActivityOrComponentName));
233         Intent appIntent = null;
234         switch (launchAppWith) {
235             case PACKAGE_NAME:
236                 PackageManager packageManager = getContext().getPackageManager();
237                 appIntent = packageManager.getLaunchIntentForPackage(
238                         packageActivityOrComponentName);
239                 appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
240                 break;
241             case ACTIVITY:
242                 appIntent = new Intent(packageActivityOrComponentName);
243                 break;
244             case COMPONENT_NAME:
245                 ComponentName componentName = ComponentName.unflattenFromString(
246                         packageActivityOrComponentName);
247                 appIntent = new Intent();
248                 appIntent.setComponent(componentName);
249                 break;
250             default:
251                 throw new AssertionError("Non-supported Launch App with: " + launchAppWith);
252         }
253         // Ensure the app is completely restarted so that none of the test app's state
254         // leaks between tests.
255         appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
256         getContext().startActivity(appIntent);
257     }
258 
259     /**
260      * Asserts that a given page is visible.
261      *
262      * @param pageSelector      selector helped to verify the page
263      * @param pageName          name of the page to be verified
264      * @param maxTimeoutSeconds max time in seconds to verify the page
265      */
assertPageVisible(BySelector pageSelector, String pageName, int maxTimeoutSeconds)266     public static void assertPageVisible(BySelector pageSelector, String pageName,
267             int maxTimeoutSeconds) {
268         assertWithMessage(format("Page[%s] not visible; selector: %s", pageName, pageSelector))
269                 .that(search(null, pageSelector, format("Page[%s]", pageName), maxTimeoutSeconds))
270                 .isTrue();
271     }
272 
273     /**
274      * Asserts that a given page is not visible.
275      *
276      * @param pageSelector selector helped to verify the page
277      * @param pageName     name of the page to be verified
278      */
assertPageNotVisible(BySelector pageSelector, String pageName)279     public static void assertPageNotVisible(BySelector pageSelector, String pageName) {
280         HealthTestingUtils.waitForCondition(
281                 () -> "Page is still visible",
282                 () -> !search(null, pageSelector, format("Page[%s]", pageName), 0));
283     }
284 
285     /**
286      * Execute the given shell command and get the detailed output
287      *
288      * @param shellCommand shell command to be executed
289      * @return the detailed output as an arraylist.
290      */
executeShellCommandWithDetailedOutput(String shellCommand)291     public static ArrayList<String> executeShellCommandWithDetailedOutput(String shellCommand) {
292         try {
293             ParcelFileDescriptor fileDescriptor =
294                     getInstrumentation().getUiAutomation().executeShellCommand(shellCommand);
295             byte[] buf = new byte[512];
296             int bytesRead;
297             FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(
298                     fileDescriptor);
299             ArrayList<String> output = new ArrayList<>();
300             while ((bytesRead = inputStream.read(buf)) != -1) {
301                 output.add(new String(buf, 0, bytesRead));
302             }
303             inputStream.close();
304             return output;
305         } catch (IOException e) {
306             throw new RuntimeException(e);
307         }
308     }
309 
310     /**
311      * Returns the current user user ID. NOTE: UserID = 0 is for System
312      *
313      * @return a current user ID
314      */
getCurrentUserId()315     public static int getCurrentUserId() {
316         Log.d(TAG, "Getting the Current User ID");
317 
318         // Example terminal output of the list all users command:
319         //
320         //  $ adb shell cmd user list -v --all
321         // 2 users:
322         //
323         // 0: id=0, name=Owner, type=full.SYSTEM, flags=FULL|INITIALIZED|PRIMARY|SYSTEM (running)
324         // 1: id=10, name=Guest, type=full.GUEST, flags=FULL|GUEST|INITIALIZED (running) (current)
325         ArrayList<String> output = executeShellCommandWithDetailedOutput(LIST_ALL_USERS_COMMAND);
326         String getCurrentUser = null;
327         for (String line : output) {
328             if (line.contains("(current)")) {
329                 getCurrentUser = line;
330                 break;
331             }
332         }
333         Pattern userRegex = Pattern.compile("[\\d]+:.*id=([\\d]+).*\\(current\\)");
334         Matcher matcher = userRegex.matcher(getCurrentUser);
335         while (matcher.find()) {
336             return Integer.parseInt(matcher.group(1));
337         }
338 
339         Log.d(TAG, "Failed to find current user ID. dumpsys activity follows:");
340         for (String line : output) {
341             Log.d(TAG, line);
342         }
343         throw new RuntimeException("Failed to find current user ID.");
344     }
345 
346     /**
347      * Returns the main user ID. Main user is the main human user on the device.
348      * Returns 0 by default, if there is no main user. Android Auto is example of HSUM without
349      * main user.
350      *
351      * NOTE: For headless system main user it is NOT 0. Therefore Main user should be used in
352      * test cases rather than owner or deprecated primary user.
353      */
getMainUserId()354     public static int getMainUserId() {
355         ArrayList<String> output = executeShellCommandWithDetailedOutput(GET_MAIN_USER_COMMAND);
356         try {
357             return Integer.parseInt(output.get(0).trim());
358         } catch (NumberFormatException e) {
359             return 0;
360         }
361     }
362 
isSplitShade()363     public static boolean isSplitShade() {
364         try {
365             Resources sysUiResources =
366                     getContext().getPackageManager().getResourcesForApplication(SYSTEM_UI_PACKAGE);
367             int resourceId =
368                     sysUiResources.getIdentifier(SPLIT_SHADE_RES_NAME, "bool", SYSTEM_UI_PACKAGE);
369             return sysUiResources.getBoolean(resourceId);
370         } catch (PackageManager.NameNotFoundException e) {
371             Log.e(TAG, "Couldn't get SysUI resources");
372         }
373 
374         // Fallback check, accurate for most but not necessarily all devices
375         int orientation = getContext().getResources().getConfiguration().orientation;
376         return isLargeScreen() && (orientation == Configuration.ORIENTATION_LANDSCAPE);
377     }
378 
isLargeScreen()379     public static boolean isLargeScreen() {
380         Point sizeDp = getUiDevice().getDisplaySizeDp();
381         return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD;
382     }
383 
384     /**
385      * Gesture for swipe
386      */
387     public enum GestureType {
388         RIGHT,
389         LEFT,
390         UP,
391         DOWN
392     }
393 
394     /**
395      * Different options used for launching an app.
396      */
397     public enum LaunchAppWith {
398         PACKAGE_NAME,
399         ACTIVITY,
400         COMPONENT_NAME
401     }
402 }
403