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