1 /**
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11  * express or implied. See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 
15 package android.accessibilityservice.cts.utils;
16 
17 import static android.accessibility.cts.common.ShellCommandBuilder.execShellCommand;
18 import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
19 import static android.accessibilityservice.cts.utils.CtsTestUtils.isAutomotive;
20 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
21 
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.fail;
24 
25 import android.accessibilityservice.AccessibilityServiceInfo;
26 import android.app.Activity;
27 import android.app.ActivityOptions;
28 import android.app.Instrumentation;
29 import android.app.KeyguardManager;
30 import android.app.UiAutomation;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageManager;
34 import android.content.pm.ResolveInfo;
35 import android.graphics.Rect;
36 import android.os.PowerManager;
37 import android.os.SystemClock;
38 import android.text.TextUtils;
39 import android.util.Log;
40 import android.util.SparseArray;
41 import android.view.Display;
42 import android.view.InputDevice;
43 import android.view.KeyCharacterMap;
44 import android.view.KeyEvent;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityNodeInfo;
47 import android.view.accessibility.AccessibilityWindowInfo;
48 
49 import androidx.test.rule.ActivityTestRule;
50 
51 import com.android.compatibility.common.util.TestUtils;
52 
53 import java.util.Arrays;
54 import java.util.List;
55 import java.util.Objects;
56 import java.util.concurrent.TimeoutException;
57 import java.util.stream.Collectors;
58 
59 /**
60  * Utilities useful when launching an activity to make sure it's all the way on the screen
61  * before we start testing it.
62  */
63 public class ActivityLaunchUtils {
64     private static final String LOG_TAG = "ActivityLaunchUtils";
65     private static final String AM_START_HOME_ACTIVITY_COMMAND =
66             "am start -a android.intent.action.MAIN -c android.intent.category.HOME";
67     public static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND =
68             "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
69     public static final String INPUT_KEYEVENT_KEYCODE_BACK =
70             "input keyevent KEYCODE_BACK";
71     public static final String INPUT_KEYEVENT_KEYCODE_MENU =
72             "input keyevent KEYCODE_MENU";
73 
74     // Precision when asserting the launched activity bounds equals the reported a11y window bounds.
75     private static final int BOUNDS_PRECISION_PX = 1;
76 
77     // Using a static variable so it can be used in lambdas. Not preserving state in it.
78     private static Activity mTempActivity;
79 
launchActivityAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityTestRule<T> rule)80     public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen(
81             Instrumentation instrumentation, UiAutomation uiAutomation,
82             ActivityTestRule<T> rule) throws Exception {
83         ActivityLauncher activityLauncher = new ActivityLauncher() {
84             @Override
85             Activity launchActivity() {
86                 return rule.launchActivity(null);
87             }
88         };
89         return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation,
90                 uiAutomation, activityLauncher, Display.DEFAULT_DISPLAY);
91     }
92 
93     /**
94      * If this activity would be launched at virtual display, please finishes this activity before
95      * this test ended. Otherwise it will be displayed on default display and impacts the next test.
96      */
launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)97     public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
98             Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz,
99             int displayId) throws Exception {
100         final ActivityOptions options = ActivityOptions.makeBasic();
101         options.setLaunchDisplayId(displayId);
102         final Intent intent = new Intent(instrumentation.getTargetContext(), clazz);
103         // Add clear task because this activity may on other display.
104         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
105 
106         ActivityLauncher activityLauncher = new ActivityLauncher() {
107             @Override
108             Activity launchActivity() {
109                 uiAutomation.adoptShellPermissionIdentity();
110                 try {
111                     return instrumentation.startActivitySync(intent, options.toBundle());
112                 } finally {
113                     uiAutomation.dropShellPermissionIdentity();
114                 }
115             }
116         };
117         return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation,
118                 uiAutomation, activityLauncher, displayId);
119     }
120 
getActivityTitle( Instrumentation instrumentation, Activity activity)121     public static CharSequence getActivityTitle(
122             Instrumentation instrumentation, Activity activity) {
123         final StringBuilder titleBuilder = new StringBuilder();
124         instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle()));
125         return titleBuilder;
126     }
127 
findWindowByTitle( UiAutomation uiAutomation, CharSequence title)128     public static AccessibilityWindowInfo findWindowByTitle(
129             UiAutomation uiAutomation, CharSequence title) {
130         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
131         return findWindowByTitleWithList(title, windows);
132     }
133 
findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)134     public static AccessibilityWindowInfo findWindowByTitleAndDisplay(
135             UiAutomation uiAutomation, CharSequence title, int displayId) {
136         final SparseArray<List<AccessibilityWindowInfo>> allWindows =
137                 uiAutomation.getWindowsOnAllDisplays();
138         final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId);
139         return findWindowByTitleWithList(title, windowsOfDisplay);
140     }
141 
homeScreenOrBust(Context context, UiAutomation uiAutomation)142     public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) {
143         wakeUpOrBust(context, uiAutomation);
144         if (context.getPackageManager().isInstantApp()) return;
145         if (isHomeScreenShowing(context, uiAutomation)) return;
146         final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
147         final int enabledFlags = serviceInfo.flags;
148         // Make sure we could query windows.
149         serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
150         uiAutomation.setServiceInfo(serviceInfo);
151         try {
152             KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
153             if (keyguardManager != null) {
154                 TestUtils.waitUntil("Screen is unlocked",
155                         (int) DEFAULT_TIMEOUT_MS / 1000,
156                         () -> {
157                             if (!keyguardManager.isKeyguardLocked()) {
158                                 return true;
159                             }
160                             execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_MENU);
161                             return false;
162                         });
163             }
164             execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND);
165             execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
166             execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK);
167             TestUtils.waitUntil("Home screen is showing",
168                     (int) DEFAULT_TIMEOUT_MS / 1000,
169                     () -> {
170                         if (isHomeScreenShowing(context, uiAutomation)) {
171                             return true;
172                         }
173                         // Attempt to close any newly-appeared system dialogs which can prevent the
174                         // home screen activity from becoming visible, active, and focused.
175                         execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
176                         return false;
177                     });
178         } catch (Exception error) {
179             Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list");
180             final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
181             if (windows == null) {
182                 Log.e(LOG_TAG, "Window list is null");
183             } else if (windows.isEmpty()) {
184                 Log.e(LOG_TAG, "Window list is empty");
185             } else {
186                 for (AccessibilityWindowInfo window : windows) {
187                     Log.e(LOG_TAG, window.toString());
188                 }
189             }
190 
191             fail("Unable to reach home screen");
192         } finally {
193             serviceInfo.flags = enabledFlags;
194             uiAutomation.setServiceInfo(serviceInfo);
195         }
196     }
197 
supportsMultiDisplay(Context context)198     public static boolean supportsMultiDisplay(Context context) {
199         return context.getPackageManager().hasSystemFeature(
200                 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS);
201     }
202 
isHomeScreenShowing(Context context, UiAutomation uiAutomation)203     public static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) {
204         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
205         final PackageManager packageManager = context.getPackageManager();
206         final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
207                 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
208                 PackageManager.MATCH_DEFAULT_ONLY);
209         final boolean isAuto = isAutomotive(context);
210 
211         // Look for an active focused window with a package name that matches
212         // the default home screen.
213         for (AccessibilityWindowInfo window : windows) {
214             if (!isAuto) {
215                 // Auto does not set its home screen app as active+focused, so only non-auto
216                 // devices enforce that the home screen is active+focused.
217                 if (!window.isActive() || !window.isFocused()) {
218                     continue;
219                 }
220             }
221             final AccessibilityNodeInfo root = window.getRoot();
222             if (root != null) {
223                 final CharSequence packageName = root.getPackageName();
224                 if (packageName != null) {
225                     for (ResolveInfo resolveInfo : resolveInfos) {
226                         if ((resolveInfo.activityInfo != null)
227                                 && packageName.equals(resolveInfo.activityInfo.packageName)) {
228                             return true;
229                         }
230                     }
231                 }
232             }
233         }
234         // List unexpected package names of default home screen that invoking ResolverActivity
235         final CharSequence homePackageNames = resolveInfos.stream()
236                 .map(r -> r.activityInfo).filter(Objects::nonNull)
237                 .map(a -> a.packageName).collect(Collectors.joining(", "));
238         Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames);
239         return false;
240     }
241 
wakeUpOrBust(Context context, UiAutomation uiAutomation)242     private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) {
243         final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS;
244         final PowerManager powerManager = context.getSystemService(PowerManager.class);
245         do {
246             if (powerManager.isInteractive()) {
247                 Log.d(LOG_TAG, "Device is interactive");
248                 return;
249             }
250 
251             Log.d(LOG_TAG, "Sending wakeup keycode");
252             final long eventTime = SystemClock.uptimeMillis();
253             uiAutomation.injectInputEvent(
254                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
255                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
256                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
257                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
258             uiAutomation.injectInputEvent(
259                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
260                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
261                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
262                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
263             try {
264                 Thread.sleep(50);
265             } catch (InterruptedException e) {
266             }
267         } while (SystemClock.uptimeMillis() < deadlineUptimeMillis);
268         fail("Unable to wake up screen");
269     }
270 
launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)271     private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
272             Instrumentation instrumentation, UiAutomation uiAutomation,
273             ActivityLauncher activityLauncher, int displayId) throws Exception {
274         final int[] location = new int[2];
275         final StringBuilder activityPackage = new StringBuilder();
276         final Rect bounds = new Rect();
277         final StringBuilder activityTitle = new StringBuilder();
278         final StringBuilder timeoutExceptionRecords = new StringBuilder();
279         // Make sure we get window events, so we'll know when the window appears
280         AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
281         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
282         uiAutomation.setServiceInfo(info);
283         // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only
284         // checking the home screen for default display.
285         if (displayId == Display.DEFAULT_DISPLAY) {
286             homeScreenOrBust(instrumentation.getContext(), uiAutomation);
287         }
288 
289         try {
290             final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
291                     () -> {
292                         mTempActivity = activityLauncher.launchActivity();
293                         instrumentation.runOnMainSync(() -> {
294                             mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
295                             activityPackage.append(mTempActivity.getPackageName());
296                         });
297                         instrumentation.waitForIdleSync();
298                         activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
299                     },
300                     (event) -> {
301                         final AccessibilityWindowInfo window =
302                                 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId);
303                         if (window == null || window.getRoot() == null
304                                 // Ignore the active & focused check for virtual displays,
305                                 // which don't get focused on launch.
306                                 || (displayId == Display.DEFAULT_DISPLAY
307                                     && (!window.isActive() || !window.isFocused()))) {
308                             // Attempt to close any system dialogs which can prevent the launched
309                             // activity from becoming visible, active, and focused.
310                             execShellCommand(uiAutomation,
311                                     AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
312                             return false;
313                         }
314 
315                         window.getBoundsInScreen(bounds);
316                         mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
317 
318                         // Stores the related information including event, location and window
319                         // as a timeout exception record.
320                         timeoutExceptionRecords.append(String.format("{Received event: %s \n"
321                                         + "Window location: %s \nA11y window: %s}\n",
322                                 event, Arrays.toString(location), window));
323 
324                         return (!bounds.isEmpty())
325                                 && Math.abs(bounds.left - location[0]) <= BOUNDS_PRECISION_PX
326                                 && Math.abs(bounds.top - location[1]) <= BOUNDS_PRECISION_PX;
327                     }, DEFAULT_TIMEOUT_MS);
328             assertNotNull(awaitedEvent);
329         } catch (TimeoutException timeout) {
330             throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n"
331                     + timeoutExceptionRecords);
332         }
333         instrumentation.waitForIdleSync();
334         return (T) mTempActivity;
335     }
336 
findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)337     public static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title,
338             List<AccessibilityWindowInfo> windows) {
339         AccessibilityWindowInfo returnValue = null;
340         if (windows != null && windows.size() > 0) {
341             for (int i = 0; i < windows.size(); i++) {
342                 final AccessibilityWindowInfo window = windows.get(i);
343                 if (TextUtils.equals(title, window.getTitle())) {
344                     returnValue = window;
345                 } else {
346                     window.recycle();
347                 }
348             }
349         }
350         return returnValue;
351     }
352 
353     private static abstract class ActivityLauncher {
launchActivity()354         abstract Activity launchActivity();
355     }
356 }
357