1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3# You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import absolute_import, print_function
6
7import os
8import re
9import time
10
11from abc import ABCMeta
12
13from . import version_codes
14
15from .adb import ADBDevice, ADBError, ADBRootError
16
17
18class ADBAndroid(ADBDevice):
19    """ADBAndroid implements :class:`ADBDevice` providing Android-specific
20    functionality.
21
22    ::
23
24       from mozdevice import ADBAndroid
25
26       adbdevice = ADBAndroid()
27       print(adbdevice.list_files("/mnt/sdcard"))
28       if adbdevice.process_exist("org.mozilla.fennec"):
29           print("Fennec is running")
30    """
31    __metaclass__ = ABCMeta
32
33    def __init__(self,
34                 device=None,
35                 adb='adb',
36                 adb_host=None,
37                 adb_port=None,
38                 test_root='',
39                 logger_name='adb',
40                 timeout=300,
41                 verbose=False,
42                 device_ready_retry_wait=20,
43                 device_ready_retry_attempts=3):
44        """Initializes the ADBAndroid object.
45
46        :param device: When a string is passed, it is interpreted as the
47            device serial number. This form is not compatible with
48            devices containing a ":" in the serial; in this case
49            ValueError will be raised.
50            When a dictionary is passed it must have one or both of
51            the keys "device_serial" and "usb". This is compatible
52            with the dictionaries in the list returned by
53            ADBHost.devices(). If the value of device_serial is a
54            valid serial not containing a ":" it will be used to
55            identify the device, otherwise the value of the usb key,
56            prefixed with "usb:" is used.
57            If None is passed and there is exactly one device attached
58            to the host, that device is used. If there is more than one
59            device attached, ValueError is raised. If no device is
60            attached the constructor will block until a device is
61            attached or the timeout is reached.
62        :type device: dict, str or None
63        :param adb_host: host of the adb server to connect to.
64        :type adb_host: str or None
65        :param adb_port: port of the adb server to connect to.
66        :type adb_port: integer or None
67        :param str logger_name: logging logger name. Defaults to 'adb'.
68        :param integer device_ready_retry_wait: number of seconds to wait
69            between attempts to check if the device is ready after a
70            reboot.
71        :param integer device_ready_retry_attempts: number of attempts when
72            checking if a device is ready.
73
74        :raises: * ADBError
75                 * ADBTimeoutError
76                 * ValueError
77        """
78        ADBDevice.__init__(self, device=device, adb=adb,
79                           adb_host=adb_host, adb_port=adb_port,
80                           test_root=test_root,
81                           logger_name=logger_name, timeout=timeout,
82                           verbose=verbose,
83                           device_ready_retry_wait=device_ready_retry_wait,
84                           device_ready_retry_attempts=device_ready_retry_attempts)
85        # https://source.android.com/devices/tech/security/selinux/index.html
86        # setenforce
87        # usage:  setenforce [ Enforcing | Permissive | 1 | 0 ]
88        # getenforce returns either Enforcing or Permissive
89
90        try:
91            self.selinux = True
92            if self.shell_output('getenforce', timeout=timeout) != 'Permissive':
93                self._logger.info('Setting SELinux Permissive Mode')
94                self.shell_output("setenforce Permissive", timeout=timeout, root=True)
95        except (ADBError, ADBRootError) as e:
96            self._logger.warning('Unable to set SELinux Permissive due to %s.' % e)
97            self.selinux = False
98
99        self.version = int(self.shell_output("getprop ro.build.version.sdk",
100                                             timeout=timeout))
101
102    def reboot(self, timeout=None):
103        """Reboots the device.
104
105        :param timeout: optional integer specifying the maximum time in
106            seconds for any spawned adb process to complete before
107            throwing an ADBTimeoutError.
108            This timeout is per adb call. The total time spent
109            may exceed this value. If it is not specified, the value
110            set in the ADB constructor is used.
111        :raises: * ADBTimeoutError
112                 * ADBError
113
114        reboot() reboots the device, issues an adb wait-for-device in order to
115        wait for the device to complete rebooting, then calls is_device_ready()
116        to determine if the device has completed booting.
117
118        If the device supports running adbd as root, adbd will be
119        restarted running as root. Then, if the device supports
120        SELinux, setenforce Permissive will be called to change
121        SELinux to permissive. This must be done after adbd is
122        restarted in order for the SELinux Permissive setting to
123        persist.
124
125        """
126        ready = ADBDevice.reboot(self, timeout=timeout)
127        self._check_adb_root(timeout=timeout)
128        return ready
129
130    # Informational methods
131
132    def get_battery_percentage(self, timeout=None):
133        """Returns the battery charge as a percentage.
134
135        :param timeout: The maximum time in
136            seconds for any spawned adb process to complete before
137            throwing an ADBTimeoutError.
138            This timeout is per adb call. The total time spent
139            may exceed this value. If it is not specified, the value
140            set in the ADBDevice constructor is used.
141        :type timeout: integer or None
142        :returns: battery charge as a percentage.
143        :raises: * ADBTimeoutError
144                 * ADBError
145        """
146        level = None
147        scale = None
148        percentage = 0
149        cmd = "dumpsys battery"
150        re_parameter = re.compile(r'\s+(\w+):\s+(\d+)')
151        lines = self.shell_output(cmd, timeout=timeout).splitlines()
152        for line in lines:
153            match = re_parameter.match(line)
154            if match:
155                parameter = match.group(1)
156                value = match.group(2)
157                if parameter == 'level':
158                    level = float(value)
159                elif parameter == 'scale':
160                    scale = float(value)
161                if parameter is not None and scale is not None:
162                    percentage = 100.0 * level / scale
163                    break
164        return percentage
165
166    def get_top_activity(self, timeout=None):
167        """Returns the name of the top activity (focused app) reported by dumpsys
168
169        :param timeout: The maximum time in
170            seconds for any spawned adb process to complete before
171            throwing an ADBTimeoutError.
172            This timeout is per adb call. The total time spent
173            may exceed this value. If it is not specified, the value
174            set in the ADBDevice constructor is used.
175        :type timeout: integer or None
176        :returns: package name of top activity or None (cannot be determined)
177        :raises: * ADBTimeoutError
178                 * ADBError
179        """
180        package = None
181        data = None
182        cmd = "dumpsys window windows"
183        try:
184            data = self.shell_output(cmd, timeout=timeout)
185        except Exception:
186            # dumpsys intermittently fails on some platforms (4.3 arm emulator)
187            return package
188        m = re.search('mFocusedApp(.+)/', data)
189        if not m:
190            # alternative format seen on newer versions of Android
191            m = re.search('FocusedApplication(.+)/', data)
192        if m:
193            line = m.group(0)
194            # Extract package name: string of non-whitespace ending in forward slash
195            m = re.search('(\S+)/$', line)
196            if m:
197                package = m.group(1)
198        if self._verbose:
199            self._logger.debug('get_top_activity: %s' % str(package))
200        return package
201
202    # System control methods
203
204    def is_device_ready(self, timeout=None):
205        """Checks if a device is ready for testing.
206
207        This method uses the android only package manager to check for
208        readiness.
209
210        :param timeout: The maximum time
211            in seconds for any spawned adb process to complete before
212            throwing an ADBTimeoutError.
213            This timeout is per adb call. The total time spent
214            may exceed this value. If it is not specified, the value
215            set in the ADB constructor is used.
216        :type timeout: integer or None
217        :raises: * ADBTimeoutError
218                 * ADBError
219        """
220        # command_output automatically inserts a 'wait-for-device'
221        # argument to adb. Issuing an empty command is the same as adb
222        # -s <device> wait-for-device. We don't send an explicit
223        # 'wait-for-device' since that would add duplicate
224        # 'wait-for-device' arguments which is an error in newer
225        # versions of adb.
226        self.command_output([], timeout=timeout)
227        pm_error_string = "Error: Could not access the Package Manager"
228        pm_list_commands = ["packages", "permission-groups", "permissions",
229                            "instrumentation", "features", "libraries"]
230        ready_path = os.path.join(self.test_root, "ready")
231        for attempt in range(self._device_ready_retry_attempts):
232            failure = 'Unknown failure'
233            success = True
234            try:
235                state = self.get_state(timeout=timeout)
236                if state != 'device':
237                    failure = "Device state: %s" % state
238                    success = False
239                else:
240                    if (self.selinux and self.shell_output('getenforce',
241                                                           timeout=timeout) != 'Permissive'):
242                        self._logger.info('Setting SELinux Permissive Mode')
243                        self.shell_output("setenforce Permissive", timeout=timeout, root=True)
244                    if self.is_dir(ready_path, timeout=timeout):
245                        self.rmdir(ready_path, timeout=timeout)
246                    self.mkdir(ready_path, timeout=timeout)
247                    self.rmdir(ready_path, timeout=timeout)
248                    # Invoke the pm list commands to see if it is up and
249                    # running.
250                    for pm_list_cmd in pm_list_commands:
251                        data = self.shell_output("pm list %s" % pm_list_cmd,
252                                                 timeout=timeout)
253                        if pm_error_string in data:
254                            failure = data
255                            success = False
256                            break
257            except ADBError as e:
258                success = False
259                failure = e.message
260
261            if not success:
262                self._logger.debug('Attempt %s of %s device not ready: %s' % (
263                    attempt + 1, self._device_ready_retry_attempts,
264                    failure))
265                time.sleep(self._device_ready_retry_wait)
266
267        return success
268
269    def power_on(self, timeout=None):
270        """Sets the device's power stayon value.
271
272        :param timeout: The maximum time in
273            seconds for any spawned adb process to complete before
274            throwing an ADBTimeoutError.
275            This timeout is per adb call. The total time spent
276            may exceed this value. If it is not specified, the value
277            set in the ADB constructor is used.
278        :type timeout: integer or None
279        :raises: * ADBTimeoutError
280                 * ADBError
281        """
282        try:
283            self.shell_output('svc power stayon true',
284                              timeout=timeout,
285                              root=True)
286        except ADBError as e:
287            # Executing this via adb shell errors, but not interactively.
288            # Any other exitcode is a real error.
289            if 'exitcode: 137' not in e.message:
290                raise
291            self._logger.warning('Unable to set power stayon true: %s' % e)
292
293    # Application management methods
294
295    def install_app(self, apk_path, replace=False, timeout=None):
296        """Installs an app on the device.
297
298        :param str apk_path: The apk file name to be installed.
299        :param bool replace: If True, replace existing application.
300        :param timeout: The maximum time in
301            seconds for any spawned adb process to complete before
302            throwing an ADBTimeoutError.
303            This timeout is per adb call. The total time spent
304            may exceed this value. If it is not specified, the value
305            set in the ADB constructor is used.
306        :type timeout: integer or None
307        :raises: * ADBTimeoutError
308                 * ADBError
309        """
310        cmd = ["install"]
311        if self.version >= version_codes.M:
312            cmd.append("-g")
313        if replace:
314            cmd.append("-r")
315        cmd.append(apk_path)
316        data = self.command_output(cmd, timeout=timeout)
317        if data.find('Success') == -1:
318            raise ADBError("install failed for %s. Got: %s" %
319                           (apk_path, data))
320
321    def is_app_installed(self, app_name, timeout=None):
322        """Returns True if an app is installed on the device.
323
324        :param str app_name: The name of the app to be checked.
325        :param timeout: The maximum time in
326            seconds for any spawned adb process to complete before
327            throwing an ADBTimeoutError.
328            This timeout is per adb call. The total time spent
329            may exceed this value. If it is not specified, the value
330            set in the ADB constructor is used.
331        :type timeout: integer or None
332        :raises: * ADBTimeoutError
333                 * ADBError
334        """
335        pm_error_string = 'Error: Could not access the Package Manager'
336        data = self.shell_output("pm list package %s" % app_name, timeout=timeout)
337        if pm_error_string in data:
338            raise ADBError(pm_error_string)
339        if app_name not in data:
340            return False
341        return True
342
343    def launch_application(self, app_name, activity_name, intent, url=None,
344                           extras=None, wait=True, fail_if_running=True,
345                           timeout=None):
346        """Launches an Android application
347
348        :param str app_name: Name of application (e.g. `com.android.chrome`)
349        :param str activity_name: Name of activity to launch (e.g. `.Main`)
350        :param str intent: Intent to launch application with
351        :param url: URL to open
352        :type url: str or None
353        :param extras: Extra arguments for application.
354        :type extras: dict or None
355        :param bool wait: If True, wait for application to start before
356            returning.
357        :param bool fail_if_running: Raise an exception if instance of
358            application is already running.
359        :param timeout: The maximum time in
360            seconds for any spawned adb process to complete before
361            throwing an ADBTimeoutError.
362            This timeout is per adb call. The total time spent
363            may exceed this value. If it is not specified, the value
364            set in the ADB constructor is used.
365        :type timeout: integer or None
366        :raises: * ADBTimeoutError
367                 * ADBError
368        """
369        # If fail_if_running is True, we throw an exception here. Only one
370        # instance of an application can be running at once on Android,
371        # starting a new instance may not be what we want depending on what
372        # we want to do
373        if fail_if_running and self.process_exist(app_name, timeout=timeout):
374            raise ADBError("Only one instance of an application may be running "
375                           "at once")
376
377        acmd = ["am", "start"] + \
378            ["-W" if wait else '', "-n", "%s/%s" % (app_name, activity_name)]
379
380        if intent:
381            acmd.extend(["-a", intent])
382
383        if extras:
384            for (key, val) in extras.iteritems():
385                if isinstance(val, int):
386                    extra_type_param = "--ei"
387                elif isinstance(val, bool):
388                    extra_type_param = "--ez"
389                else:
390                    extra_type_param = "--es"
391                acmd.extend([extra_type_param, str(key), str(val)])
392
393        if url:
394            acmd.extend(["-d", url])
395
396        cmd = self._escape_command_line(acmd)
397        self.shell_output(cmd, timeout=timeout)
398
399    def launch_fennec(self, app_name, intent="android.intent.action.VIEW",
400                      moz_env=None, extra_args=None, url=None, wait=True,
401                      fail_if_running=True, timeout=None):
402        """Convenience method to launch Fennec on Android with various
403        debugging arguments
404
405        :param str app_name: Name of fennec application (e.g.
406            `org.mozilla.fennec`)
407        :param str intent: Intent to launch application.
408        :param moz_env: Mozilla specific environment to pass into
409            application.
410        :type moz_env: str or None
411        :param extra_args: Extra arguments to be parsed by fennec.
412        :type extra_args: str or None
413        :param url: URL to open
414        :type url: str or None
415        :param bool wait: If True, wait for application to start before
416            returning.
417        :param bool fail_if_running: Raise an exception if instance of
418            application is already running.
419        :param timeout: The maximum time in
420            seconds for any spawned adb process to complete before
421            throwing an ADBTimeoutError.
422            This timeout is per adb call. The total time spent
423            may exceed this value. If it is not specified, the value
424            set in the ADB constructor is used.
425        :type timeout: integer or None
426        :raises: * ADBTimeoutError
427                 * ADBError
428        """
429        extras = {}
430
431        if moz_env:
432            # moz_env is expected to be a dictionary of environment variables:
433            # Fennec itself will set them when launched
434            for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()):
435                extras["env" + str(env_count)] = env_key + "=" + env_val
436
437        # Additional command line arguments that fennec will read and use (e.g.
438        # with a custom profile)
439        if extra_args:
440            extras['args'] = " ".join(extra_args)
441
442        self.launch_application(app_name, ".App",
443                                intent, url=url, extras=extras,
444                                wait=wait, fail_if_running=fail_if_running,
445                                timeout=timeout)
446
447    def stop_application(self, app_name, timeout=None, root=False):
448        """Stops the specified application
449
450        For Android 3.0+, we use the "am force-stop" to do this, which
451        is reliable and does not require root. For earlier versions of
452        Android, we simply try to manually kill the processes started
453        by the app repeatedly until none is around any more. This is
454        less reliable and does require root.
455
456        :param str app_name: Name of application (e.g. `com.android.chrome`)
457        :param timeout: The maximum time in
458            seconds for any spawned adb process to complete before
459            throwing an ADBTimeoutError.
460            This timeout is per adb call. The total time spent
461            may exceed this value. If it is not specified, the value
462            set in the ADB constructor is used.
463        :type timeout: integer or None
464        :param bool root: Flag specifying if the command should be
465            executed as root.
466        :raises: * ADBTimeoutError
467                 * ADBError
468        """
469        if self.version >= version_codes.HONEYCOMB:
470            self.shell_output("am force-stop %s" % app_name,
471                              timeout=timeout, root=root)
472        else:
473            num_tries = 0
474            max_tries = 5
475            while self.process_exist(app_name, timeout=timeout):
476                if num_tries > max_tries:
477                    raise ADBError("Couldn't successfully kill %s after %s "
478                                   "tries" % (app_name, max_tries))
479                self.pkill(app_name, timeout=timeout, root=root)
480                num_tries += 1
481
482                # sleep for a short duration to make sure there are no
483                # additional processes in the process of being launched
484                # (this is not 100% guaranteed to work since it is inherently
485                # racey, but it's the best we can do)
486                time.sleep(1)
487
488    def uninstall_app(self, app_name, reboot=False, timeout=None):
489        """Uninstalls an app on the device.
490
491        :param str app_name: The name of the app to be
492            uninstalled.
493        :param bool reboot: Flag indicating that the device should
494            be rebooted after the app is uninstalled. No reboot occurs
495            if the app is not installed.
496        :param timeout: The maximum time in
497            seconds for any spawned adb process to complete before
498            throwing an ADBTimeoutError.
499            This timeout is per adb call. The total time spent
500            may exceed this value. If it is not specified, the value
501            set in the ADB constructor is used.
502        :type timeout: integer or None
503        :raises: * ADBTimeoutError
504                 * ADBError
505        """
506        if self.is_app_installed(app_name, timeout=timeout):
507            data = self.command_output(["uninstall", app_name], timeout=timeout)
508            if data.find('Success') == -1:
509                self._logger.debug('uninstall_app failed: %s' % data)
510                raise ADBError("uninstall failed for %s. Got: %s" % (app_name, data))
511            if reboot:
512                self.reboot(timeout=timeout)
513
514    def update_app(self, apk_path, timeout=None):
515        """Updates an app on the device and reboots.
516
517        :param str apk_path: The apk file name to be
518            updated.
519        :param timeout: The maximum time in
520            seconds for any spawned adb process to complete before
521            throwing an ADBTimeoutError.
522            This timeout is per adb call. The total time spent
523            may exceed this value. If it is not specified, the value
524            set in the ADB constructor is used.
525        :type timeout: integer or None
526        :raises: * ADBTimeoutError
527                 * ADBError
528        """
529        cmd = ["install", "-r"]
530        if self.version >= version_codes.M:
531            cmd.append("-g")
532        cmd.append(apk_path)
533        output = self.command_output(cmd, timeout=timeout)
534        self.reboot(timeout=timeout)
535        return output
536