Ejemplo n.º 1
0
def UiAutomator():
    current_device = airtest.cli.runner.device()
    if not current_device:
        set_serialno()
    current_device = airtest.cli.runner.device()
    dev = AutomatorDevice(current_device.serialno)
    return AutomatorWrapper(dev)
Ejemplo n.º 2
0
 def test_serial(self):
     with patch('uiautomator.AutomatorServer') as AutomatorServer:
         AutomatorDevice("abcdefhijklmn")
         AutomatorServer.assert_called_once_with(serial="abcdefhijklmn",
                                                 local_port=None,
                                                 adb_server_host=None,
                                                 adb_server_port=None)
Ejemplo n.º 3
0
    def __init__(self, serial=None, **kwargs):
        """Initial AndroidDevice
        Args:
            serial: string specify which device

        Returns:
            AndroidDevice object

        Raises:
            EnvironmentError
        """
        self.__display = None
        serial = serial or getenvs('ATX_ADB_SERIALNO', 'ANDROID_SERIAL')
        self._host = kwargs.get('host') or getenvs(
            'ATX_ADB_HOST', 'ANDROID_ADB_SERVER_HOST') or '127.0.0.1'
        self._port = int(
            kwargs.get('port')
            or getenvs('ATX_ADB_PORT', 'ANDROID_ADB_SERVER_PORT') or 5037)

        self._adb_client = adbkit.Client(self._host, self._port)
        self._adb_device = self._adb_client.device(serial)
        self._adb_shell_timeout = 30.0  # max adb shell exec time

        kwargs['adb_server_host'] = kwargs.pop('host', self._host)
        kwargs['adb_server_port'] = kwargs.pop('port', self._port)

        self._uiauto = UiaDevice(serial, **kwargs)
        DeviceMixin.__init__(self)

        self._randid = base.id_generator(5)

        self.screen_rotation = None
        self.screenshot_method = consts.SCREENSHOT_METHOD_AUTO

        # inherts from atx-uiautomator
        self.swipe = self._uiauto.swipe
        self.drag = self._uiauto.drag
        self.press = self._uiauto.press
        self.long_click = self._uiauto.long_click
        self.dump = self._uiauto.dump
