| /* |
| * 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 com.android.uiautomator.core; |
| |
| import android.content.Context; |
| import android.graphics.Point; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.Display; |
| import android.view.IWindowManager; |
| import android.view.KeyEvent; |
| import android.view.Surface; |
| import android.view.WindowManagerImpl; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| import com.android.internal.statusbar.IStatusBarService; |
| import com.android.internal.util.Predicate; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * UiDevice provides access to device wide states. Also provides methods to simulate |
| * pressing hardware buttons such as DPad or the soft buttons such as Home and Menu. |
| */ |
| public class UiDevice { |
| private static final String LOG_TAG = UiDevice.class.getSimpleName(); |
| |
| private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000; |
| |
| // store for registered UiWatchers |
| private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); |
| private final List<String> mWatchersTriggers = new ArrayList<String>(); |
| |
| // remember if we're executing in the context of a UiWatcher |
| private boolean mInWatcherContext = false; |
| |
| // provides access the {@link QueryController} and {@link InteractionController} |
| private final UiAutomatorBridge mUiAutomationBridge; |
| |
| // reference to self |
| private static UiDevice mDevice; |
| |
| private UiDevice() { |
| mUiAutomationBridge = new UiAutomatorBridge(); |
| mDevice = this; |
| } |
| |
| boolean isInWatcherContext() { |
| return mInWatcherContext; |
| } |
| |
| /** |
| * Provides access the {@link QueryController} and {@link InteractionController} |
| * @return {@link UiAutomatorBridge} |
| */ |
| UiAutomatorBridge getAutomatorBridge() { |
| return mUiAutomationBridge; |
| } |
| /** |
| * Allow both the direct creation of a UiDevice and retrieving a existing |
| * instance of UiDevice. This helps tests and their libraries to have access |
| * to UiDevice with necessitating having to always pass copies of UiDevice |
| * instances around. |
| * @return UiDevice instance |
| */ |
| public static UiDevice getInstance() { |
| if (mDevice == null) { |
| mDevice = new UiDevice(); |
| } |
| return mDevice; |
| } |
| |
| /** |
| * Returns the display size in dp (device-independent pixel) |
| * |
| * The returned display size is adjusted per screen rotation |
| * |
| * @return |
| * @hide |
| */ |
| public Point getDisplaySizeDp() { |
| Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); |
| Point p = new Point(); |
| display.getSize(p); |
| DisplayMetrics metrics = new DisplayMetrics(); |
| display.getMetrics(metrics); |
| float dpx = p.x / metrics.density; |
| float dpy = p.y / metrics.density; |
| p.x = Math.round(dpx); |
| p.y = Math.round(dpy); |
| return p; |
| } |
| |
| /** |
| * Returns the product name of the device |
| * |
| * This provides info on what type of device that the test is running on. However, for the |
| * purpose of adapting to different styles of UI, test should favor |
| * {@link UiDevice#getDisplaySizeDp()} over this method, and only use product name as a fallback |
| * mechanism |
| */ |
| public String getProductName() { |
| return Build.PRODUCT; |
| } |
| |
| /** |
| * This method returns the text from the last UI traversal event received. |
| * This is helpful in WebView when the test performs directional arrow presses to focus |
| * on different elements inside the WebView. The accessibility fires events |
| * with every text highlighted. One can read the contents of a WebView control this way |
| * however slow slow and unreliable it is. When the view control used can return a |
| * reference to is Document Object Model, it is recommended then to use the view's |
| * DOM instead. |
| * @return text of the last traversal event else an empty string |
| */ |
| public String getLastTraversedText() { |
| return mUiAutomationBridge.getQueryController().getLastTraversedText(); |
| } |
| |
| /** |
| * Helper to clear the text saved from the last accessibility UI traversal event. |
| * See {@link #getLastTraversedText()}. |
| */ |
| public void clearLastTraversedText() { |
| mUiAutomationBridge.getQueryController().clearLastTraversedText(); |
| } |
| |
| /** |
| * Helper method to do a short press on MENU button |
| * @return true if successful else false |
| */ |
| public boolean pressMenu() { |
| return pressKeyCode(KeyEvent.KEYCODE_MENU); |
| } |
| |
| /** |
| * Helper method to do a short press on BACK button |
| * @return true if successful else false |
| */ |
| public boolean pressBack() { |
| return pressKeyCode(KeyEvent.KEYCODE_BACK); |
| } |
| |
| /** |
| * Helper method to do a short press on HOME button |
| * @return true if successful else false |
| */ |
| public boolean pressHome() { |
| return pressKeyCode(KeyEvent.KEYCODE_HOME); |
| } |
| |
| /** |
| * Helper method to do a short press on SEARCH button |
| * @return true if successful else false |
| */ |
| public boolean pressSearch() { |
| return pressKeyCode(KeyEvent.KEYCODE_SEARCH); |
| } |
| |
| /** |
| * Helper method to do a short press on DOWN button |
| * @return true if successful else false |
| */ |
| public boolean pressDPadCenter() { |
| return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); |
| } |
| |
| /** |
| * Helper method to do a short press on DOWN button |
| * @return true if successful else false |
| */ |
| public boolean pressDPadDown() { |
| return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); |
| } |
| |
| /** |
| * Helper method to do a short press on UP button |
| * @return true if successful else false |
| */ |
| public boolean pressDPadUp() { |
| return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); |
| } |
| |
| /** |
| * Helper method to do a short press on LEFT button |
| * @return true if successful else false |
| */ |
| public boolean pressDPadLeft() { |
| return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); |
| } |
| |
| /** |
| * Helper method to do a short press on RIGTH button |
| * @return true if successful else false |
| */ |
| public boolean pressDPadRight() { |
| return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); |
| } |
| |
| /** |
| * Helper method to do a short press on DELETE |
| * @return true if successful else false |
| */ |
| public boolean pressDelete() { |
| return pressKeyCode(KeyEvent.KEYCODE_DEL); |
| } |
| |
| /** |
| * Helper method to do a short press on ENTER |
| * @return true if successful else false |
| */ |
| public boolean pressEnter() { |
| return pressKeyCode(KeyEvent.KEYCODE_ENTER); |
| } |
| |
| /** |
| * Helper method to do a short press using a key code. See {@link KeyEvent} |
| * @return true if successful else false |
| */ |
| public boolean pressKeyCode(int keyCode) { |
| waitForIdle(); |
| return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0); |
| } |
| |
| /** |
| * Helper method to do a short press using a key code. See {@link KeyEvent} |
| * @param keyCode See {@link KeyEvent} |
| * @param metaState See {@link KeyEvent} |
| * @return true if successful else false |
| */ |
| public boolean pressKeyCode(int keyCode, int metaState) { |
| waitForIdle(); |
| return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState); |
| } |
| |
| /** |
| * Gets the width of the display, in pixels. The width and height details |
| * are reported based on the current orientation of the display. |
| * @return width in pixels or zero on failure |
| */ |
| public int getDisplayWidth() { |
| IWindowManager wm = IWindowManager.Stub.asInterface( |
| ServiceManager.getService(Context.WINDOW_SERVICE)); |
| Point p = new Point(); |
| try { |
| wm.getDisplaySize(p); |
| } catch (RemoteException e) { |
| return 0; |
| } |
| return p.x; |
| } |
| |
| /** |
| * Press recent apps soft key |
| * @return true if successful |
| * @throws RemoteException |
| */ |
| public boolean pressRecentApps() throws RemoteException { |
| waitForIdle(); |
| final IStatusBarService statusBar = IStatusBarService.Stub.asInterface( |
| ServiceManager.getService(Context.STATUS_BAR_SERVICE)); |
| |
| if (statusBar != null) { |
| statusBar.toggleRecentApps(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Gets the height of the display, in pixels. The size is adjusted based |
| * on the current orientation of the display. |
| * @return height in pixels or zero on failure |
| */ |
| public int getDisplayHeight() { |
| IWindowManager wm = IWindowManager.Stub.asInterface( |
| ServiceManager.getService(Context.WINDOW_SERVICE)); |
| Point p = new Point(); |
| try { |
| wm.getDisplaySize(p); |
| } catch (RemoteException e) { |
| return 0; |
| } |
| return p.y; |
| } |
| |
| /** |
| * Perform a click at arbitrary coordinates 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()) { |
| return (false); |
| } |
| return getAutomatorBridge().getInteractionController().click(x, y); |
| } |
| |
| /** |
| * Performs a swipe from one coordinate to another 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 |
| * @param startY |
| * @param endX |
| * @param endY |
| * @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) { |
| return mUiAutomationBridge.getInteractionController() |
| .scrollSwipe(startX, startY, endX, endY, steps); |
| } |
| |
| /** |
| * Performs a swipe between points in the Point array. 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(Point[] segments, int segmentSteps) { |
| return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); |
| } |
| |
| public void waitForIdle() { |
| waitForIdle(DEFAULT_TIMEOUT_MILLIS); |
| } |
| |
| public void waitForIdle(long time) { |
| mUiAutomationBridge.waitForIdle(time); |
| } |
| |
| /** |
| * Last activity to report accessibility events |
| * @return String name of activity |
| */ |
| public String getCurrentActivityName() { |
| return mUiAutomationBridge.getQueryController().getCurrentActivityName(); |
| } |
| |
| /** |
| * Last package to report accessibility events |
| * @return String name of package |
| */ |
| public String getCurrentPackageName() { |
| return mUiAutomationBridge.getQueryController().getCurrentPackageName(); |
| } |
| |
| /** |
| * Registers a condition watcher to be called by the automation library only when a |
| * {@link UiObject} method call is in progress and is in retry waiting to match |
| * its UI element. Only during these conditions the watchers are invoked to check if |
| * there is something else unexpected on the screen that may be causing the match failure |
| * and retries. Under normal conditions when UiObject methods are immediately matching |
| * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()} |
| * |
| * @param name of watcher |
| * @param watcher {@link UiWatcher} |
| */ |
| public void registerWatcher(String name, UiWatcher watcher) { |
| if (mInWatcherContext) { |
| throw new IllegalStateException("Cannot register new watcher from within another"); |
| } |
| mWatchers.put(name, watcher); |
| } |
| |
| /** |
| * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}. |
| * |
| * @param name of watcher used when <code>registerWatcher</code> was called. |
| * @throws UiAutomationException |
| */ |
| public void removeWatcher(String name) { |
| if (mInWatcherContext) { |
| throw new IllegalStateException("Cannot remove a watcher from within another"); |
| } |
| mWatchers.remove(name); |
| } |
| |
| /** |
| * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers |
| * to run. |
| */ |
| 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(LOG_TAG, "Exceuting watcher: " + watcherName, e); |
| } finally { |
| mInWatcherContext = false; |
| } |
| } |
| } |
| } |
| |
| /** |
| * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and |
| * returns true from its implementation of {@link UiWatcher#checkForCondition()} then |
| * it is considered triggered. |
| */ |
| public void resetWatcherTriggers() { |
| mWatchersTriggers.clear(); |
| } |
| |
| /** |
| * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and |
| * returns true from its implementation of {@link UiWatcher#checkForCondition()} then |
| * it is considered triggered. This method can be used to check if a specific UiWatcher |
| * has been triggered during the test. 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. |
| */ |
| public boolean hasWatcherTriggered(String watcherName) { |
| return mWatchersTriggers.contains(watcherName); |
| } |
| |
| /** |
| * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)} |
| */ |
| public boolean hasAnyWatcherTriggered() { |
| return mWatchersTriggers.size() > 0; |
| } |
| |
| private void setWatcherTriggered(String watcherName) { |
| if (!hasWatcherTriggered(watcherName)) { |
| mWatchersTriggers.add(watcherName); |
| } |
| } |
| |
| /** |
| * Check if the device is in its natural orientation. This is determined by checking if the |
| * orientation is at 0 or 180 degrees. |
| * @return true if it is in natural orientation |
| */ |
| public boolean isNaturalOrientation() { |
| Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); |
| return display.getRotation() == Surface.ROTATION_0 || |
| display.getRotation() == Surface.ROTATION_180; |
| } |
| |
| /** |
| * Disables the sensors and freezes the device rotation at its |
| * current rotation state. |
| * @throws RemoteException |
| */ |
| public void freezeRotation() throws RemoteException { |
| getAutomatorBridge().getInteractionController().freezeRotation(); |
| } |
| |
| /** |
| * Re-enables the sensors and un-freezes the device rotation allowing its contents |
| * to rotate with the device physical rotation. Note that by un-freezing the rotation, |
| * the screen contents may suddenly rotate depending on the current physical position |
| * of the test device. During a test execution, it is best to keep the device frozen |
| * in a specific orientation until the test case execution is completed. |
| * @throws RemoteException |
| */ |
| public void unfreezeRotation() throws RemoteException { |
| getAutomatorBridge().getInteractionController().unfreezeRotation(); |
| } |
| |
| /** |
| * Orients the device to the left and also freezes rotation in that |
| * orientation by disabling the sensors. If you want to un-freeze the rotation |
| * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing |
| * so may cause the screen contents to get re-oriented depending on the current |
| * physical position of the test device. |
| * @throws RemoteException |
| */ |
| public void setOrientationLeft() throws RemoteException { |
| getAutomatorBridge().getInteractionController().setRotationLeft(); |
| } |
| |
| /** |
| * Orients the device to the right and also freezes rotation in that |
| * orientation by disabling the sensors. If you want to un-freeze the rotation |
| * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing |
| * so may cause the screen contents to get re-oriented depending on the current |
| * physical position of the test device. |
| * @throws RemoteException |
| */ |
| public void setOrientationRight() throws RemoteException { |
| getAutomatorBridge().getInteractionController().setRotationRight(); |
| } |
| |
| /** |
| * Rotates right and also freezes rotation in that orientation by |
| * disabling the sensors. If you want to un-freeze the rotation |
| * and re-enable the sensors see {@link #unfreezeRotation()}. Note |
| * that doing so may cause the screen contents to rotate |
| * depending on the current physical position of the test device. |
| * @throws RemoteException |
| */ |
| public void setOrientationNatural() throws RemoteException { |
| getAutomatorBridge().getInteractionController().setRotationNatural(); |
| } |
| |
| /** |
| * This method simply presses the power button if the screen is OFF else |
| * it does nothing if the screen is already ON. If the screen was OFF and |
| * it just got turned ON, this method will insert a 500ms delay to allow |
| * the device time to wake up and accept input. |
| * @throws RemoteException |
| */ |
| public void wakeUp() throws RemoteException { |
| if(getAutomatorBridge().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 screen is ON |
| * @return true if the screen is ON else false |
| * @throws RemoteException |
| */ |
| public boolean isScreenOn() throws RemoteException { |
| return getAutomatorBridge().getInteractionController().isScreenOn(); |
| } |
| |
| /** |
| * This method simply presses the power button if the screen is ON else |
| * it does nothing if the screen is already OFF. |
| * @throws RemoteException |
| */ |
| public void sleep() throws RemoteException { |
| getAutomatorBridge().getInteractionController().sleepDevice(); |
| } |
| |
| /** |
| * Helper method used for debugging to dump the current window's layout hierarchy. |
| * The file root location is /data/local/tmp |
| * |
| * @param fileName |
| */ |
| public void dumpWindowHierarchy(String fileName) { |
| AccessibilityNodeInfo root = |
| getAutomatorBridge().getQueryController().getAccessibilityRootNode(); |
| if(root != null) { |
| AccessibilityNodeInfoDumper.dumpWindowToFile( |
| root, new File(new File(Environment.getDataDirectory(), |
| "local/tmp"), fileName)); |
| } |
| } |
| |
| /** |
| * Waits for a window content update event to occur |
| * |
| * if a package name for window is specified, but current window is not with the same package |
| * name, the function will return immediately |
| * |
| * @param packageName the specified window package name; maybe <code>null</code>, and a window |
| * update from any frontend window will end the wait |
| * @param timeout the timeout for the wait |
| * |
| * @return true if a window update occured, false if timeout has reached or current window is |
| * not the specified package name |
| */ |
| public boolean waitForWindowUpdate(final String packageName, long timeout) { |
| if (packageName != null) { |
| if (!packageName.equals(getCurrentPackageName())) { |
| return false; |
| } |
| } |
| Runnable emptyRunnable = new Runnable() { |
| @Override |
| public void run() { |
| } |
| }; |
| Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { |
| @Override |
| public boolean apply(AccessibilityEvent t) { |
| if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { |
| return packageName == null || packageName.equals(t.getPackageName()); |
| } |
| return false; |
| } |
| }; |
| try { |
| getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( |
| emptyRunnable, checkWindowUpdate, timeout); |
| } catch (TimeoutException e) { |
| return false; |
| } catch (Exception e) { |
| Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); |
| return false; |
| } |
| return true; |
| } |
| } |