blob: 4ecc13fdd810ef194414bf2c7f1e224c0d675c0a [file] [log] [blame]
/**
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.accessibilityservice.cts.utils;
import static android.accessibility.cts.common.ShellCommandBuilder.execShellCommand;
import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.os.PowerManager;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import androidx.test.rule.ActivityTestRule;
import com.android.compatibility.common.util.TestUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
/**
* Utilities useful when launching an activity to make sure it's all the way on the screen
* before we start testing it.
*/
public class ActivityLaunchUtils {
private static final String LOG_TAG = "ActivityLaunchUtils";
private static final String AM_START_HOME_ACTIVITY_COMMAND =
"am start -a android.intent.action.MAIN -c android.intent.category.HOME";
public static final String AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND =
"am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
// Using a static variable so it can be used in lambdas. Not preserving state in it.
private static Activity mTempActivity;
public static <T extends Activity> T launchActivityAndWaitForItToBeOnscreen(
Instrumentation instrumentation, UiAutomation uiAutomation,
ActivityTestRule<T> rule) throws Exception {
ActivityLauncher activityLauncher = new ActivityLauncher() {
@Override
Activity launchActivity() {
return rule.launchActivity(null);
}
};
return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation,
uiAutomation, activityLauncher, Display.DEFAULT_DISPLAY);
}
/**
* If this activity would be launched at virtual display, please finishes this activity before
* this test ended. Otherwise it will be displayed on default display and impacts the next test.
*/
public static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
Instrumentation instrumentation, UiAutomation uiAutomation, Class<T> clazz,
int displayId) throws Exception {
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(displayId);
final Intent intent = new Intent(instrumentation.getTargetContext(), clazz);
// Add clear task because this activity may on other display.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityLauncher activityLauncher = new ActivityLauncher() {
@Override
Activity launchActivity() {
uiAutomation.adoptShellPermissionIdentity();
try {
return instrumentation.startActivitySync(intent, options.toBundle());
} finally {
uiAutomation.dropShellPermissionIdentity();
}
}
};
return launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(instrumentation,
uiAutomation, activityLauncher, displayId);
}
public static CharSequence getActivityTitle(
Instrumentation instrumentation, Activity activity) {
final StringBuilder titleBuilder = new StringBuilder();
instrumentation.runOnMainSync(() -> titleBuilder.append(activity.getTitle()));
return titleBuilder;
}
public static AccessibilityWindowInfo findWindowByTitle(
UiAutomation uiAutomation, CharSequence title) {
final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
return findWindowByTitleWithList(title, windows);
}
public static AccessibilityWindowInfo findWindowByTitleAndDisplay(
UiAutomation uiAutomation, CharSequence title, int displayId) {
final SparseArray<List<AccessibilityWindowInfo>> allWindows =
uiAutomation.getWindowsOnAllDisplays();
final List<AccessibilityWindowInfo> windowsOfDisplay = allWindows.get(displayId);
return findWindowByTitleWithList(title, windowsOfDisplay);
}
public static void homeScreenOrBust(Context context, UiAutomation uiAutomation) {
wakeUpOrBust(context, uiAutomation);
if (context.getPackageManager().isInstantApp()) return;
if (isHomeScreenShowing(context, uiAutomation)) return;
final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
final int enabledFlags = serviceInfo.flags;
// Make sure we could query windows.
serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
uiAutomation.setServiceInfo(serviceInfo);
try {
executeAndWaitOn(
uiAutomation,
() -> {
execShellCommand(uiAutomation, AM_START_HOME_ACTIVITY_COMMAND);
execShellCommand(uiAutomation, AM_BROADCAST_CLOSE_SYSTEM_DIALOG_COMMAND);
},
() -> isHomeScreenShowing(context, uiAutomation),
DEFAULT_TIMEOUT_MS,
"home screen");
} catch (AssertionError error) {
Log.e(LOG_TAG, "Timed out looking for home screen. Dumping window list");
final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
if (windows == null) {
Log.e(LOG_TAG, "Window list is null");
} else if (windows.isEmpty()) {
Log.e(LOG_TAG, "Window list is empty");
} else {
for (AccessibilityWindowInfo window : windows) {
Log.e(LOG_TAG, window.toString());
}
}
fail("Unable to reach home screen");
} finally {
serviceInfo.flags = enabledFlags;
uiAutomation.setServiceInfo(serviceInfo);
}
}
public static boolean supportsMultiDisplay(Context context) {
return context.getPackageManager().hasSystemFeature(
FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS);
}
private static boolean isHomeScreenShowing(Context context, UiAutomation uiAutomation) {
final List<AccessibilityWindowInfo> windows = uiAutomation.getWindows();
final PackageManager packageManager = context.getPackageManager();
final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
PackageManager.MATCH_DEFAULT_ONLY);
// Look for a window with a package name that matches the default home screen
for (AccessibilityWindowInfo window : windows) {
final AccessibilityNodeInfo root = window.getRoot();
if (root != null) {
final CharSequence packageName = root.getPackageName();
if (packageName != null) {
for (ResolveInfo resolveInfo : resolveInfos) {
if ((resolveInfo.activityInfo != null)
&& packageName.equals(resolveInfo.activityInfo.packageName)) {
return true;
}
}
}
}
}
// List unexpected package names of default home screen that invoking ResolverActivity
final CharSequence homePackageNames = resolveInfos.stream()
.map(r -> r.activityInfo).filter(Objects::nonNull)
.map(a -> a.packageName).collect(Collectors.joining(", "));
Log.v(LOG_TAG, "No window matched with package names of home screen: " + homePackageNames);
return false;
}
private static void wakeUpOrBust(Context context, UiAutomation uiAutomation) {
final long deadlineUptimeMillis = SystemClock.uptimeMillis() + DEFAULT_TIMEOUT_MS;
final PowerManager powerManager = context.getSystemService(PowerManager.class);
do {
if (powerManager.isInteractive()) {
Log.d(LOG_TAG, "Device is interactive");
return;
}
Log.d(LOG_TAG, "Sending wakeup keycode");
final long eventTime = SystemClock.uptimeMillis();
uiAutomation.injectInputEvent(
new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
InputDevice.SOURCE_KEYBOARD), true /* sync */);
uiAutomation.injectInputEvent(
new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_WAKEUP, 0 /* repeat */, 0 /* metastate */,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, 0 /* flags */,
InputDevice.SOURCE_KEYBOARD), true /* sync */);
try {
Thread.sleep(50);
} catch (InterruptedException e) {}
} while (SystemClock.uptimeMillis() < deadlineUptimeMillis);
fail("Unable to wake up screen");
}
/**
* Executes a command and waits for a specified condition up to a given wait timeout. It checks
* condition result each time when events delivered, and throws exception if the condition
* result is not {@code true} within the given timeout.
*/
private static void executeAndWaitOn(UiAutomation uiAutomation, Runnable command,
BooleanSupplier condition, long timeoutMillis, String conditionName) {
final Object waitObject = new Object();
final long executionStartTimeMillis = SystemClock.uptimeMillis();
try {
uiAutomation.setOnAccessibilityEventListener((event) -> {
if (event.getEventTime() < executionStartTimeMillis) {
return;
}
synchronized (waitObject) {
waitObject.notifyAll();
}
});
command.run();
TestUtils.waitOn(waitObject, condition, timeoutMillis, conditionName);
} finally {
uiAutomation.setOnAccessibilityEventListener(null);
}
}
private static <T extends Activity> T launchActivityOnSpecifiedDisplayAndWaitForItToBeOnscreen(
Instrumentation instrumentation, UiAutomation uiAutomation,
ActivityLauncher activityLauncher, int displayId) throws Exception {
final int[] location = new int[2];
final StringBuilder activityPackage = new StringBuilder();
final Rect bounds = new Rect();
final StringBuilder activityTitle = new StringBuilder();
final StringBuilder timeoutExceptionRecords = new StringBuilder();
// Make sure we get window events, so we'll know when the window appears
AccessibilityServiceInfo info = uiAutomation.getServiceInfo();
info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
uiAutomation.setServiceInfo(info);
// There is no any window on virtual display even doing GLOBAL_ACTION_HOME, so only
// checking the home screen for default display.
if (displayId == Display.DEFAULT_DISPLAY) {
homeScreenOrBust(instrumentation.getContext(), uiAutomation);
}
try {
final AccessibilityEvent awaitedEvent = uiAutomation.executeAndWaitForEvent(
() -> {
mTempActivity = activityLauncher.launchActivity();
instrumentation.runOnMainSync(() -> {
mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
activityPackage.append(mTempActivity.getPackageName());
});
instrumentation.waitForIdleSync();
activityTitle.append(getActivityTitle(instrumentation, mTempActivity));
},
(event) -> {
final AccessibilityWindowInfo window =
findWindowByTitleAndDisplay(uiAutomation, activityTitle, displayId);
if (window == null) return false;
if (window.getRoot() == null) return false;
window.getBoundsInScreen(bounds);
mTempActivity.getWindow().getDecorView().getLocationOnScreen(location);
// Stores the related information including event, location and window
// as a timeout exception record.
timeoutExceptionRecords.append(String.format("{Received event: %s \n"
+ "Window location: %s \nA11y window: %s}\n",
event, Arrays.toString(location), window));
return (!bounds.isEmpty())
&& (bounds.left == location[0]) && (bounds.top == location[1]);
}, DEFAULT_TIMEOUT_MS);
assertNotNull(awaitedEvent);
} catch (TimeoutException timeout) {
throw new TimeoutException(timeout.getMessage() + "\n\nTimeout exception records : \n"
+ timeoutExceptionRecords);
}
return (T) mTempActivity;
}
private static AccessibilityWindowInfo findWindowByTitleWithList(CharSequence title,
List<AccessibilityWindowInfo> windows) {
AccessibilityWindowInfo returnValue = null;
if (windows != null && windows.size() > 0) {
for (int i = 0; i < windows.size(); i++) {
final AccessibilityWindowInfo window = windows.get(i);
if (TextUtils.equals(title, window.getTitle())) {
returnValue = window;
} else {
window.recycle();
}
}
}
return returnValue;
}
private static abstract class ActivityLauncher {
abstract Activity launchActivity();
}
}