Ejemplo n.º 4
0
class TestDevice(unittest.TestCase):

    def setUp(self):
        self.device = AutomatorDevice()
        self.device.server = MagicMock()
        self.device.server.jsonrpc = MagicMock()

    def test_info(self):
        self.device.server.jsonrpc.deviceInfo = MagicMock()
        self.device.server.jsonrpc.deviceInfo.return_value = {}
        self.assertEqual(self.device.info, {})
        self.device.server.jsonrpc.deviceInfo.assert_called_once_with()

    def test_click(self):
        self.device.server.jsonrpc.click = MagicMock()
        self.device.server.jsonrpc.click.return_value = True
        self.assertEqual(self.device.click(1, 2), True)
        self.device.server.jsonrpc.click.assert_called_once_with(1, 2)

    def test_swipe(self):
        self.device.server.jsonrpc.swipe = MagicMock()
        self.device.server.jsonrpc.swipe.return_value = True
        self.assertEqual(self.device.swipe(1, 2, 3, 4, 100), True)
        self.device.server.jsonrpc.swipe.assert_called_once_with(1, 2, 3, 4, 100)

    def test_drag(self):
        self.device.server.jsonrpc.drag = MagicMock()
        self.device.server.jsonrpc.drag.return_value = True
        self.assertEqual(self.device.drag(1, 2, 3, 4, 100), True)
        self.device.server.jsonrpc.drag.assert_called_once_with(1, 2, 3, 4, 100)

    def test_dump(self):
        self.device.server.jsonrpc.dumpWindowHierarchy = MagicMock()
        self.device.server.jsonrpc.dumpWindowHierarchy.return_value = "1.xml"
        self.device.server.adb.cmd = cmd = MagicMock()
        cmd.return_value.returncode = 0
        self.assertEqual(self.device.dump("test.xml"), "test.xml")
        self.device.server.jsonrpc.dumpWindowHierarchy.assert_called_once_with(True, "dump.xml")
        self.assertEqual(cmd.call_args_list, [call("pull", "1.xml", "test.xml"), call("shell", "rm", "1.xml")])

        self.device.server.jsonrpc.dumpWindowHierarchy.return_value = None
        self.assertEqual(self.device.dump("test.xml"), None)

    def test_screenshot(self):
        self.device.server.jsonrpc.takeScreenshot = MagicMock()
        self.device.server.jsonrpc.takeScreenshot.return_value = "1.png"
        self.device.server.adb.cmd = cmd = MagicMock()
        cmd.return_value.returncode = 0
        self.assertEqual(self.device.screenshot("a.png", 1.0, 99), "a.png")
        self.device.server.jsonrpc.takeScreenshot.assert_called_once_with("screenshot.png", 1.0, 99)
        self.assertEqual(cmd.call_args_list, [call("pull", "1.png", "a.png"), call("shell", "rm", "1.png")])

        self.device.server.jsonrpc.takeScreenshot.return_value = None
        self.assertEqual(self.device.screenshot("a.png", 1.0, 100), None)

    def test_freeze_rotation(self):
        self.device.server.jsonrpc.freezeRotation = MagicMock()
        self.device.freeze_rotation(True)
        self.device.freeze_rotation(False)
        self.assertEqual(self.device.server.jsonrpc.freezeRotation.call_args_list, [call(True), call(False)])

    def test_orientation(self):
        self.device.server.jsonrpc.deviceInfo = MagicMock()
        orientation = {
            0: "natural",
            1: "left",
            2: "upsidedown",
            3: "right"
        }
        for i in range(4):
            self.device.server.jsonrpc.deviceInfo.return_value = {"displayRotation": i}
            self.assertEqual(self.device.orientation, orientation[i])
        # set
        orientations = [
            (0, "natural", "n", 0),
            (1, "left", "l", 90),
            (2, "upsidedown", "u", 180),
            (3, "right", "r", 270)
        ]
        for values in orientations:
            for value in values:
                self.device.server.jsonrpc.setOrientation = MagicMock()
                self.device.orientation = value
                self.device.server.jsonrpc.setOrientation.assert_called_once_with(values[1])

        with self.assertRaises(ValueError):
            self.device.orientation = "invalid orientation"

    def test_last_traversed_text(self):
        self.device.server.jsonrpc.getLastTraversedText = MagicMock()
        self.device.server.jsonrpc.getLastTraversedText.return_value = "abcdef"
        self.assertEqual(self.device.last_traversed_text, "abcdef")
        self.device.server.jsonrpc.getLastTraversedText.assert_called_once_with()

    def test_clear_traversed_text(self):
        self.device.server.jsonrpc.clearLastTraversedText = MagicMock()
        self.device.clear_traversed_text()
        self.device.server.jsonrpc.clearLastTraversedText.assert_called_once_with()

    def test_open(self):
        self.device.server.jsonrpc.openNotification = MagicMock()
        self.device.open.notification()
        self.device.server.jsonrpc.openNotification.assert_called_once_with()
        self.device.server.jsonrpc.openQuickSettings = MagicMock()
        self.device.open.quick_settings()
        self.device.server.jsonrpc.openQuickSettings.assert_called_once_with()

    def test_watchers(self):
        names = ["a", "b", "c"]
        self.device.server.jsonrpc.getWatchers = MagicMock()
        self.device.server.jsonrpc.getWatchers.return_value = names
        self.assertEqual(self.device.watchers, names)
        self.device.server.jsonrpc.getWatchers.assert_called_once_with()

        self.device.server.jsonrpc.hasAnyWatcherTriggered = MagicMock()
        self.device.server.jsonrpc.hasAnyWatcherTriggered.return_value = True
        self.assertEqual(self.device.watchers.triggered, True)
        self.device.server.jsonrpc.hasAnyWatcherTriggered.assert_called_once_with()

        self.device.server.jsonrpc.removeWatcher = MagicMock()
        self.device.watchers.remove("a")
        self.device.server.jsonrpc.removeWatcher.assert_called_once_with("a")
        self.device.server.jsonrpc.removeWatcher = MagicMock()
        self.device.watchers.remove()
        self.assertEqual(self.device.server.jsonrpc.removeWatcher.call_args_list, [call(name) for name in names])

        self.device.server.jsonrpc.resetWatcherTriggers = MagicMock()
        self.device.watchers.reset()
        self.device.server.jsonrpc.resetWatcherTriggers.assert_called_once_with()

        self.device.server.jsonrpc.runWatchers = MagicMock()
        self.device.watchers.run()
        self.device.server.jsonrpc.runWatchers.assert_called_once_with()

    def test_watcher(self):
        self.device.server.jsonrpc.hasWatcherTriggered = MagicMock()
        self.device.server.jsonrpc.hasWatcherTriggered.return_value = False
        self.assertFalse(self.device.watcher("name").triggered)
        self.device.server.jsonrpc.hasWatcherTriggered.assert_called_once_with("name")

        self.device.server.jsonrpc.removeWatcher = MagicMock()
        self.device.watcher("a").remove()
        self.device.server.jsonrpc.removeWatcher.assert_called_once_with("a")

        self.device.server.jsonrpc.registerClickUiObjectWatcher = MagicMock()
        condition1 = {"text": "my text", "className": "android"}
        condition2 = {"description": "my desc", "clickable": True}
        target = {"className": "android.widget.Button", "text": "OK"}
        self.device.watcher("watcher").when(**condition1).when(**condition2).click(**target)
        self.device.server.jsonrpc.registerClickUiObjectWatcher.assert_called_once_with("watcher", [Selector(**condition1), Selector(**condition2)], Selector(**target))

        self.device.server.jsonrpc.registerPressKeyskWatcher = MagicMock()
        self.device.watcher("watcher2").when(**condition1).when(**condition2).press.back.home.power("menu")
        self.device.server.jsonrpc.registerPressKeyskWatcher.assert_called_once_with(
            "watcher2", [Selector(**condition1), Selector(**condition2)], ("back", "home", "power", "menu"))

    def test_press(self):
        key = ["home", "back", "left", "right", "up", "down", "center",
               "menu", "search", "enter", "delete", "del", "recent",
               "volume_up", "volume_down", "volume_mute", "camera", "power"]
        self.device.server.jsonrpc.pressKey = MagicMock()
        self.device.server.jsonrpc.pressKey.return_value = True
        self.assertTrue(self.device.press.home())
        self.device.server.jsonrpc.pressKey.return_value = False
        self.assertFalse(self.device.press.back())
        self.device.server.jsonrpc.pressKey.return_value = False
        for k in key:
            self.assertFalse(self.device.press(k))
        self.assertEqual(self.device.server.jsonrpc.pressKey.call_args_list, [call("home"), call("back")] + [call(k) for k in key])

        self.device.server.jsonrpc.pressKeyCode.return_value = True
        self.assertTrue(self.device.press(1))
        self.assertTrue(self.device.press(1, 2))
        self.assertEqual(self.device.server.jsonrpc.pressKeyCode.call_args_list, [call(1), call(1, 2)])

    def test_wakeup(self):
        self.device.server.jsonrpc.wakeUp = MagicMock()
        self.device.wakeup()
        self.device.server.jsonrpc.wakeUp.assert_called_once_with()

        self.device.server.jsonrpc.wakeUp = MagicMock()
        self.device.screen.on()
        self.device.server.jsonrpc.wakeUp.assert_called_once_with()

        self.device.server.jsonrpc.wakeUp = MagicMock()
        self.device.screen("on")
        self.device.server.jsonrpc.wakeUp.assert_called_once_with()

    def test_sleep(self):
        self.device.server.jsonrpc.sleep = MagicMock()
        self.device.sleep()
        self.device.server.jsonrpc.sleep.assert_called_once_with()

        self.device.server.jsonrpc.sleep = MagicMock()
        self.device.screen.off()
        self.device.server.jsonrpc.sleep.assert_called_once_with()

        self.device.server.jsonrpc.sleep = MagicMock()
        self.device.screen("off")
        self.device.server.jsonrpc.sleep.assert_called_once_with()

    def test_wait_idle(self):
        self.device.server.jsonrpc.waitForIdle = MagicMock()
        self.device.server.jsonrpc.waitForIdle.return_value = True
        self.assertTrue(self.device.wait.idle(timeout=10))
        self.device.server.jsonrpc.waitForIdle.assert_called_once_with(10)

        self.device.server.jsonrpc.waitForIdle = MagicMock()
        self.device.server.jsonrpc.waitForIdle.return_value = False
        self.assertFalse(self.device.wait("idle", timeout=10))
        self.device.server.jsonrpc.waitForIdle.assert_called_once_with(10)

    def test_wait_update(self):
        self.device.server.jsonrpc.waitForWindowUpdate = MagicMock()
        self.device.server.jsonrpc.waitForWindowUpdate.return_value = True
        self.assertTrue(self.device.wait.update(timeout=10, package_name="android"))
        self.device.server.jsonrpc.waitForWindowUpdate.assert_called_once_with("android", 10)

        self.device.server.jsonrpc.waitForWindowUpdate = MagicMock()
        self.device.server.jsonrpc.waitForWindowUpdate.return_value = False
        self.assertFalse(self.device.wait("update", timeout=100, package_name="android"))
        self.device.server.jsonrpc.waitForWindowUpdate.assert_called_once_with("android", 100)

    def test_get_info_attr(self):
        info = {"test_a": 1, "test_b": "string", "displayWidth": 720, "displayHeight": 1024}
        self.device.server.jsonrpc.deviceInfo = MagicMock()
        self.device.server.jsonrpc.deviceInfo.return_value = info
        for k in info:
            self.assertEqual(getattr(self.device, k), info[k])
        self.assertEqual(self.device.width, info["displayWidth"])
        self.assertEqual(self.device.height, info["displayHeight"])
        with self.assertRaises(AttributeError):
            self.device.not_exists

    def test_device_obj(self):
        with patch("uiautomator.AutomatorDeviceObject") as AutomatorDeviceObject:
            kwargs = {"text": "abc", "description": "description...", "clickable": True}
            self.device(**kwargs)
            AutomatorDeviceObject.assert_called_once_with(self.device, Selector(**kwargs))

        with patch("uiautomator.AutomatorDeviceObject") as AutomatorDeviceObject:
            AutomatorDeviceObject.return_value.exists = True
            self.assertTrue(self.device.exists(clickable=True))
            AutomatorDeviceObject.return_value.exists = False
            self.assertFalse(self.device.exists(text="..."))
