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