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