Ejemplo n.º 5
0
 def setUp(self):
     self.device = AutomatorDevice()
     self.device.server = MagicMock()
     self.device.server.jsonrpc = MagicMock()
Ejemplo n.º 6
0
class AndroidDevice(DeviceMixin):
    def __init__(self, serial=None, **kwargs):
        """Initial AndroidDevice
        Args:
            serial: string specify which device

        Returns:
            AndroidDevice object

        Raises:
            EnvironmentError
        """
        self.__display = None
        serial = serial or getenvs('ATX_ADB_SERIALNO', 'ANDROID_SERIAL')
        self._host = kwargs.get('host') or getenvs(
            'ATX_ADB_HOST', 'ANDROID_ADB_SERVER_HOST') or '127.0.0.1'
        self._port = int(
            kwargs.get('port')
            or getenvs('ATX_ADB_PORT', 'ANDROID_ADB_SERVER_PORT') or 5037)

        self._adb_client = adbkit.Client(self._host, self._port)
        self._adb_device = self._adb_client.device(serial)
        self._adb_shell_timeout = 30.0  # max adb shell exec time

        kwargs['adb_server_host'] = kwargs.pop('host', self._host)
        kwargs['adb_server_port'] = kwargs.pop('port', self._port)

        self._uiauto = UiaDevice(serial, **kwargs)
        DeviceMixin.__init__(self)

        self._randid = base.id_generator(5)

        self.screen_rotation = None
        self.screenshot_method = consts.SCREENSHOT_METHOD_AUTO

        # inherts from atx-uiautomator
        self.swipe = self._uiauto.swipe
        self.drag = self._uiauto.drag
        self.press = self._uiauto.press
        self.long_click = self._uiauto.long_click
        self.dump = self._uiauto.dump

    @property
    def info(self):
        return self._uiauto.info

    @property
    def uiautomator(self):
        """
        Returns:
            uiautomator: Device object describes in https://github.com/openatx/atx-uiautomator
        """
        return self._uiauto

    # TODO: not working in java-uiautomator
    # def swipe_points(self, points, steps=100):
    #     """
    #     Args:
    #         points: array of points, eg: [(10, 12), (40, 15)]
    #         steps:  the number of steps for the gesture. Steps are injected about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.

    #     Returns:
    #         a boolean value represents if all touch events for this gesture are injected successfully
    #     """
    #     return self._uiauto.swipePoints(points, steps)

    def __call__(self, *args, **kwargs):
        return self._uiauto(*args, **kwargs)

    @property
    def serial(self):
        """ Android Device Serial Number """
        return self._adb_device.serial

    @property
    def adb_server_host(self):
        return self._host

    @property
    def adb_server_port(self):
        return self._port

    @property
    def adb_device(self):
        return self._adb_device

    @property
    def wlan_ip(self):
        """ Wlan IP """
        return self.adb_shell(['getprop', 'dhcp.wlan0.ipaddress']).strip()

    def forward(self, device_port, local_port=None):
        """Forward device port to local
        Args:
            device_port: port inside device
            local_port: port on PC, if this value is None, a port will random pick one.

        Returns:
            tuple, (host, local_port)
        """
        port = self._adb_device.forward(device_port, local_port)
        return (self._host, port)

    def current_app(self):
        """Get current app (package, activity)
        Returns:
            namedtuple ['package', 'activity', 'pid']
            activity, pid maybe None

        Raises:
            RuntimeError
        """
        AppInfo = collections.namedtuple('AppInfo',
                                         ['package', 'activity', 'pid'])
        try:
            ai = self._adb_device.current_app()
            return AppInfo(ai['package'], ai['activity'], ai.get('pid'))
        except RuntimeError:
            return AppInfo(self.info['currentPackageName'], None, None)

    @property
    def current_package_name(self):
        return self.info['currentPackageName']

    def is_app_alive(self, package_name):
        """ Deprecated: use current_package_name instaed.
        Check if app in running in foreground """
        return self.info['currentPackageName'] == package_name

    def sleep(self, secs=None):
        """Depreciated. use delay instead."""
        if secs is None:
            self._uiauto.sleep()
        else:
            self.delay(secs)

    @property
    def display(self):
        """Virtual keyborad may get small d.info['displayHeight']
        """
        for line in self.adb_shell('dumpsys display').splitlines():
            m = _DISPLAY_RE.search(line, 0)
            if not m:
                continue
            w = int(m.group('width'))
            h = int(m.group('height'))
            return collections.namedtuple('Display', ['width', 'height'])(w, h)
        else:
            w, h = self.info['displayWidth'], self.info['displayHeight']
            return collections.namedtuple('Display', ['width', 'height'])(w, h)

    @property
    def rotation(self):
        """
        Rotaion of the phone

        0: normal
        1: home key on the right
        2: home key on the top
        3: home key on the left
        """
        if self.screen_rotation in range(4):
            return self.screen_rotation
        return self.adb_device.rotation() or self.info['displayRotation']

    @rotation.setter
    def rotation(self, r):
        if not isinstance(r, int):
            raise TypeError("r must be int")
        self.screen_rotation = r

    def _minicap_params(self):
        """
        Used about 0.1s
        uiautomator d.info is now well working with device which has virtual menu.
        """
        rotation = self.rotation

        # rotation not working on SumSUNG 9502
        return '{x}x{y}@{x}x{y}/{r}'.format(x=self.display.width,
                                            y=self.display.height,
                                            r=rotation * 90)

    def _mktemp(self, suffix='.jpg'):
        prefix = 'atx-tmp-{}-'.format(uuid.uuid1())
        return tempfile.mktemp(prefix=prefix, suffix='.jpg')

    def _screenshot_minicap(self):
        phone_tmp_file = '/data/local/tmp/_atx_screen-{}.jpg'.format(
            self._randid)
        local_tmp_file = self._mktemp()
        command = 'LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -P {} -s > {}'.format(
            self._minicap_params(), phone_tmp_file)
        try:
            self.adb_shell(command)
            self.adb_cmd(['pull', phone_tmp_file, local_tmp_file])
            image = imutils.open_as_pillow(local_tmp_file)
            # Fix rotation not rotate right.
            (width, height) = image.size
            if self.screen_rotation in [1, 3] and width < height:
                image = image.rotate(90, Image.BILINEAR, expand=True)
            return image
        except IOError:
            raise IOError("Screenshot use minicap failed.")
        finally:
            self.adb_shell(['rm', phone_tmp_file])
            base.remove_force(local_tmp_file)

    def _screenshot_uiauto(self):
        tmp_file = self._mktemp()
        self._uiauto.screenshot(tmp_file)
        try:
            return imutils.open_as_pillow(tmp_file)
        except IOError:
            raise IOError("Screenshot use uiautomator failed.")
        finally:
            base.remove_force(tmp_file)

    # @hook_wrap(consts.EVENT_CLICK)
    def do_tap(self, x, y):
        """
        Touch specify position

        Args:
            x, y: int

        Returns:
            None
        """
        return self._uiauto.click(x, y)

    def _take_screenshot(self):
        screen = None
        if self.screenshot_method == consts.SCREENSHOT_METHOD_UIAUTOMATOR:
            screen = self._screenshot_uiauto()
        elif self.screenshot_method == consts.SCREENSHOT_METHOD_MINICAP:
            screen = self._screenshot_minicap()
        elif self.screenshot_method == consts.SCREENSHOT_METHOD_AUTO:
            try:
                screen = self._screenshot_minicap()
                self.screenshot_method = consts.SCREENSHOT_METHOD_MINICAP
            except IOError:
                screen = self._screenshot_uiauto()
                self.screenshot_method = consts.SCREENSHOT_METHOD_UIAUTOMATOR
        else:
            raise TypeError('Invalid screenshot_method')
        return screen

    def raw_cmd(self, *args, **kwargs):
        '''
        Return subprocess.Process instance
        '''
        return self.adb_device.raw_cmd(*args, **kwargs)

    def adb_cmd(self, command, **kwargs):
        '''
        Run adb command, for example: adb(['pull', '/data/local/tmp/a.png'])

        Args:
            command: string or list of string

        Returns:
            command output
        '''
        kwargs['timeout'] = kwargs.get('timeout', self._adb_shell_timeout)
        if isinstance(command, list) or isinstance(command, tuple):
            return self.adb_device.run_cmd(*list(command), **kwargs)
        return self.adb_device.run_cmd(command, **kwargs)

    def adb_shell(self, command, **kwargs):
        '''
        Run adb shell command

        Args:
            command: string or list of string

        Returns:
            command output
        '''
        if isinstance(command, list) or isinstance(command, tuple):
            return self.adb_cmd(['shell'] + list(command), **kwargs)
        else:
            return self.adb_cmd(['shell'] + [command], **kwargs)

    @property
    def properties(self):
        '''
        Android Properties, extracted from `adb shell getprop`

        Returns:
            dict of props, for
            example:

                {'ro.bluetooth.dun': 'true'}
        '''
        props = {}
        for line in self.adb_shell(['getprop']).splitlines():
            m = _PROP_PATTERN.match(line)
            if m:
                props[m.group('key')] = m.group('value')
        return props

    def start_app(self, package_name, activity=None, stop=False):
        '''
        Start application

        Args:
            - package_name (string): like com.example.app1
            - activity (string): optional, activity name

        Returns time used (unit second), if activity is not None

        Document: usage: adb shell am start
            -D: enable debugging
            -W: wait for launch to complete
            --start-profiler <FILE>: start profiler and send results to <FILE>
            --sampling INTERVAL: use sample profiling with INTERVAL microseconds
                between samples (use with --start-profiler)
            -P <FILE>: like above, but profiling stops when app goes idle
            -R: repeat the activity launch <COUNT> times.  Prior to each repeat,
                the top activity will be finished.
            -S: force stop the target app before starting the activity
            --opengl-trace: enable tracing of OpenGL functions
            --user <USER_ID> | current: Specify which user to run as; if not
                specified then run as the current user.
        '''
        _pattern = re.compile(r'TotalTime: (\d+)')
        if activity is None:
            self.adb_shell([
                'monkey', '-p', package_name, '-c',
                'android.intent.category.LAUNCHER', '1'
            ])
        else:
            args = ['-W']
            if stop:
                args.append('-S')
            output = self.adb_shell(
                ['am', 'start'] + args +
                ['-n', '%s/%s' % (package_name, activity)])
            m = _pattern.search(output)
            if m:
                return int(m.group(1)) / 1000.0

    def stop_app(self, package_name, clear=False):
        '''
        Stop application

        Args:
            package_name: string like com.example.app1
            clear: bool, remove user data

        Returns:
            None
        '''
        if clear:
            self.adb_shell(['pm', 'clear', package_name])
        else:
            self.adb_shell(['am', 'force-stop', package_name])
        return self

    def takeSnapshot(self, filename):
        '''
        Deprecated, use screenshot instead.
        '''
        warnings.warn("deprecated, use snapshot instead", DeprecationWarning)
        return self.screenshot(filename)

    def _parse_xml_node(self, node):
        # ['bounds', 'checkable', 'class', 'text', 'resource_id', 'package']
        __alias = {
            'class': 'class_name',
            'resource-id': 'resource_id',
            'content-desc': 'content_desc',
            'long-clickable': 'long_clickable',
        }

        def parse_bounds(text):
            m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', text)
            if m is None:
                return None
            return Bounds(*map(int, m.groups()))

        def str2bool(v):
            return v.lower() in ("yes", "true", "t", "1")

        def convstr(v):
            return v.encode('utf-8')

        parsers = {
            'bounds': parse_bounds,
            'text': convstr,
            'class_name': convstr,
            'resource_id': convstr,
            'package': convstr,
            'checkable': str2bool,
            'scrollable': str2bool,
            'focused': str2bool,
            'clickable': str2bool,
            'enabled': str2bool,
            'selected': str2bool,
            'long_clickable': str2bool,
            'focusable': str2bool,
            'password': str2bool,
            'index': int,
            'content_desc': convstr,
        }
        ks = {}
        for key, value in node.attributes.items():
            key = __alias.get(key, key)
            f = parsers.get(key)
            if value is None:
                ks[key] = None
            elif f:
                ks[key] = f(value)
        for key in parsers.keys():
            ks[key] = ks.get(key)
        ks['xml'] = node

        return UINode(**ks)

    def dump_nodes(self):
        """Dump current screen UI to list
        Returns:
            List of UINode object, For
            example:

            [UINode(
                bounds=Bounds(left=0, top=0, right=480, bottom=168),
                checkable=False,
                class_name='android.view.View',
                text='',
                resource_id='',
                package='com.sonyericsson.advancedwidget.clock')]
        """
        xmldata = self._uiauto.dump()
        dom = xml.dom.minidom.parseString(xmldata.encode('utf-8'))
        root = dom.documentElement
        nodes = root.getElementsByTagName('node')
        ui_nodes = []
        for node in nodes:
            ui_nodes.append(self._parse_xml_node(node))
        return ui_nodes

    def dump_view(self):
        """Current Page XML
        """
        warnings.warn("deprecated, source() instead", DeprecationWarning)
        return self._uiauto.dump()

    def source(self, *args, **kwargs):
        """
        Dump page xml
        """
        return self._uiauto.dump(*args, **kwargs)

    def _escape_text(self, s, utf7=False):
        s = s.replace(' ', '%s')
        if utf7:
            s = s.encode('utf-7')
        return s

    def keyevent(self, keycode):
        """call adb shell input keyevent ${keycode}

        Args:
            - keycode(string): for example, KEYCODE_ENTER

        keycode need reference:
        http://developer.android.com/reference/android/view/KeyEvent.html
        """
        self.adb_shell(['input', 'keyevent', keycode])

    def enable_ime(self, ime):
        """
        Enable input methods

        Args:
            - ime(string): for example "android.unicode.ime/.Utf7ImeService"

        Raises:
            RuntimeError
        """
        self.adb_shell(['ime', 'enable', ime])
        self.adb_shell(['ime', 'set', ime])

        from_time = time.time()
        while time.time() - from_time < 10.0:
            if ime == self.current_ime(
            ):  # and self._adb_device.is_keyboard_shown():
                return
            time.sleep(0.2)
        else:
            raise RuntimeError("Error switch to input-method (%s)." % ime)

    def _is_utf7ime(self, ime=None):
        if ime is None:
            ime = self.current_ime()
        return ime in [
            'android.unicode.ime/.Utf7ImeService',
            'com.netease.atx.assistant/.ime.Utf7ImeService',
            'com.netease.nie.yosemite/.ime.ImeService'
        ]

    def prepare_ime(self):
        """
        Change current method to adb-keyboard

        Raises:
            RuntimeError
        """
        if self._is_utf7ime():
            return True

        for ime in self.input_methods():
            if self._is_utf7ime(ime):
                self.enable_ime(ime)
                return True
        return False
        # raise RuntimeError("Input method for programers not detected.\n" +
        #     "\tInstall with: python -m atx install atx-assistant")

    def _shell_type(self, text):
        first = True
        for s in text.split('%s'):
            if first:
                first = False
            else:
                self.adb_shell(['input', 'text', '%'])
                s = 's' + s
            if s == '':
                continue
            estext = self._escape_text(s)
            self.adb_shell(['input', 'text', estext])

    def type(self,
             s,
             enter=False,
             next=False,
             clear=False,
             **ui_select_kwargs):
        """Input some text, this method has been tested not very stable on some device.
        "Hi world" maybe spell into "H iworld"

        Args:
            - s: string (text to input), better to be unicode
            - enter(bool): input enter at last
            - next(bool): perform editor action Next
            - clear(bool): clear text before type
            - ui_select_kwargs(**): tap then type

        The android source code show that
        space need to change to %s
        insteresting thing is that if want to input %s, it is really unconvinent.
        android source code can be found here.
        https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r1/cmds/input/src/com/android/commands/input/Input.java#159
        app source see here: https://github.com/openatx/android-unicode
        """
        if ui_select_kwargs:
            ui_object = self(**ui_select_kwargs)
            ui_object.click()

        if clear:
            self.clear_text()

        utext = strutils.decode(s)
        if self.prepare_ime():
            estext = base64.b64encode(utext.encode('utf-7'))
            self.adb_shell([
                'am', 'broadcast', '-a', 'ADB_INPUT_TEXT', '--es', 'format',
                'base64', '--es', 'msg', estext
            ])
        else:
            self._shell_type(utext)

        if enter:
            self.keyevent('KEYCODE_ENTER')
        if next:
            # FIXME(ssx): maybe KEYCODE_NAVIGATE_NEXT
            self.adb_shell([
                'am', 'broadcast', '-a', 'ADB_EDITOR_CODE', '--ei', 'code', '5'
            ])

    def clear_text(self, count=100):
        """Clear text
        Args:
            - count (int): send KEY_DEL count
        """
        self.prepare_ime()
        self.keyevent('KEYCODE_MOVE_END')
        self.adb_shell([
            'am', 'broadcast', '-a', 'ADB_INPUT_CODE', '--ei', 'code', '67',
            '--ei', 'repeat',
            str(count)
        ])

    def input_methods(self):
        """
        Get all input methods

        Return example: ['com.sohu.inputmethod.sogou/.SogouIME', 'android.unicode.ime/.Utf7ImeService']
        """
        imes = []
        for line in self.adb_shell(['ime', 'list', '-s', '-a']).splitlines():
            line = line.strip()
            if re.match('^.+/.+$', line):
                imes.append(line)
        return imes

    def current_ime(self):
        ''' Get current input method '''
        dumpout = self.adb_shell(['dumpsys', 'input_method'])
        m = _INPUT_METHOD_RE.search(dumpout)
        if m:
            return m.group(1)
