[go: nahoru, domu]

blob: a70fb2efb1209bb2fa1a7da01cb32d114ca370b8 [file] [log] [blame]
/*
* Copyright (C) 2012 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 androidx.test.uiautomator;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.SuppressLint;
import android.app.Instrumentation;
import android.app.Service;
import android.app.UiAutomation;
import android.app.UiAutomation.AccessibilityEventFilter;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import androidx.annotation.Discouraged;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RequiresApi;
import androidx.test.uiautomator.util.Traces;
import androidx.test.uiautomator.util.Traces.Section;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;
/**
* UiDevice provides access to state information about the device.
* You can also use this class to simulate user actions on the device,
* such as pressing the d-pad or pressing the Home and Menu buttons.
*/
public class UiDevice implements Searchable {
static final String TAG = UiDevice.class.getSimpleName();
private static final int MAX_UIAUTOMATION_RETRY = 3;
private static final int UIAUTOMATION_RETRY_INTERVAL = 500; // ms
// Workaround for stale accessibility cache issues: duration after which the a11y service flags
// should be reset (when fetching a UiAutomation instance) to periodically invalidate the cache.
private static final long SERVICE_FLAGS_TIMEOUT = 2_000; // ms
// Use a short timeout after HOME or BACK key presses, as no events might be generated if
// already on the home page or if there is nothing to go back to.
private static final long KEY_PRESS_EVENT_TIMEOUT = 1_000; // ms
private static final long ROTATION_TIMEOUT = 2_000; // ms
// Singleton instance.
private static UiDevice sInstance;
private final Instrumentation mInstrumentation;
private final QueryController mQueryController;
private final InteractionController mInteractionController;
private final DisplayManager mDisplayManager;
private final WaitMixin<UiDevice> mWaitMixin = new WaitMixin<>(this);
// Track accessibility service flags to determine when the underlying connection has changed.
private int mCachedServiceFlags = -1;
private long mLastServiceFlagsTime = -1;
private boolean mCompressed = false;
// Lazily created UI context per display, used to access UI components/configurations.
private final Map<Integer, Context> mUiContexts = new HashMap<>();
// Track registered UiWatchers, and whether currently in a UiWatcher execution.
private final Map<String, UiWatcher> mWatchers = new LinkedHashMap<>();
private final List<String> mWatchersTriggers = new ArrayList<>();
private boolean mInWatcherContext = false;
/** Private constructor. Clients should use {@link UiDevice#getInstance(Instrumentation)}. */
UiDevice(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mQueryController = new QueryController(this);
mInteractionController = new InteractionController(this);
mDisplayManager = (DisplayManager) instrumentation.getContext().getSystemService(
Service.DISPLAY_SERVICE);
}
boolean isInWatcherContext() {
return mInWatcherContext;
}
/**
* Returns a UiObject which represents a view that matches the specified selector criteria.
*
* @param selector
* @return UiObject object
*/
@NonNull
public UiObject findObject(@NonNull UiSelector selector) {
return new UiObject(this, selector);
}
/** Returns whether there is a match for the given {@code selector} criteria. */
@Override
public boolean hasObject(@NonNull BySelector selector) {
Log.d(TAG, String.format("Searching for node with selector: %s.", selector));
AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
if (node != null) {
node.recycle();
return true;
}
return false;
}
/**
* Returns the first object to match the {@code selector} criteria,
* or null if no matching objects are found.
*/
@Override
@SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
public UiObject2 findObject(@NonNull BySelector selector) {
Log.d(TAG, String.format("Retrieving node with selector: %s.", selector));
AccessibilityNodeInfo node = ByMatcher.findMatch(this, selector, getWindowRoots());
if (node == null) {
Log.d(TAG, String.format("Node not found with selector: %s.", selector));
return null;
}
return UiObject2.create(this, selector, node);
}
/** Returns all objects that match the {@code selector} criteria. */
@Override
@NonNull
public List<UiObject2> findObjects(@NonNull BySelector selector) {
Log.d(TAG, String.format("Retrieving nodes with selector: %s.", selector));
List<UiObject2> ret = new ArrayList<>();
for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
UiObject2 object = UiObject2.create(this, selector, node);
if (object != null) {
ret.add(object);
}
}
return ret;
}
/**
* Waits for given the {@code condition} to be met.
*
* @param condition The {@link SearchCondition} to evaluate.
* @param timeout Maximum amount of time to wait in milliseconds.
* @return The final result returned by the {@code condition}, or null if the {@code condition}
* was not met before the {@code timeout}.
*/
public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
return wait((Condition<? super UiDevice, U>) condition, timeout);
}
/**
* Waits for given the {@code condition} to be met.
*
* @param condition The {@link Condition} to evaluate.
* @param timeout Maximum amount of time to wait in milliseconds.
* @return The final result returned by the {@code condition}, or null if the {@code condition}
* was not met before the {@code timeout}.
*/
public <U> U wait(@NonNull Condition<? super UiDevice, U> condition, long timeout) {
try (Section ignored = Traces.trace("UiDevice#wait")) {
Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
return mWaitMixin.wait(condition, timeout);
}
}
/**
* Performs the provided {@code action} and waits for the {@code condition} to be met.
*
* @param action The {@link Runnable} action to perform.
* @param condition The {@link EventCondition} to evaluate.
* @param timeout Maximum amount of time to wait in milliseconds.
* @return The final result returned by the condition.
*/
public <U> U performActionAndWait(@NonNull Runnable action,
@NonNull EventCondition<U> condition, long timeout) {
try (Section ignored = Traces.trace("UiDevice#performActionAndWait")) {
AccessibilityEvent event = null;
Log.d(TAG, String.format("Performing action %s and waiting %dms for %s.", action,
timeout, condition));
try {
event = getUiAutomation().executeAndWaitForEvent(
action, condition, timeout);
} catch (TimeoutException e) {
// Ignore
Log.w(TAG, String.format("Timed out waiting %dms on the condition.", timeout), e);
}
if (event != null) {
event.recycle();
}
return condition.getResult();
}
}
/**
* Enables or disables layout hierarchy compression.
*
* If compression is enabled, the layout hierarchy derived from the Acessibility
* framework will only contain nodes that are important for uiautomator
* testing. Any unnecessary surrounding layout nodes that make viewing
* and searching the hierarchy inefficient are removed.
*
* @param compressed true to enable compression; else, false to disable
* @deprecated Typo in function name, should use {@link #setCompressedLayoutHierarchy(boolean)}
* instead.
*/
@Deprecated
public void setCompressedLayoutHeirarchy(boolean compressed) {
this.setCompressedLayoutHierarchy(compressed);
}
/**
* Enables or disables layout hierarchy compression.
*
* If compression is enabled, the layout hierarchy derived from the Accessibility
* framework will only contain nodes that are important for uiautomator
* testing. Any unnecessary surrounding layout nodes that make viewing
* and searching the hierarchy inefficient are removed.
*
* @param compressed true to enable compression; else, false to disable
*/
public void setCompressedLayoutHierarchy(boolean compressed) {
mCompressed = compressed;
mCachedServiceFlags = -1; // Reset cached accessibility service flags to force an update.
}
/**
* Retrieves a singleton instance of UiDevice
*
* @deprecated Should use {@link #getInstance(Instrumentation)} instead. This version hides
* UiDevice's dependency on having an Instrumentation reference and is prone to misuse.
* @return UiDevice instance
*/
@Deprecated
@NonNull
public static UiDevice getInstance() {
if (sInstance == null) {
throw new IllegalStateException("UiDevice singleton not initialized");
}
return sInstance;
}
/**
* Retrieves a singleton instance of UiDevice. A new instance will be created if
* instrumentation is also new.
*
* @return UiDevice instance
*/
@NonNull
public static UiDevice getInstance(@NonNull Instrumentation instrumentation) {
if (sInstance == null || !instrumentation.equals(sInstance.mInstrumentation)) {
Log.i(TAG, String.format("Creating a new instance, old instance exists: %b",
(sInstance != null)));
sInstance = new UiDevice(instrumentation);
}
return sInstance;
}
/**
* Returns the default display size in dp (device-independent pixel).
* <p>The returned display size is adjusted per screen rotation. Also this will return the
* actual size of the screen, rather than adjusted per system decorations (like status bar).
*
* @see DisplayMetrics#density
* @return a Point containing the display size in dp
*/
@NonNull
public Point getDisplaySizeDp() {
Point p = getDisplaySize(Display.DEFAULT_DISPLAY);
Context context = getUiContext(Display.DEFAULT_DISPLAY);
int densityDpi = context.getResources().getConfiguration().densityDpi;
float density = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
return new Point(Math.round(p.x / density), Math.round(p.y / density));
}
/**
* Retrieves the product name of the device.
*
* This method provides information on what type of device the test is running on. This value is
* the same as returned by invoking #adb shell getprop ro.product.name.
*
* @return product name of the device
*/
@NonNull
public String getProductName() {
return Build.PRODUCT;
}
/**
* Retrieves the text from the last UI traversal event received.
*
* You can use this method to read the contents in a WebView container
* because the accessibility framework fires events
* as each text is highlighted. You can write a test to perform
* directional arrow presses to focus on different elements inside a WebView,
* and call this method to get the text from each traversed element.
* If you are testing a view container that can return a reference to a
* Document Object Model (DOM) object, your test should use the view's
* DOM instead.
*
* @return text of the last traversal event, else return an empty string
*/
@SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
public String getLastTraversedText() {
return getQueryController().getLastTraversedText();
}
/**
* Clears the text from the last UI traversal event.
* See {@link #getLastTraversedText()}.
*/
public void clearLastTraversedText() {
Log.d(TAG, "Clearing last traversed text.");
getQueryController().clearLastTraversedText();
}
/**
* Simulates a short press on the MENU button.
* @return true if successful, else return false
*/
public boolean pressMenu() {
waitForIdle();
Log.d(TAG, "Pressing menu button.");
return getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the BACK button.
* @return true if successful, else return false
*/
public boolean pressBack() {
waitForIdle();
Log.d(TAG, "Pressing back button.");
return getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the HOME button.
* @return true if successful, else return false
*/
public boolean pressHome() {
waitForIdle();
Log.d(TAG, "Pressing home button.");
return getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the SEARCH button.
* @return true if successful, else return false
*/
public boolean pressSearch() {
return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
}
/**
* Simulates a short press on the CENTER button.
* @return true if successful, else return false
*/
public boolean pressDPadCenter() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
}
/**
* Simulates a short press on the DOWN button.
* @return true if successful, else return false
*/
public boolean pressDPadDown() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
}
/**
* Simulates a short press on the UP button.
* @return true if successful, else return false
*/
public boolean pressDPadUp() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
}
/**
* Simulates a short press on the LEFT button.
* @return true if successful, else return false
*/
public boolean pressDPadLeft() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
}
/**
* Simulates a short press on the RIGHT button.
* @return true if successful, else return false
*/
public boolean pressDPadRight() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
}
/**
* Simulates a short press on the DELETE key.
* @return true if successful, else return false
*/
public boolean pressDelete() {
return pressKeyCode(KeyEvent.KEYCODE_DEL);
}
/**
* Simulates a short press on the ENTER key.
* @return true if successful, else return false
*/
public boolean pressEnter() {
return pressKeyCode(KeyEvent.KEYCODE_ENTER);
}
/**
* Simulates a short press using a key code.
*
* See {@link KeyEvent}
* @return true if successful, else return false
*/
public boolean pressKeyCode(int keyCode) {
return pressKeyCode(keyCode, 0);
}
/**
* Simulates a short press using a key code.
*
* See {@link KeyEvent}.
* @param keyCode the key code of the event.
* @param metaState an integer in which each bit set to 1 represents a pressed meta key
* @return true if successful, else return false
*/
public boolean pressKeyCode(int keyCode, int metaState) {
return pressKeyCodes(new int[]{keyCode}, metaState);
}
/**
* Presses one or more keys.
* <br/>
* For example, you can simulate taking a screenshot on the device by pressing both the
* power and volume down keys.
* <pre>{@code pressKeyCodes(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_VOLUME_DOWN})}
* </pre>
*
* @see KeyEvent
* @param keyCodes array of key codes.
* @return true if successful, else return false
*/
public boolean pressKeyCodes(@NonNull int[] keyCodes) {
return pressKeyCodes(keyCodes, 0);
}
/**
* Presses one or more keys.
* <br/>
* For example, you can simulate taking a screenshot on the device by pressing both the
* power and volume down keys.
* <pre>{@code pressKeyCodes(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_VOLUME_DOWN})}
* </pre>
*
* @see KeyEvent
* @param keyCodes array of key codes.
* @param metaState an integer in which each bit set to 1 represents a pressed meta key
* @return true if successful, else return false
*/
public boolean pressKeyCodes(@NonNull int[] keyCodes, int metaState) {
waitForIdle();
Log.d(TAG, String.format("Pressing keycodes %s with modifier %d.",
Arrays.toString(keyCodes),
metaState));
return getInteractionController().sendKeys(keyCodes, metaState);
}
/**
* Simulates a short press on the Recent Apps button.
*
* @return true if successful, else return false
* @throws RemoteException never
*/
public boolean pressRecentApps() throws RemoteException {
waitForIdle();
Log.d(TAG, "Pressing recent apps button.");
return getUiAutomation().performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
}
/**
* Opens the notification shade.
*
* @return true if successful, else return false
*/
public boolean openNotification() {
waitForIdle();
Log.d(TAG, "Opening notification.");
return getUiAutomation().performGlobalAction(
AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
}
/**
* Opens the Quick Settings shade.
*
* @return true if successful, else return false
*/
public boolean openQuickSettings() {
waitForIdle();
Log.d(TAG, "Opening quick settings.");
return getUiAutomation().performGlobalAction(
AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
}
/**
* Gets the width of the default display, in pixels. The size is adjusted based on the
* current orientation of the display.
*
* @return width in pixels
*/
public @Px int getDisplayWidth() {
return getDisplayWidth(Display.DEFAULT_DISPLAY);
}
/**
* Gets the width of the display with {@code displayId}, in pixels. The size is adjusted
* based on the current orientation of the display.
*
* @param displayId the display ID. Use {@link Display#getDisplayId()} to get the ID.
* @return width in pixels
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
public @Px int getDisplayWidth(int displayId) {
return getDisplaySize(displayId).x;
}
/**
* Gets the height of the default display, in pixels. The size is adjusted based on the
* current orientation of the display.
*
* @return height in pixels
*/
public @Px int getDisplayHeight() {
return getDisplayHeight(Display.DEFAULT_DISPLAY);
}
/**
* Gets the height of the display with {@code displayId}, in pixels. The size is adjusted
* based on the current orientation of the display.
*
* @param displayId the display ID. Use {@link Display#getDisplayId()} to get the ID.
* @return height in pixels
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
public @Px int getDisplayHeight(int displayId) {
return getDisplaySize(displayId).y;
}
/**
* Perform a click at arbitrary coordinates on the default display specified by the user.
*
* @param x coordinate
* @param y coordinate
* @return true if the click succeeded else false
*/
public boolean click(int x, int y) {
if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
Log.w(TAG, String.format("Cannot click. Point (%d, %d) is outside display (%d, %d).",
x, y, getDisplayWidth(), getDisplayHeight()));
return false;
}
Log.d(TAG, String.format("Clicking on (%d, %d).", x, y));
return getInteractionController().clickNoSync(x, y);
}
/**
* Performs a swipe from one coordinate to another on the default display using the number of
* steps to determine smoothness and speed. Each step execution is throttled to 5ms per step.
* So for a 100 steps, the swipe will take about 1/2 second to complete.
*
* @param startX X-axis value for the starting coordinate
* @param startY Y-axis value for the starting coordinate
* @param endX X-axis value for the ending coordinate
* @param endY Y-axis value for the ending coordinate
* @param steps is the number of move steps sent to the system
* @return false if the operation fails or the coordinates are invalid
*/
public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
Log.d(TAG, String.format("Swiping from (%d, %d) to (%d, %d) in %d steps.", startX, startY,
endX, endY, steps));
return getInteractionController()
.swipe(startX, startY, endX, endY, steps);
}
/**
* Performs a swipe from one coordinate to another coordinate on the default display. You can
* control the smoothness and speed of the swipe by specifying the number of steps. Each step
* execution is throttled to 5 milliseconds per step, so for a 100 steps, the swipe will take
* around 0.5 seconds to complete.
*
* @param startX X-axis value for the starting coordinate
* @param startY Y-axis value for the starting coordinate
* @param endX X-axis value for the ending coordinate
* @param endY Y-axis value for the ending coordinate
* @param steps is the number of steps for the swipe action
* @return true if swipe is performed, false if the operation fails or the coordinates are
* invalid
*/
public boolean drag(int startX, int startY, int endX, int endY, int steps) {
Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.", startX, startY,
endX, endY, steps));
return getInteractionController()
.swipe(startX, startY, endX, endY, steps, true);
}
/**
* Performs a swipe between points in the Point array on the default display. Each step
* execution is throttled to 5ms per step. So for a 100 steps, the swipe will take about 1/2
* second to complete.
*
* @param segments is Point array containing at least one Point object
* @param segmentSteps steps to inject between two Points
* @return true on success
*/
public boolean swipe(@NonNull Point[] segments, int segmentSteps) {
Log.d(TAG, String.format("Swiping between %s in %d steps.", Arrays.toString(segments),
segmentSteps * (segments.length - 1)));
return getInteractionController().swipe(segments, segmentSteps);
}
/**
* Waits for the current application to idle.
* Default wait timeout is 10 seconds
*/
public void waitForIdle() {
try (Section ignored = Traces.trace("UiDevice#waitForIdle")) {
getQueryController().waitForIdle();
}
}
/**
* Waits for the current application to idle.
* @param timeout in milliseconds
*/
public void waitForIdle(long timeout) {
try (Section ignored = Traces.trace("UiDevice#waitForIdle")) {
getQueryController().waitForIdle(timeout);
}
}
/**
* Retrieves the last activity to report accessibility events.
* @deprecated The results returned should be considered unreliable
* @return String name of activity
*/
@Deprecated
@SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
public String getCurrentActivityName() {
return getQueryController().getCurrentActivityName();
}
/**
* Retrieves the name of the last package to report accessibility events.
* @return String name of package
*/
@SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
public String getCurrentPackageName() {
return getQueryController().getCurrentPackageName();
}
/**
* Registers a {@link UiWatcher} to run automatically when the testing framework is unable to
* find a match using a {@link UiSelector}. See {@link #runWatchers()}
*
* @param name to register the UiWatcher
* @param watcher {@link UiWatcher}
*/
public void registerWatcher(@Nullable String name, @Nullable UiWatcher watcher) {
Log.d(TAG, String.format("Registering watcher %s.", name));
if (mInWatcherContext) {
throw new IllegalStateException("Cannot register new watcher from within another");
}
mWatchers.put(name, watcher);
}
/**
* Removes a previously registered {@link UiWatcher}.
*
* See {@link #registerWatcher(String, UiWatcher)}
* @param name used to register the UiWatcher
*/
public void removeWatcher(@Nullable String name) {
Log.d(TAG, String.format("Removing watcher %s.", name));
if (mInWatcherContext) {
throw new IllegalStateException("Cannot remove a watcher from within another");
}
mWatchers.remove(name);
}
/**
* This method forces all registered watchers to run.
* See {@link #registerWatcher(String, UiWatcher)}
*/
public void runWatchers() {
if (mInWatcherContext) {
return;
}
for (String watcherName : mWatchers.keySet()) {
UiWatcher watcher = mWatchers.get(watcherName);
if (watcher != null) {
try {
mInWatcherContext = true;
if (watcher.checkForCondition()) {
setWatcherTriggered(watcherName);
}
} catch (Exception e) {
Log.e(TAG, String.format("Failed to execute watcher %s.", watcherName), e);
} finally {
mInWatcherContext = false;
}
}
}
}
/**
* Resets a {@link UiWatcher} that has been triggered.
* If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call
* returned <code>true</code>, then the UiWatcher is considered triggered.
* See {@link #registerWatcher(String, UiWatcher)}
*/
public void resetWatcherTriggers() {
Log.d(TAG, "Resetting all watchers.");
mWatchersTriggers.clear();
}
/**
* Checks if a specific registered {@link UiWatcher} has triggered.
* See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its
* {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then
* the UiWatcher is considered triggered. This is helpful if a watcher is detecting errors
* from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered.
*
* @param watcherName
* @return true if triggered else false
*/
public boolean hasWatcherTriggered(@Nullable String watcherName) {
return mWatchersTriggers.contains(watcherName);
}
/**
* Checks if any registered {@link UiWatcher} have triggered.
*
* See {@link #registerWatcher(String, UiWatcher)}
* See {@link #hasWatcherTriggered(String)}
*/
public boolean hasAnyWatcherTriggered() {
return mWatchersTriggers.size() > 0;
}
/**
* Used internally by this class to set a {@link UiWatcher} state as triggered.
* @param watcherName
*/
private void setWatcherTriggered(String watcherName) {
if (!hasWatcherTriggered(watcherName)) {
mWatchersTriggers.add(watcherName);
}
}
/**
* @return true if default display is in its natural or flipped (180 degrees) orientation
*/
public boolean isNaturalOrientation() {
return isNaturalOrientation(Display.DEFAULT_DISPLAY);
}
/**
* @return true if display with {@code displayId} is in its natural or flipped (180 degrees)
* orientation
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
private boolean isNaturalOrientation(int displayId) {
int ret = getDisplayRotation(displayId);
return ret == UiAutomation.ROTATION_FREEZE_0
|| ret == UiAutomation.ROTATION_FREEZE_180;
}
/**
* @return the current rotation of the default display
* @see Display#getRotation()
*/
public int getDisplayRotation() {
return getDisplayRotation(Display.DEFAULT_DISPLAY);
}
/**
* @return the current rotation of the display with {@code displayId}
* @see Display#getDisplayId()
* @see Display#getRotation()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
public int getDisplayRotation(int displayId) {
waitForIdle();
Display display = getDisplayById(displayId);
if (display == null) {
throw new IllegalArgumentException(String.format("Display %d not found or not "
+ "accessible", displayId));
}
return display.getRotation();
}
/**
* Freezes the default display rotation at its current state.
* @throws RemoteException never
*/
public void freezeRotation() throws RemoteException {
Log.d(TAG, "Freezing rotation.");
getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
}
/**
* Freezes the rotation of the display with {@code displayId} at its current state.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void freezeRotation(int displayId) {
Log.d(TAG, String.format("Freezing rotation on display %d.", displayId));
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
executeShellCommand(String.format("cmd window user-rotation -d %d lock",
displayId));
} else {
int rotation = getDisplayRotation(displayId);
executeShellCommand(String.format("cmd window set-user-rotation lock -d %d %d",
displayId, rotation));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Un-freezes the default display rotation allowing its contents to rotate with its physical
* rotation. During testing, it is best to keep the default display frozen in a specific
* orientation.
* <p>Note: Need to wait a short period for the rotation animation to complete before
* performing another operation.
* @throws RemoteException never
*/
public void unfreezeRotation() throws RemoteException {
Log.d(TAG, "Unfreezing rotation.");
getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
}
/**
* Un-freezes the rotation of the display with {@code displayId} allowing its contents to
* rotate with its physical rotation. During testing, it is best to keep the display frozen
* in a specific orientation.
* <p>Note: Need to wait a short period for the rotation animation to complete before
* performing another operation.
* <p>Note: Some secondary displays don't have rotation sensors and therefore won't respond
* to this method.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
*/
@RequiresApi(30)
public void unfreezeRotation(int displayId) {
Log.d(TAG, String.format("Unfreezing rotation on display %d.", displayId));
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
executeShellCommand(String.format("cmd window user-rotation -d %d free",
displayId));
} else {
executeShellCommand(String.format("cmd window set-user-rotation free -d %d",
displayId));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Orients the default display to the left and freezes rotation. Use
* {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: This rotation is relative to the natural orientation which depends on the device
* type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
* {@link #setOrientationLandscape()}.
* @throws RemoteException never
*/
public void setOrientationLeft() throws RemoteException {
Log.d(TAG, "Setting orientation to left.");
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
}
/**
* Orients the display with {@code displayId} to the left and freezes rotation. Use
* {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: This rotation is relative to the natural orientation which depends on the device
* type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
* {@link #setOrientationLandscape()}.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void setOrientationLeft(int displayId) {
Log.d(TAG, String.format("Setting orientation to left on display %d.", displayId));
rotateWithCommand(Surface.ROTATION_90, displayId);
}
/**
* Orients the default display to the right and freezes rotation. Use
* {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: This rotation is relative to the natural orientation which depends on the device
* type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
* {@link #setOrientationLandscape()}.
* @throws RemoteException never
*/
public void setOrientationRight() throws RemoteException {
Log.d(TAG, "Setting orientation to right.");
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_270);
}
/**
* Orients the display with {@code displayId} to the right and freezes rotation. Use
* {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: This rotation is relative to the natural orientation which depends on the device
* type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
* {@link #setOrientationLandscape()}.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void setOrientationRight(int displayId) {
Log.d(TAG, String.format("Setting orientation to right on display %d.", displayId));
rotateWithCommand(Surface.ROTATION_270, displayId);
}
/**
* Orients the default display to its natural orientation and freezes rotation. Use
* {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: The natural orientation depends on the device type (e.g. phone vs. tablet).
* Consider using {@link #setOrientationPortrait()} and {@link #setOrientationLandscape()}.
* @throws RemoteException never
*/
public void setOrientationNatural() throws RemoteException {
Log.d(TAG, "Setting orientation to natural.");
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
}
/**
* Orients the display with {@code displayId} to its natural orientation and freezes rotation
* . Use {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: The natural orientation depends on the device type (e.g. phone vs. tablet).
* Consider using {@link #setOrientationPortrait()} and {@link #setOrientationLandscape()}.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void setOrientationNatural(int displayId) {
Log.d(TAG, String.format("Setting orientation to natural on display %d.", displayId));
rotateWithCommand(Surface.ROTATION_0, displayId);
}
/**
* Orients the default display to its portrait orientation (height >= width) and freezes
* rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
* @throws RemoteException never
*/
public void setOrientationPortrait() throws RemoteException {
Log.d(TAG, "Setting orientation to portrait.");
if (getDisplayHeight() >= getDisplayWidth()) {
freezeRotation(); // Already in portrait orientation.
} else if (isNaturalOrientation()) {
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
} else {
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
}
}
/**
* Orients the display with {@code displayId} to its portrait orientation (height >= width) and
* freezes rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void setOrientationPortrait(int displayId) {
Log.d(TAG, String.format("Setting orientation to portrait on display %d.", displayId));
if (getDisplayHeight(displayId) >= getDisplayWidth(displayId)) {
freezeRotation(displayId); // Already in portrait orientation.
} else if (isNaturalOrientation(displayId)) {
rotateWithCommand(Surface.ROTATION_90, displayId);
} else {
rotateWithCommand(Surface.ROTATION_0, displayId);
}
}
/**
* Orients the default display to its landscape orientation (width >= height) and freezes
* rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
* @throws RemoteException never
*/
public void setOrientationLandscape() throws RemoteException {
Log.d(TAG, "Setting orientation to landscape.");
if (getDisplayWidth() >= getDisplayHeight()) {
freezeRotation(); // Already in landscape orientation.
} else if (isNaturalOrientation()) {
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
} else {
rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
}
}
/**
* Orients the display with {@code displayId} to its landscape orientation (width >= height) and
* freezes rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
* <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
* officially supported.
* @see Display#getDisplayId()
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
public void setOrientationLandscape(int displayId) {
Log.d(TAG, String.format("Setting orientation to landscape on display %d.", displayId));
if (getDisplayWidth(displayId) >= getDisplayHeight(displayId)) {
freezeRotation(displayId); // Already in landscape orientation.
} else if (isNaturalOrientation(displayId)) {
rotateWithCommand(Surface.ROTATION_90, displayId);
} else {
rotateWithCommand(Surface.ROTATION_0, displayId);
}
}
/** Rotates the default display using UiAutomation and waits for the rotation to be detected. */
private void rotateWithUiAutomation(int rotation) {
getUiAutomation().setRotation(rotation);
waitRotationComplete(rotation, Display.DEFAULT_DISPLAY);
}
/**
* Rotates the display using shell command and waits for the rotation to be detected.
*
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
@RequiresApi(30)
private void rotateWithCommand(int rotation, int displayId) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
executeShellCommand(String.format("cmd window user-rotation -d %d lock %d",
displayId, rotation));
} else {
executeShellCommand(String.format("cmd window set-user-rotation lock -d %d %d",
displayId, rotation));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
waitRotationComplete(rotation, displayId);
}
/**
* Waits for the display with {@code displayId} to be in {@code rotation}.
*
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
private void waitRotationComplete(int rotation, int displayId) {
Condition<UiDevice, Boolean> rotationCondition = new Condition<UiDevice, Boolean>() {
@Override
public Boolean apply(UiDevice device) {
return device.getDisplayRotation(displayId) == rotation;
}
@NonNull
@Override
public String toString() {
return String.format("Condition[displayRotation=%d, displayId=%d]", rotation,
displayId);
}
};
if (!wait(rotationCondition, ROTATION_TIMEOUT)) {
Log.w(TAG, String.format("Didn't detect rotation within %dms.", ROTATION_TIMEOUT));
}
}
/**
* This method simulates pressing the power button if the default display is OFF, else it does
* nothing if the default display is already ON.
* <p>If the default display was OFF and it just got turned ON, this method will insert a 500ms
* delay for the device to wake up and accept input.
*
* @throws RemoteException
*/
public void wakeUp() throws RemoteException {
Log.d(TAG, "Turning on screen.");
if(getInteractionController().wakeDevice()) {
// Sync delay to allow the window manager to start accepting input after the device
// is awakened.
SystemClock.sleep(500);
}
}
/**
* Checks the power manager if the default display is ON.
*
* @return true if the screen is ON else false
* @throws RemoteException
*/
public boolean isScreenOn() throws RemoteException {
return getInteractionController().isScreenOn();
}
/**
* This method simply presses the power button if the default display is ON, else it does
* nothing if the default display is already OFF.
*
* @throws RemoteException
*/
public void sleep() throws RemoteException {
Log.d(TAG, "Turning off screen.");
getInteractionController().sleepDevice();
}
/**
* Helper method used for debugging to dump the current window's layout hierarchy.
* Relative file paths are stored the application's internal private storage location.
*
* @param fileName
* @deprecated Use {@link UiDevice#dumpWindowHierarchy(File)} or
* {@link UiDevice#dumpWindowHierarchy(OutputStream)} instead.
*/
@Deprecated
public void dumpWindowHierarchy(@NonNull String fileName) {
File dumpFile = new File(fileName);
if (!dumpFile.isAbsolute()) {
dumpFile = mInstrumentation.getContext().getFileStreamPath(fileName);
}
try {
dumpWindowHierarchy(dumpFile);
} catch (IOException e) {
// Ignore to preserve existing behavior. Ugh.
}
}
/**
* Dump the current window hierarchy to a {@link java.io.File}.
*
* @param dest The file in which to store the window hierarchy information.
* @throws IOException
*/
public void dumpWindowHierarchy(@NonNull File dest) throws IOException {
Log.d(TAG, String.format("Dumping window hierarchy to %s.", dest));
try (OutputStream stream = new BufferedOutputStream(new FileOutputStream(dest))) {
AccessibilityNodeInfoDumper.dumpWindowHierarchy(this, stream);
}
}
/**
* Dump the current window hierarchy to an {@link java.io.OutputStream}.
*
* @param out The output stream that the window hierarchy information is written to.
* @throws IOException
*/
public void dumpWindowHierarchy(@NonNull OutputStream out) throws IOException {
Log.d(TAG, String.format("Dumping window hierarchy to %s.", out));
AccessibilityNodeInfoDumper.dumpWindowHierarchy(this, out);
}
/**
* Waits for a window content update event to occur.
*
* If a package name for the window is specified, but the current window
* does not have the same package name, the function returns immediately.
*
* @param packageName the specified window package name (can be <code>null</code>).
* If <code>null</code>, a window update from any front-end window will end the wait
* @param timeout the timeout for the wait
*
* @return true if a window update occurred, false if timeout has elapsed or if the current
* window does not have the specified package name
*/
public boolean waitForWindowUpdate(@Nullable String packageName, long timeout) {
try (Section ignored = Traces.trace("UiDevice#waitForWindowUpdate")) {
if (packageName != null) {
if (!packageName.equals(getCurrentPackageName())) {
Log.w(TAG, String.format("Skipping wait as package %s does not match current "
+ "window %s.", packageName, getCurrentPackageName()));
return false;
}
}
Runnable emptyRunnable = () -> {
};
AccessibilityEventFilter checkWindowUpdate = t -> {
if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
return packageName == null || (t.getPackageName() != null
&& packageName.contentEquals(t.getPackageName()));
}
return false;
};
Log.d(TAG, String.format("Waiting %dms for window update of package %s.", timeout,
packageName));
try {
getUiAutomation().executeAndWaitForEvent(emptyRunnable, checkWindowUpdate, timeout);
} catch (TimeoutException e) {
Log.w(TAG, String.format("Timed out waiting %dms on window update.", timeout), e);
return false;
} catch (Exception e) {
Log.e(TAG, "Failed to wait for window update.", e);
return false;
}
return true;
}
}
/**
* Take a screenshot of current window and store it as PNG
*
* Default scale of 1.0f (original size) and 90% quality is used
* The screenshot is adjusted per screen rotation
*
* @param storePath where the PNG should be written to
* @return true if screen shot is created successfully, false otherwise
*/
public boolean takeScreenshot(@NonNull File storePath) {
return takeScreenshot(storePath, 1.0f, 90);
}
/**
* Take a screenshot of current window and store it as PNG
*
* The screenshot is adjusted per screen rotation
*
* @param storePath where the PNG should be written to
* @param scale scale the screenshot down if needed; 1.0f for original size
* @param quality quality of the PNG compression; range: 0-100
* @return true if screen shot is created successfully, false otherwise
*/
public boolean takeScreenshot(@NonNull File storePath, float scale, int quality) {
Log.d(TAG, String.format("Taking screenshot (scale=%f, quality=%d) and storing at %s.",
scale, quality, storePath));
Bitmap screenshot = getUiAutomation().takeScreenshot();
if (screenshot == null) {
Log.w(TAG, "Failed to take screenshot.");
return false;
}
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(storePath))) {
screenshot = Bitmap.createScaledBitmap(screenshot,
Math.round(scale * screenshot.getWidth()),
Math.round(scale * screenshot.getHeight()), false);
screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos);
bos.flush();
return true;
} catch (IOException ioe) {
Log.e(TAG, "Failed to save screenshot.", ioe);
return false;
} finally {
screenshot.recycle();
}
}
/**
* Retrieves the default launcher package name.
*
* <p>As of Android 11 (API level 30), apps must declare the packages and intents they intend
* to query. To use this method, an app will need to include the following in its manifest:
* <pre>{@code
* <queries>
* <intent>
* <action android:name="android.intent.action.MAIN"/>
* <category android:name="android.intent.category.HOME"/>
* </intent>
* </queries>
* }</pre>
*
* @return package name of the default launcher
*/
@SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
public String getLauncherPackageName() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
PackageManager pm = mInstrumentation.getContext().getPackageManager();
ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
return resolveInfo.activityInfo.packageName;
}
/**
* Executes a shell command using shell user identity, and return the standard output in string.
* <p>
* Calling function with large amount of output will have memory impacts, and the function call
* will block if the command executed is blocking.
*
* @param cmd the command to run
* @return the standard output of the command
* @throws IOException if an I/O error occurs while reading output
*/
@Discouraged(message = "Can be useful for simple commands, but lacks support for proper error"
+ " handling, input data, or complex commands (quotes, pipes) that can be obtained "
+ "from UiAutomation#executeShellCommandRwe or similar utilities.")
@RequiresApi(21)
@NonNull
public String executeShellCommand(@NonNull String cmd) throws IOException {
Log.d(TAG, String.format("Executing shell command: %s", cmd));
try (ParcelFileDescriptor pfd = Api21Impl.executeShellCommand(getUiAutomation(), cmd);
FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
byte[] buf = new byte[512];
int bytesRead;
StringBuilder stdout = new StringBuilder();
while ((bytesRead = fis.read(buf)) != -1) {
stdout.append(new String(buf, 0, bytesRead));
}
return stdout.toString();
}
}
/**
* Gets the display with {@code displayId}. The display may be null because it may be a private
* virtual display, for example.
*/
@Nullable
Display getDisplayById(int displayId) {
return mDisplayManager.getDisplay(displayId);
}
/**
* Gets the size of the display with {@code displayId}, in pixels. The size is adjusted based
* on the current orientation of the display.
*
* @see Display#getRealSize(Point)
* @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
*/
Point getDisplaySize(int displayId) {
Point p = new Point();
Display display = getDisplayById(displayId);
if (display == null) {
throw new IllegalArgumentException(String.format("Display %d not found or not "
+ "accessible", displayId));
}
display.getRealSize(p);
return p;
}
@RequiresApi(21)
private List<AccessibilityWindowInfo> getWindows(UiAutomation uiAutomation) {
// Support multi-display searches for API level 30 and up.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
final List<AccessibilityWindowInfo> windowList = new ArrayList<>();
final SparseArray<List<AccessibilityWindowInfo>> allWindows =
Api30Impl.getWindowsOnAllDisplays(uiAutomation);
for (int index = 0; index < allWindows.size(); index++) {
windowList.addAll(allWindows.valueAt(index));
}
return windowList;
}
return Api21Impl.getWindows(uiAutomation);
}
/** Returns a list containing the root {@link AccessibilityNodeInfo}s for each active window */
AccessibilityNodeInfo[] getWindowRoots() {
waitForIdle();
Set<AccessibilityNodeInfo> roots = new HashSet<>();
UiAutomation uiAutomation = getUiAutomation();
// Ensure the active window root is included.
AccessibilityNodeInfo activeRoot = uiAutomation.getRootInActiveWindow();
if (activeRoot != null) {
roots.add(activeRoot);
} else {
Log.w(TAG, "Active window root not found.");
}
// Support multi-window searches for API level 21 and up.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
for (final AccessibilityWindowInfo window : getWindows(uiAutomation)) {
final AccessibilityNodeInfo root = Api21Impl.getRoot(window);
if (root == null) {
Log.w(TAG, "Skipping null root node for window: " + window);
continue;
}
roots.add(root);
}
}
return roots.toArray(new AccessibilityNodeInfo[0]);
}
Instrumentation getInstrumentation() {
return mInstrumentation;
}
Context getUiContext(int displayId) {
Context context = mUiContexts.get(displayId);
if (context == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
final Display display = getDisplayById(displayId);
if (display != null) {
context = Api31Impl.createWindowContext(mInstrumentation.getContext(), display);
} else {
// The display may be null because it may be private display, for example. In
// such a case, use the instrumentation's context instead.
context = mInstrumentation.getContext();
}
} else {
context = mInstrumentation.getContext();
}
mUiContexts.put(displayId, context);
}
return context;
}
UiAutomation getUiAutomation() {
UiAutomation uiAutomation;
int flags = Configurator.getInstance().getUiAutomationFlags();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uiAutomation = Api24Impl.getUiAutomationWithRetry(getInstrumentation(), flags);
} else {
if (flags != Configurator.DEFAULT_UIAUTOMATION_FLAGS) {
Log.w(TAG, "UiAutomation flags not supported prior to API 24");
}
uiAutomation = getInstrumentation().getUiAutomation();
}
if (uiAutomation == null) {
throw new NullPointerException("Got null UiAutomation from instrumentation.");
}
// Verify and update the accessibility service flags if necessary. These might get reset
// if the underlying UiAutomationConnection is recreated.
AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
if (serviceInfo == null) {
Log.w(TAG, "Cannot verify accessibility service flags. "
+ "Multi-window support (searching non-active windows) may be disabled.");
return uiAutomation;
}
boolean serviceFlagsChanged = serviceInfo.flags != mCachedServiceFlags;
if (serviceFlagsChanged
|| SystemClock.uptimeMillis() - mLastServiceFlagsTime > SERVICE_FLAGS_TIMEOUT) {
// Enable multi-window support for API 21+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
}
// Enable or disable hierarchy compression.
if (mCompressed) {
serviceInfo.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
} else {
serviceInfo.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
}
if (serviceFlagsChanged) {
Log.d(TAG, String.format("Setting accessibility service flags: %d",
serviceInfo.flags));
}
uiAutomation.setServiceInfo(serviceInfo);
mCachedServiceFlags = serviceInfo.flags;
mLastServiceFlagsTime = SystemClock.uptimeMillis();
}
return uiAutomation;
}
QueryController getQueryController() {
return mQueryController;
}
InteractionController getInteractionController() {
return mInteractionController;
}
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() {
}
@DoNotInline
static ParcelFileDescriptor executeShellCommand(UiAutomation uiAutomation, String command) {
return uiAutomation.executeShellCommand(command);
}
@DoNotInline
static List<AccessibilityWindowInfo> getWindows(UiAutomation uiAutomation) {
return uiAutomation.getWindows();
}
@DoNotInline
static AccessibilityNodeInfo getRoot(AccessibilityWindowInfo accessibilityWindowInfo) {
return accessibilityWindowInfo.getRoot();
}
}
@RequiresApi(24)
static class Api24Impl {
private Api24Impl() {
}
@DoNotInline
static UiAutomation getUiAutomationWithRetry(Instrumentation instrumentation, int flags) {
UiAutomation uiAutomation = null;
for (int i = 0; i < MAX_UIAUTOMATION_RETRY; i++) {
uiAutomation = instrumentation.getUiAutomation(flags);
if (uiAutomation != null) {
break;
}
if (i < MAX_UIAUTOMATION_RETRY - 1) {
Log.e(TAG, "Got null UiAutomation from instrumentation - Retrying...");
SystemClock.sleep(UIAUTOMATION_RETRY_INTERVAL);
}
}
return uiAutomation;
}
}
@RequiresApi(30)
static class Api30Impl {
private Api30Impl() {
}
@DoNotInline
static SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays(
UiAutomation uiAutomation) {
return uiAutomation.getWindowsOnAllDisplays();
}
}
@RequiresApi(31)
static class Api31Impl {
private Api31Impl() {
}
@DoNotInline
static Context createWindowContext(Context context, Display display) {
return context.createWindowContext(display,
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, null);
}
}
}