• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * Launches an activity on a specified display and waits for it to be onscreen.
97      *
98      * <p>This method creates an intent to launch the specified activity class on the given display
99      * ID. It ensures that the activity is launched in a new task and clears any existing task that
100      * may be associated with the activity. The method also handles shell permissions required for
101      * launching the activity.</p>
102      *
103      * @param instrumentation The instrumentation instance used to start the activity.
104      * @param uiAutomation The UiAutomation instance used to manage UI interactions.
105      * @param clazz The class of the activity to be launched.
106      * @param displayId The ID of the display on which the activity should be launched.
107      * @return An instance of the launched activity.
108      * @throws Exception If there is an error launching the activity or waiting for it.
109      */
launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz, int displayId)110     public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
111             Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz,
112             int displayId) throws Exception {
113         final ActivityOptions options = ActivityOptions.makeBasic();
114         options.setLaunchDisplayId(displayId);
115         final Intent intent = new Intent(instrumentation.getTargetContext(), clazz);
116         // Add clear task because this activity may on other display.
117         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
118 
119         ActivityLauncher activityLauncher = new ActivityLauncher() {
120             @Override
121             Activity launchActivity() {
122                 uiAutomation.adoptShellPermissionIdentity();
123                 try {
124                     return instrumentation.startActivitySync(intent, options.toBundle());
125                 } finally {
126                     uiAutomation.dropShellPermissionIdentity();
127                 }
128             }
129         };
130         return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation,
131                 uiAutomation, activityLauncher, displayId);
132     }
133 
getActivityTitle( Instrumentation instrumentation, Activity activity)134     public static CharSequence getActivityTitle(
135             Instrumentation instrumentation, Activity activity) {
136         final StringBuilder titleBuilder = new StringBuilder();
137         instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle()));
138         return titleBuilder;
139     }
140 
findWindowByTitle( UiAutomation uiAutomation, CharSequence title)141     public static AccessibilityWindowInfo findWindowByTitle(
142             UiAutomation uiAutomation, CharSequence title) {
143         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
144         return findWindowByTitleWithList(title, windows);
145     }
146 
findWindowByTitleAndDisplay( UiAutomation uiAutomation, CharSequence title, int displayId)147     public static AccessibilityWindowInfo findWindowByTitleAndDisplay(
148             UiAutomation uiAutomation, CharSequence title, int displayId) {
149         final SparseArray<List<AccessibilityWindowInfo>> allWindows =
150                 uiAutomation.getWindowsOnAllDisplays();
151         final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId);
152         return findWindowByTitleWithList(title, windowsOfDisplay);
153     }
154 
homeScreenOrBust(Context context, UiAutomation uiAutomation)155     public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) {
156         wakeUpOrBust(context, uiAutomation);
157         if (context.getPackageManager().isInstantApp()) return;
158         if (isHomeScreenShowing(context, uiAutomation)) return;
159         final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
160         final int enabledFlags = serviceInfo.flags;
161         // Make sure we could query windows.
162         serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
163         uiAutomation.setServiceInfo(serviceInfo);
164         try {
165             KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
166             if (keyguardManager != null) {
167                 TestUtils.waitUntil("Screen is unlocked",
168                         (int) DEFAULT_TIMEOUT_MS / 1000,
169                         () -> {
170                             if (!keyguardManager.isKeyguardLocked()) {
171                                 return true;
172                             }
173                             execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_MENU);
174                             return false;
175                         });
176             }
177             execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND);
178             execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
179             execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK);
180             TestUtils.waitUntil("Home screen is showing",
181                     (int) DEFAULT_TIMEOUT_MS / 1000,
182                     () -> {
183                         if (isHomeScreenShowing(context, uiAutomation)) {
184                             return true;
185                         }
186                         // Attempt to close any newly-appeared system dialogs which can prevent the
187                         // home screen activity from becoming visible, active, and focused.
188                         execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
189                         execShellCommand(uiAutomation, INPUT_KEYEVENT_KEYCODE_BACK);
190                         execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND);
191                         return false;
192                     });
193         } catch (Exception error) {
194             Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list");
195             final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
196             if (windows == null) {
197                 Log.e(LOG_TAG, "Window list is null");
198             } else if (windows.isEmpty()) {
199                 Log.e(LOG_TAG, "Window list is empty");
200             } else {
201                 for (AccessibilityWindowInfo window : windows) {
202                     Log.e(LOG_TAG, window.toString());
203                 }
204             }
205 
206             fail("Unable to reach home screen");
207         } finally {
208             serviceInfo.flags = enabledFlags;
209             uiAutomation.setServiceInfo(serviceInfo);
210         }
211     }
212 
supportsMultiDisplay(Context context)213     public static boolean supportsMultiDisplay(Context context) {
214         return context.getPackageManager().hasSystemFeature(
215                 FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS);
216     }
217 
isHomeScreenShowing(Context context, UiAutomation uiAutomation)218     public static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) {
219         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
220         final PackageManager packageManager = context.getPackageManager();
221         final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
222                 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
223                 PackageManager.MATCH_DEFAULT_ONLY);
224         final boolean isAuto = isAutomotive(context);
225 
226         // Look for an active focused window with a package name that matches
227         // the default home screen.
228         for (AccessibilityWindowInfo window : windows) {
229             if (!isAuto) {
230                 // Auto does not set its home screen app as active+focused, so only non-auto
231                 // devices enforce that the home screen is active+focused.
232                 if (!window.isActive() || !window.isFocused()) {
233                     continue;
234                 }
235             }
236             final AccessibilityNodeInfo root = window.getRoot();
237             if (root != null) {
238                 final CharSequence packageName = root.getPackageName();
239                 if (packageName != null) {
240                     for (ResolveInfo resolveInfo : resolveInfos) {
241                         if ((resolveInfo.activityInfo != null)
242                                 && packageName.equals(resolveInfo.activityInfo.packageName)) {
243                             return true;
244                         }
245                     }
246                 }
247             }
248         }
249         // List unexpected package names of default home screen that invoking ResolverActivity
250         final CharSequence homePackageNames = resolveInfos.stream()
251                 .map(r -> r.activityInfo).filter(Objects::nonNull)
252                 .map(a -> a.packageName).collect(Collectors.joining(", "));
253         Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames);
254         return false;
255     }
256 
wakeUpOrBust(Context context, UiAutomation uiAutomation)257     private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) {
258         final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS;
259         final PowerManager powerManager = context.getSystemService(PowerManager.class);
260         do {
261             if (powerManager.isInteractive()) {
262                 Log.d(LOG_TAG, "Device is interactive");
263                 return;
264             }
265 
266             Log.d(LOG_TAG, "Sending wakeup keycode");
267             final long eventTime = SystemClock.uptimeMillis();
268             uiAutomation.injectInputEvent(
269                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
270                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
271                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
272                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
273             uiAutomation.injectInputEvent(
274                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
275                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
276                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
277                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
278             try {
279                 Thread.sleep(50);
280             } catch (InterruptedException e) {
281             }
282         } while (SystemClock.uptimeMillis() < deadlineUptimeMillis);
283         fail("Unable to wake up screen");
284     }
285 
launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityLauncher activityLauncher, int displayId)286     private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
287             Instrumentation instrumentation, UiAutomation uiAutomation,
288             ActivityLauncher activityLauncher, int displayId) throws Exception {
289         final int[] location = new int[2];
290         final StringBuilder activityPackage = new StringBuilder();
291         final Rect bounds = new Rect();
292         final StringBuilder activityTitle = new StringBuilder();
293         final StringBuilder timeoutExceptionRecords = new StringBuilder();
294         // Make sure we get window events, so we'll know when the window appears
295         AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
296         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
297         uiAutomation.setServiceInfo(info);
298         // There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only
299         // checking the home screen for default display.
300         if (displayId == Display.DEFAULT_DISPLAY) {
301             homeScreenOrBust(instrumentation.getContext(), uiAutomation);
302         }
303 
304         try {
305             final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
306                     () -> {
307                         mTempActivity = activityLauncher.launchActivity();
308                         instrumentation.runOnMainSync(() -> {
309                             mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
310                             activityPackage.append(mTempActivity.getPackageName());
311                         });
312                         instrumentation.waitForIdleSync();
313                         activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
314                     },
315                     (event) -> {
316                         final AccessibilityWindowInfo window =
317                                 findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId);
318                         if (window == null || window.getRoot() == null
319                                 // Ignore the active & focused check for virtual displays,
320                                 // which don't get focused on launch.
321                                 || (displayId == Display.DEFAULT_DISPLAY
322                                     && (!window.isActive() || !window.isFocused()))) {
323                             // Attempt to close any system dialogs which can prevent the launched
324                             // activity from becoming visible, active, and focused.
325                             execShellCommand(uiAutomation,
326                                     AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
327                             return false;
328                         }
329 
330                         window.getBoundsInScreen(bounds);
331                         mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
332 
333                         // Stores the related information including event, location and window
334                         // as a timeout exception record.
335                         timeoutExceptionRecords.append(String.format("{Received event: %s \n"
336                                         + "Window location: %s \nA11y window: %s}\n",
337                                 event, Arrays.toString(location), window));
338 
339                         return (!bounds.isEmpty())
340                                 && Math.abs(bounds.left - location[0]) <= BOUNDS_PRECISION_PX
341                                 && Math.abs(bounds.top - location[1]) <= BOUNDS_PRECISION_PX;
342                     }, DEFAULT_TIMEOUT_MS);
343             assertNotNull(awaitedEvent);
344         } catch (TimeoutException timeout) {
345             throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n"
346                     + timeoutExceptionRecords);
347         }
348         instrumentation.waitForIdleSync();
349         return (T) mTempActivity;
350     }
351 
findWindowByTitleWithList(CharSequence title, List<AccessibilityWindowInfo> windows)352     public static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title,
353             List<AccessibilityWindowInfo> windows) {
354         AccessibilityWindowInfo returnValue = null;
355         if (windows != null && windows.size() > 0) {
356             for (int i = 0; i < windows.size(); i++) {
357                 final AccessibilityWindowInfo window = windows.get(i);
358                 if (TextUtils.equals(title, window.getTitle())) {
359                     returnValue = window;
360                 } else {
361                     window.recycle();
362                 }
363             }
364         }
365         return returnValue;
366     }
367 
368     private static abstract class ActivityLauncher {
launchActivity()369         abstract Activity launchActivity();
370     }
371 }
372