Ejemplo n.º 7
0
    def setup_device(self) -> bool:
        """
        Sets up device through Android Debug Bridge in local port 5037.
        Returns whether or not successful. Currently only supports 16x9
        aspect ratios.
        """
        adb = Client(host='127.0.0.1', port=5037)
        devices = adb.devices()
    
        if len(devices) == 0:
            return False

        device = devices[0]

            # Configure movements based on resolution.
        res_str = device.shell('wm size').replace('Physical size: ', '')
        res_width, res_height = list(map(int, res_str.split('x')))

        # Account for status bar/nav bar.
        navbar_str = device.shell(
            "dumpsys window windows | sed -n '/Window .*NavigationBar.*" \
            ":/,/Window .*:/p' | grep 'Requested'"
            )

        statusbar_str = device.shell(
            "dumpsys window windows | sed -n '/Window .*StatusBar.*" \
            ":/,/Window .*:/p' | grep 'Requested'"
            )

        nav_match = re.search('h=(\d+)', navbar_str)
        navbar_height = int(nav_match.group(1)) if nav_match else 0

        status_match = re.search('h=(\d+)', statusbar_str)
        statusbar_height = int(status_match.group(1)) if status_match else 0

        # Adjust res width/height to fit actually usable screen.
        usable_res_height = res_height - statusbar_height - navbar_height

        # PAD uses black bars to pad screen in order to fit aspect ratio.
        # Calculate the height of the bars so that we can locate lower left corner.
        game_res_height = (res_width * self.height_ratio) // self.width_ratio
        bar_height = (usable_res_height - game_res_height)

        bottom_y = res_height - (navbar_height + bar_height)

        # Calculate radius of orbs and how much to shift when moving.
        radius = (res_width // self.board_cols) // 2
        change = (res_width) // self.board_cols

        top_y = bottom_y - self.board_rows * change 

        # Create 2d array of actual coordinates.
        coordinates = \
        [
            [
                (radius + col * change, top_y + radius + row * change)
                for col in range(self.board_cols)
            ] 
            for row in range(self.board_rows)
        ]

        # Set private variables.
        self.coordinates = coordinates
        self.device = AutomatorDevice()

        # For PAD board dimensions/coordinates
        self.bottom = bottom_y
        self.top = top_y
        self.left = 0
        self.right = res_width

        return True
Ejemplo n.º 8
0
class Interface:
    def __init__(
        self,
        board_rows: int = 5,
        board_cols: int = 6,
        width_ratio: int = 9,
        height_ratio: int = 16,
        swipe_ms: int = 50
    ) -> None:
        """
        Args:
            `board_rows`: number of rows for PAD board.
            `board_cols`: number of columns for PAD board.
            `width_ratio`: in-game aspect ratio for width.
            `height_ratio`: in-game aspect ratio for height.
            `swipe_ms`: time in ms to move one orb to another location.
        """

        # Info for configuration.
        self.board_rows = board_rows
        self.board_cols = board_cols
        self.width_ratio = width_ratio
        self.height_ratio = height_ratio
        self.ms = swipe_ms

        # Device specific info.
        self.device = None
        self.coordinates = None

        self.top = None
        self.right = None
        self.left = None
        self.bottom = None


    def setup_device(self) -> bool:
        """
        Sets up device through Android Debug Bridge in local port 5037.
        Returns whether or not successful. Currently only supports 16x9
        aspect ratios.
        """
        adb = Client(host='127.0.0.1', port=5037)
        devices = adb.devices()
    
        if len(devices) == 0:
            return False

        device = devices[0]

            # Configure movements based on resolution.
        res_str = device.shell('wm size').replace('Physical size: ', '')
        res_width, res_height = list(map(int, res_str.split('x')))

        # Account for status bar/nav bar.
        navbar_str = device.shell(
            "dumpsys window windows | sed -n '/Window .*NavigationBar.*" \
            ":/,/Window .*:/p' | grep 'Requested'"
            )

        statusbar_str = device.shell(
            "dumpsys window windows | sed -n '/Window .*StatusBar.*" \
            ":/,/Window .*:/p' | grep 'Requested'"
            )

        nav_match = re.search('h=(\d+)', navbar_str)
        navbar_height = int(nav_match.group(1)) if nav_match else 0

        status_match = re.search('h=(\d+)', statusbar_str)
        statusbar_height = int(status_match.group(1)) if status_match else 0

        # Adjust res width/height to fit actually usable screen.
        usable_res_height = res_height - statusbar_height - navbar_height

        # PAD uses black bars to pad screen in order to fit aspect ratio.
        # Calculate the height of the bars so that we can locate lower left corner.
        game_res_height = (res_width * self.height_ratio) // self.width_ratio
        bar_height = (usable_res_height - game_res_height)

        bottom_y = res_height - (navbar_height + bar_height)

        # Calculate radius of orbs and how much to shift when moving.
        radius = (res_width // self.board_cols) // 2
        change = (res_width) // self.board_cols

        top_y = bottom_y - self.board_rows * change 

        # Create 2d array of actual coordinates.
        coordinates = \
        [
            [
                (radius + col * change, top_y + radius + row * change)
                for col in range(self.board_cols)
            ] 
            for row in range(self.board_rows)
        ]

        # Set private variables.
        self.coordinates = coordinates
        self.device = AutomatorDevice()

        # For PAD board dimensions/coordinates
        self.bottom = bottom_y
        self.top = top_y
        self.left = 0
        self.right = res_width

        return True

    def _path_to_coord(
            self,
            path: List[Directions],
            start: Tuple[int, int]
        ) -> List[Tuple[int, int]]:
        """
            Takes in a list of directions and converts it to 
            actual pixel coordinates on the phone. Returns None
            if errored.
        """

        x, y = start
        if x < 0 or x >= self.board_cols or y < 0 or y >= self.board_rows:
            return None
        
        if len(path) < 1:
            return None

        path_coord = []

        for op in path:
            coord = self.coordinates[y][x]
            path_coord.append(deepcopy(coord))

            direction = op.value
            
            # Error if not present.
            if direction is None:
                return None

            # Move x and y.
            x, y = tuple(
                map(lambda src, change: src + change, (x,y), direction)
            )

            if x < 0 or x >= self.board_cols or y < 0 or y >= self.board_rows:
                return None

        # Get the last coordinate.
        path_coord.append(deepcopy(self.coordinates[y][x]))
        return path_coord

    def input_swipes(
            self,
            path: List[Directions],
            start: Tuple[int, int]
        ) -> None:
        """
            Takes in a list of directions and inputs
            the swipes to the phone. Requires that `setup_device`
            has been called.
        """
        path_coord = self._path_to_coord(path, start)

        # If errored out, return None.
        if not path_coord:
            return None

        mod_steps = self.ms // 5
        self.device.swipePoints(path_coord, mod_steps)
    
    def board_screencap(self) -> List[List[Image.Image]]:
        """
            Captures the screen and returns an array
            of Pillow Images that contain the orbs.
        """

        # Try 3 times. If errored, then quit.
        filename = None
        for i in range(3):
            filename = self.device.screenshot(LOCATION)
            if filename is not None:
                break
        
        if filename is None:
            return None

        with Image.open(filename) as im:
            raw_orbs = []

            # Get the specific orb images.
            dx = (self.right - self.left) // self.board_cols
            dy = (self.bottom - self.top) // self.board_rows

            for row in range(self.board_rows):
                orb_row = []
                for col in range(self.board_cols):
                    orb = im.crop((
                        self.left + dx * col,
                        self.top + dy * row,
                        self.left + dx * (col + 1),
                        self.top + dy * (row + 1)
                    ))
                    orb_row.append(orb)
                raw_orbs.append(orb_row)

        # Remove screencap after getting orbs.
        remove(LOCATION)

        return raw_orbs