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.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
18 
19 import static org.junit.Assert.assertNotNull;
20 import static org.junit.Assert.fail;
21 
22 import android.accessibilityservice.AccessibilityService;
23 import android.accessibilityservice.AccessibilityServiceInfo;
24 import android.app.Activity;
25 import android.app.Instrumentation;
26 import android.app.UiAutomation;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.graphics.Rect;
32 import android.os.PowerManager;
33 import android.os.SystemClock;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.InputDevice;
37 import android.view.KeyCharacterMap;
38 import android.view.KeyEvent;
39 import android.view.accessibility.AccessibilityEvent;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 import android.view.accessibility.AccessibilityWindowInfo;
42 
43 import androidx.test.rule.ActivityTestRule;
44 
45 import com.android.compatibility.common.util.TestUtils;
46 
47 import java.util.List;
48 import java.util.Objects;
49 import java.util.function.BooleanSupplier;
50 import java.util.stream.Collectors;
51 
52 /**
53  * Utilities useful when launching an activity to make sure it's all the way on the screen
54  * before we start testing it.
55  */
56 public class ActivityLaunchUtils {
57     private static final String LOG_TAG = "ActivityLaunchUtils";
58 
59     // Using a static variable so it can be used in lambdas. Not preserving state in it.
60     private static Activity mTempActivity;
61 
launchActivityAndWaitForItToBeOnscreen( Instrumentation instrumentation, UiAutomation uiAutomation, ActivityTestRule<T> rule)62     public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen(
63             Instrumentation instrumentation, UiAutomation uiAutomation,
64             ActivityTestRule<T> rule) throws Exception {
65         final int[] location = new int[2];
66         final StringBuilder activityPackage = new StringBuilder();
67         final Rect bounds = new Rect();
68         final StringBuilder activityTitle = new StringBuilder();
69         // Make sure we get window events, so we'll know when the window appears
70         AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
71         info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
72         uiAutomation.setServiceInfo(info);
73         homeScreenOrBust(instrumentation.getContext(), uiAutomation);
74         final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
75                 () -> {
76                     mTempActivity = rule.launchActivity(null);
77                     final StringBuilder builder = new StringBuilder();
78                     instrumentation.runOnMainSync(() -> {
79                         mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
80                         activityPackage.append(mTempActivity.getPackageName());
81                     });
82                     instrumentation.waitForIdleSync();
83                     activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
84                 },
85                 (event) -> {
86                     final AccessibilityWindowInfo window =
87                             findWindowByTitle(uiAutomation, activityTitle);
88                     if (window == null) return false;
89                     window.getBoundsInScreen(bounds);
90                     mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
91                     if (bounds.isEmpty()) {
92                         return false;
93                     }
94                     return (!bounds.isEmpty())
95                             && (bounds.left == location[0]) && (bounds.top == location[1]);
96                 }, DEFAULT_TIMEOUT_MS);
97         assertNotNull(awaitedEvent);
98         return (T) mTempActivity;
99     }
100 
getActivityTitle( Instrumentation instrumentation, Activity activity)101     public static CharSequence getActivityTitle(
102             Instrumentation instrumentation, Activity activity) {
103         final StringBuilder titleBuilder = new StringBuilder();
104         instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle()));
105         return titleBuilder;
106     }
107 
findWindowByTitle( UiAutomation uiAutomation, CharSequence title)108     public static AccessibilityWindowInfo findWindowByTitle(
109             UiAutomation uiAutomation, CharSequence title) {
110         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
111         AccessibilityWindowInfo returnValue = null;
112         for (int i = 0; i < windows.size(); i++) {
113             final AccessibilityWindowInfo window = windows.get(i);
114             if (TextUtils.equals(title, window.getTitle())) {
115                 returnValue = window;
116             } else {
117                 window.recycle();
118             }
119         }
120         return returnValue;
121     }
122 
homeScreenOrBust(Context context, UiAutomation uiAutomation)123     public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) {
124         wakeUpOrBust(context, uiAutomation);
125         if (context.getPackageManager().isInstantApp()) return;
126         if (isHomeScreenShowing(context, uiAutomation)) return;
127         try {
128             executeAndWaitOn(
129                     uiAutomation,
130                     () -> uiAutomation.performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME),
131                     () -> isHomeScreenShowing(context, uiAutomation),
132                     DEFAULT_TIMEOUT_MS,
133                     "home screen");
134         } catch (AssertionError error) {
135             Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list");
136             final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
137             if (windows == null) {
138                 Log.e(LOG_TAG, "Window list is null");
139             } else if (windows.isEmpty()) {
140                 Log.e(LOG_TAG, "Window list is empty");
141             } else {
142                 for (AccessibilityWindowInfo window : windows) {
143                     Log.e(LOG_TAG, window.toString());
144                 }
145             }
146 
147             fail("Unable to reach home screen");
148         }
149     }
150 
isHomeScreenShowing(Context context, UiAutomation uiAutomation)151     private static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) {
152         final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
153         final PackageManager packageManager = context.getPackageManager();
154         final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
155                 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
156                 PackageManager.MATCH_DEFAULT_ONLY);
157 
158         // Look for a window with a package name that matches the default home screen
159         for (AccessibilityWindowInfo window : windows) {
160             final AccessibilityNodeInfo root = window.getRoot();
161             if (root != null) {
162                 final CharSequence packageName = root.getPackageName();
163                 if (packageName != null) {
164                     for (ResolveInfo resolveInfo : resolveInfos) {
165                         if ((resolveInfo.activityInfo != null)
166                                 && packageName.equals(resolveInfo.activityInfo.packageName)) {
167                             return true;
168                         }
169                     }
170                 }
171             }
172         }
173         // List unexpected package names of default home screen that invoking ResolverActivity
174         final CharSequence homePackageNames = resolveInfos.stream()
175                 .map(r -> r.activityInfo).filter(Objects::nonNull)
176                 .map(a -> a.packageName).collect(Collectors.joining(", "));
177         Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames);
178         return false;
179     }
180 
wakeUpOrBust(Context context, UiAutomation uiAutomation)181     private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) {
182         final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS;
183         final PowerManager powerManager = context.getSystemService(PowerManager.class);
184         do {
185             if (powerManager.isInteractive()) {
186                 Log.d(LOG_TAG, "Device is interactive");
187                 return;
188             }
189 
190             Log.d(LOG_TAG, "Sending wakeup keycode");
191             final long eventTime = SystemClock.uptimeMillis();
192             uiAutomation.injectInputEvent(
193                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
194                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
195                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
196                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
197             uiAutomation.injectInputEvent(
198                     new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
199                             KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
200                             KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
201                             InputDevice.SOURCE_KEYBOARD), true /* sync */);
202             try {
203                 Thread.sleep(50);
204             } catch (InterruptedException e) {}
205         } while (SystemClock.uptimeMillis() < deadlineUptimeMillis);
206         fail("Unable to wake up screen");
207     }
208 
209     /**
210      * Executes a command and waits for a specified condition up to a given wait timeout. It checks
211      * condition result each time when events delivered, and throws exception if the condition
212      * result is not {@code true} within the given timeout.
213      */
executeAndWaitOn(UiAutomation uiAutomation, Runnable command, BooleanSupplier condition, long timeoutMillis, String conditionName)214     private static void executeAndWaitOn(UiAutomation uiAutomation, Runnable command,
215             BooleanSupplier condition, long timeoutMillis, String conditionName) {
216         final Object waitObject = new Object();
217         final long executionStartTimeMillis = SystemClock.uptimeMillis();
218         try {
219             uiAutomation.setOnAccessibilityEventListener((event) -> {
220                 if (event.getEventTime() < executionStartTimeMillis) {
221                     return;
222                 }
223                 synchronized (waitObject) {
224                     waitObject.notifyAll();
225                 }
226             });
227             command.run();
228             TestUtils.waitOn(waitObject, condition, timeoutMillis, conditionName);
229         } finally {
230             uiAutomation.setOnAccessibilityEventListener(null);
231         }
232     }
233 }
234