xref: /aosp_15_r20/external/autotest/client/common_lib/cros/arc.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import collections
11import glob
12import logging
13import os
14import pipes
15import shutil
16import socket
17import sys
18import tempfile
19import time
20
21from autotest_lib.client.bin import test, utils
22from autotest_lib.client.common_lib import error
23from autotest_lib.client.common_lib.cros import chrome, arc_common
24from six.moves import range
25
26_ADB_KEYS_PATH = '/tmp/adb_keys'
27_ADB_VENDOR_KEYS = 'ADB_VENDOR_KEYS'
28_ANDROID_CONTAINER_PID_PATH = '/run/containers/android*/container.pid'
29_ANDROID_DATA_ROOT_PATH = '/opt/google/containers/android/rootfs/android-data'
30_ANDROID_CONTAINER_ROOT_PATH = '/opt/google/containers/android/rootfs'
31_SCREENSHOT_DIR_PATH = '/var/log/arc-screenshots'
32_SCREENSHOT_BASENAME = 'arc-screenshot'
33_MAX_SCREENSHOT_NUM = 10
34# This address should match the one present in
35# https://chromium.googlesource.com/chromiumos/overlays/chromiumos-overlay/+/main/chromeos-base/arc-sslh-init/files/sslh.conf
36_ADBD_ADDRESS = ('100.115.92.2', 5555)
37_ADBD_PID_PATH = '/run/arc/adbd.pid'
38_SDCARD_PID_PATH = '/run/arc/sdcard.pid'
39_ANDROID_ADB_KEYS_PATH = '/data/misc/adb/adb_keys'
40_PROCESS_CHECK_INTERVAL_SECONDS = 1
41_PROPERTY_CHECK_INTERVAL_SECONDS = 1
42_WAIT_FOR_ADB_READY = 60
43_WAIT_FOR_ANDROID_PROCESS_SECONDS = 60
44_PLAY_STORE_PKG = 'com.android.vending'
45_SETTINGS_PKG = 'com.android.settings'
46
47
48def setup_adb_host():
49    """Setup ADB host keys.
50
51    This sets up the files and environment variables that wait_for_adb_ready()
52    needs"""
53    if _ADB_VENDOR_KEYS in os.environ:
54        return
55    if not os.path.exists(_ADB_KEYS_PATH):
56        os.mkdir(_ADB_KEYS_PATH)
57    # adb expects $HOME to be writable.
58    os.environ['HOME'] = _ADB_KEYS_PATH
59
60    # Generate and save keys for adb if needed
61    key_path = os.path.join(_ADB_KEYS_PATH, 'test_key')
62    if not os.path.exists(key_path):
63        utils.system('adb keygen ' + pipes.quote(key_path))
64    os.environ[_ADB_VENDOR_KEYS] = key_path
65
66
67def restart_adbd(timeout):
68    """Restarts the adb daemon.
69
70    Follows the same logic as tast.
71    """
72    logging.debug('restarting adbd')
73    config = 'adb'
74    _android_shell('setprop persist.sys.usb.config ' + config)
75    _android_shell('setprop sys.usb.config ' + config)
76
77    def property_check():
78        return _android_shell('getprop sys.usb.state') == config
79
80    try:
81        utils.poll_for_condition(
82                condition=property_check,
83                desc='Wait for sys.usb.state',
84                timeout=timeout,
85                sleep_interval=_PROPERTY_CHECK_INTERVAL_SECONDS)
86    except utils.TimeoutError:
87        raise error.TestFail('Timed out waiting for sys.usb.state change')
88
89    _android_shell('setprop ctl.restart adbd')
90
91
92def restart_adb():
93    """Restarts adb.
94
95    Follows the same logic as in tast, specifically avoiding kill-server
96    since it is unreliable (crbug.com/855325).
97    """
98    logging.debug('killing and restarting adb server')
99    utils.system('killall --quiet --wait -KILL adb')
100    utils.system('adb start-server')
101
102
103def is_adb_connected():
104    """Return true if adb is connected to the container."""
105    output = utils.system_output('adb get-state', ignore_status=True)
106    logging.debug('adb get-state: %s', output)
107    return output.strip() == 'device'
108
109
110def _is_android_data_mounted():
111    """Return true if Android's /data is mounted with partial boot enabled."""
112    return _android_shell('getprop ro.data_mounted', ignore_status=True) == '1'
113
114
115def get_zygote_type():
116    """Return zygote service type."""
117    return _android_shell('getprop ro.zygote', ignore_status=True)
118
119
120def get_sdk_version():
121    """Return the SDK level version for Android."""
122    return _android_shell('getprop ro.build.version.sdk')
123
124
125def get_product():
126    """Return the product string used for the Android build."""
127    return _android_shell('getprop ro.build.product', ignore_status=True)
128
129
130def _is_tcp_port_reachable(address):
131    """Return whether a TCP port described by |address| is reachable."""
132    try:
133        s = socket.create_connection(address)
134        s.close()
135        return True
136    except socket.error:
137        return False
138
139
140def _wait_for_data_mounted(timeout):
141    utils.poll_for_condition(
142            condition=_is_android_data_mounted,
143            desc='Wait for /data mounted',
144            timeout=timeout,
145            sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS)
146
147
148def wait_for_adb_ready(timeout=_WAIT_FOR_ADB_READY):
149    """Wait for the ADB client to connect to the ARC container.
150
151    @param timeout: Timeout in seconds.
152    """
153    # Although adbd is started at login screen, we still need /data to be
154    # mounted to set up key-based authentication. /data should be mounted
155    # once the user has logged in.
156
157    initial_timeout = timeout
158
159    start_time = time.time()
160    _wait_for_data_mounted(timeout)
161    timeout -= (time.time() - start_time)
162    start_time = time.time()
163    arc_common.wait_for_android_boot(timeout)
164    timeout -= (time.time() - start_time)
165
166    setup_adb_host()
167    if is_adb_connected():
168        return
169
170    # Push keys for adb.
171    pubkey_path = os.environ[_ADB_VENDOR_KEYS] + '.pub'
172    with open(pubkey_path, 'r') as f:
173        _write_android_file(_ANDROID_ADB_KEYS_PATH, f.read())
174    _android_shell('chown shell ' + pipes.quote(_ANDROID_ADB_KEYS_PATH))
175    _android_shell('restorecon ' + pipes.quote(_ANDROID_ADB_KEYS_PATH))
176
177    attempt_count = 3
178    timeout = timeout / attempt_count
179
180    for i in range(attempt_count):
181        if _restart_adb_and_wait_for_ready(timeout):
182            return
183    raise error.TestFail(
184            'Failed to connect to adb in %d seconds.' % initial_timeout)
185
186
187def _restart_adb_and_wait_for_ready(timeout):
188    """Restart adb/adbd and wait adb connection is ready.
189
190    @param timeout: Timeout in seconds.
191    @return True in case adb connection was established or throw an error in
192            case persistent error occured.
193    """
194
195    # Restart adbd and adb.
196    start_time = time.time()
197    restart_adbd(timeout)
198    timeout -= (time.time() - start_time)
199    start_time = time.time()
200    restart_adb()
201    timeout -= (time.time() - start_time)
202
203    try:
204        utils.poll_for_condition(condition=is_adb_connected,
205                                 timeout=timeout)
206        return True
207    except (utils.TimeoutError):
208        # The operation has failed, but let's try to clarify the failure to
209        # avoid shifting blame to adb.
210
211        # First, collect some information and log it.
212        arc_alive = is_android_container_alive()
213        arc_booted = _android_shell('getprop ro.arc.boot_completed',
214                                    ignore_status=True)
215        arc_system_events = _android_shell(
216            'logcat -d -b events *:S arc_system_event', ignore_status=True)
217        adbd_pid = _android_shell('pidof -s adbd', ignore_status=True)
218        adbd_port_reachable = _is_tcp_port_reachable(_ADBD_ADDRESS)
219        adb_state = utils.system_output('adb get-state', ignore_status=True)
220        logging.debug('ARC alive: %s', arc_alive)
221        logging.debug('ARC booted: %s', arc_booted)
222        logging.debug('ARC system events: %s', arc_system_events)
223        logging.debug('adbd process: %s', adbd_pid)
224        logging.debug('adbd port reachable: %s', adbd_port_reachable)
225        logging.debug('adb state: %s', adb_state)
226
227        # Now go through the usual suspects and raise nicer errors to make the
228        # actual failure clearer.
229        if not arc_alive:
230            raise error.TestFail('ARC is not alive.')
231        if arc_booted != '1':
232            raise error.TestFail('ARC did not finish booting.')
233        return False
234
235
236def grant_permissions(package, permissions):
237    """Grants permissions to a package.
238
239    @param package: Package name.
240    @param permissions: A list of permissions.
241
242    """
243    for permission in permissions:
244        adb_shell('pm grant %s android.permission.%s' % (
245                  pipes.quote(package), pipes.quote(permission)))
246
247
248def adb_cmd(cmd, **kwargs):
249    """Executed cmd using adb. Must wait for adb ready.
250
251    @param cmd: Command to run.
252    """
253    # TODO(b/79122489) - Assert if cmd == 'root'
254    wait_for_adb_ready()
255    return utils.system_output('adb %s' % cmd, **kwargs)
256
257
258def adb_shell(cmd, **kwargs):
259    """Executed shell command with adb.
260
261    @param cmd: Command to run.
262    """
263    output = adb_cmd('shell %s' % pipes.quote(cmd), **kwargs)
264    # Some android commands include a trailing CRLF in their output.
265    if kwargs.pop('strip_trailing_whitespace', True):
266        output = output.rstrip()
267    return output
268
269
270def adb_install(apk, auto_grant_permissions=True, ignore_status=False):
271    """Install an apk into container. You must connect first.
272
273    @param apk: Package to install.
274    @param auto_grant_permissions: Set to false to not automatically grant all
275    permissions. Most tests should not care.
276    @param ignore_status: Set to true to allow the install command to fail,
277    for example if you are installing multiple architectures and only need
278    one to succeed.
279    """
280    flags = '-g' if auto_grant_permissions else ''
281    return adb_cmd('install -r -t %s %s' % (flags, apk),
282                   timeout=60*5,
283                   ignore_status=ignore_status)
284
285
286def adb_uninstall(apk):
287    """Remove an apk from container. You must connect first.
288
289    @param apk: Package to uninstall.
290    """
291    return adb_cmd('uninstall %s' % apk)
292
293
294def adb_reboot():
295    """Reboots the container and block until container pid is gone.
296
297    You must connect first.
298    """
299    old_pid = get_container_pid()
300    logging.info('Trying to reboot PID:%s', old_pid)
301    adb_cmd('reboot', ignore_status=True)
302    # Ensure that the old container is no longer booted
303    utils.poll_for_condition(
304        lambda: not utils.pid_is_alive(int(old_pid)), timeout=10)
305
306
307# This adb_root() function is deceiving in that it works just fine on debug
308# builds of ARC (user-debug, eng). However "adb root" does not work on user
309# builds as run by the autotest machines when testing prerelease images. In fact
310# it will silently fail. You will need to find another way to do do what you
311# need to do as root.
312#
313# TODO(b/79122489) - Remove this function.
314def adb_root():
315    """Restart adbd with root permission."""
316
317    adb_cmd('root')
318
319
320def get_container_root():
321    """Returns path to Android container root directory."""
322    return _ANDROID_CONTAINER_ROOT_PATH
323
324
325def get_container_pid_path():
326    """Returns the container's PID file path.
327
328    Raises:
329      TestError if no PID file is found, or more than one files are found.
330    """
331    # Find the PID file rather than the android-XXXXXX/ directory to ignore
332    # stale and empty android-XXXXXX/ directories when they exist.
333    arc_container_pid_files = glob.glob(_ANDROID_CONTAINER_PID_PATH)
334
335    if len(arc_container_pid_files) == 0:
336        raise error.TestError('Android container.pid not available')
337
338    if len(arc_container_pid_files) > 1:
339        raise error.TestError(
340                'Multiple Android container.pid files found: %r. '
341                'Reboot your DUT to recover.' % (arc_container_pid_files))
342
343    return arc_container_pid_files[0]
344
345
346def get_android_data_root():
347    """Returns path to ChromeOS directory that bind-mounts Android's /data."""
348    return _ANDROID_DATA_ROOT_PATH
349
350
351def get_container_pid():
352    """Returns the PID of the container."""
353    return utils.read_one_line(get_container_pid_path())
354
355
356def get_adbd_pid():
357    """Returns the PID of the adbd proxy container."""
358    if not os.path.exists(_ADBD_PID_PATH):
359        # The adbd proxy does not run on all boards.
360        return None
361    return utils.read_one_line(_ADBD_PID_PATH)
362
363
364def is_android_process_running(process_name):
365    """Return whether Android has completed booting.
366
367    @param process_name: Process name.
368    """
369    output = adb_shell('pgrep -c -f %s' % pipes.quote(process_name),
370                       ignore_status=True)
371    return int(output) > 0
372
373
374def check_android_file_exists(filename):
375    """Checks whether the given file exists in the Android filesystem
376
377    @param filename: File to check.
378    """
379    return adb_shell(
380        'test -e {} && echo FileExists'.format(pipes.quote(filename)),
381        ignore_status=True).find("FileExists") >= 0
382
383
384def read_android_file(filename):
385    """Reads a file in Android filesystem.
386
387    @param filename: File to read.
388    """
389    with tempfile.NamedTemporaryFile() as tmpfile:
390        adb_cmd('pull %s %s' % (pipes.quote(filename),
391                                pipes.quote(tmpfile.name)))
392        with open(tmpfile.name) as f:
393            return f.read()
394
395    return None
396
397
398def write_android_file(filename, data):
399    """Writes to a file in Android filesystem.
400
401    @param filename: File to write.
402    @param data: Data to write.
403    """
404    with tempfile.NamedTemporaryFile() as tmpfile:
405        tmpfile.write(data)
406        tmpfile.flush()
407
408        adb_cmd('push %s %s' % (pipes.quote(tmpfile.name),
409                                pipes.quote(filename)))
410
411
412def _write_android_file(filename, data):
413    """Writes to a file in Android filesystem.
414
415    This is an internal function used to bootstrap adb.
416    Tests should use write_android_file instead.
417    """
418    android_cmd = 'cat > %s' % pipes.quote(filename)
419    cros_cmd = 'android-sh -c %s' % pipes.quote(android_cmd)
420    utils.run(cros_cmd, stdin=data)
421
422
423def get_android_file_stats(filename):
424    """Returns an object of file stats for an Android file.
425
426    The returned object supported limited attributes, but can be easily extended
427    if needed. Note that the value are all string.
428
429    This uses _android_shell to run as root, so that it can access to all files
430    inside the container. On non-debuggable build, adb shell is not rootable.
431    """
432    mapping = {
433        '%a': 'mode',
434        '%g': 'gid',
435        '%h': 'nlink',
436        '%u': 'uid',
437    }
438    output = _android_shell(
439        'stat -c "%s" %s' % (' '.join(mapping.keys()), pipes.quote(filename)),
440        ignore_status=True)
441    stats = output.split(' ')
442    if len(stats) != len(mapping):
443        raise error.TestError('Unexpected output from stat: %s' % output)
444    _Stats = collections.namedtuple('_Stats', mapping.values())
445    return _Stats(*stats)
446
447
448def remove_android_file(filename):
449    """Removes a file in Android filesystem.
450
451    @param filename: File to remove.
452    """
453    adb_shell('rm -f %s' % pipes.quote(filename))
454
455
456def wait_for_android_boot(timeout=None):
457    """Sleep until Android has completed booting or timeout occurs.
458
459    @param timeout: Timeout in seconds.
460    """
461    arc_common.wait_for_android_boot(timeout)
462
463
464def wait_for_android_process(process_name,
465                             timeout=_WAIT_FOR_ANDROID_PROCESS_SECONDS):
466    """Sleep until an Android process is running or timeout occurs.
467
468    @param process_name: Process name.
469    @param timeout: Timeout in seconds.
470    """
471    condition = lambda: is_android_process_running(process_name)
472    utils.poll_for_condition(condition=condition,
473                             desc='%s is running' % process_name,
474                             timeout=timeout,
475                             sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS)
476
477
478def _android_shell(cmd, **kwargs):
479    """Execute cmd instead the Android container.
480
481    This function is strictly for internal use only, as commands do not run in
482    a fully consistent Android environment. Prefer adb_shell instead.
483    """
484    return utils.system_output('android-sh -c {}'.format(pipes.quote(cmd)),
485                               **kwargs)
486
487
488def is_android_container_alive():
489    """Check if android container is alive."""
490    try:
491        container_pid = get_container_pid()
492    except Exception as e:
493        logging.error('is_android_container_alive failed: %r', e)
494        return False
495    return utils.pid_is_alive(int(container_pid))
496
497
498def _is_in_installed_packages_list(package, option=None):
499    """Check if a package is in the list returned by pm list packages.
500
501    adb must be ready.
502
503    @param package: Package in request.
504    @param option: An option for the command adb shell pm list packages.
505                   Valid values include '-s', '-3', '-d', and '-e'.
506    """
507    command = 'pm list packages'
508    if option:
509        command += ' ' + option
510    packages = adb_shell(command).splitlines()
511    package_entry = 'package:' + package
512    ret = package_entry in packages
513
514    if not ret:
515        logging.info('Could not find "%s" in %s',
516                     package_entry, str(packages))
517    return ret
518
519
520def is_package_installed(package):
521    """Check if a package is installed. adb must be ready.
522
523    @param package: Package in request.
524    """
525    return _is_in_installed_packages_list(package)
526
527
528def is_package_disabled(package):
529    """Check if an installed package is disabled. adb must be ready.
530
531    @param package: Package in request.
532    """
533    return _is_in_installed_packages_list(package, '-d')
534
535
536def get_package_install_path(package):
537    """Returns the apk install location of the given package."""
538    output = adb_shell('pm path {}'.format(pipes.quote(package)))
539    return output.split(':')[1]
540
541
542def _before_iteration_hook(obj):
543    """Executed by parent class before every iteration.
544
545    This function resets the run_once_finished flag before every iteration
546    so we can detect failure on every single iteration.
547
548    Args:
549        obj: the test itself
550    """
551    obj.run_once_finished = False
552
553
554def _after_iteration_hook(obj):
555    """Executed by parent class after every iteration.
556
557    The parent class will handle exceptions and failures in the run and will
558    always call this hook afterwards. Take a screenshot if the run has not
559    been marked as finished (i.e. there was a failure/exception).
560
561    Args:
562        obj: the test itself
563    """
564    if not obj.run_once_finished:
565        if is_adb_connected():
566            logging.debug('Recent activities dump:\n%s',
567                          adb_shell('dumpsys activity recents',
568                                    ignore_status=True))
569        if not os.path.exists(_SCREENSHOT_DIR_PATH):
570            os.mkdir(_SCREENSHOT_DIR_PATH, 0o755)
571        obj.num_screenshots += 1
572        if obj.num_screenshots <= _MAX_SCREENSHOT_NUM:
573            logging.warning('Iteration %d failed, taking a screenshot.',
574                            obj.iteration)
575            try:
576                utils.run('screenshot "{}/{}_iter{}.png"'.format(
577                    _SCREENSHOT_DIR_PATH, _SCREENSHOT_BASENAME, obj.iteration))
578            except Exception as e:
579                logging.warning('Unable to capture screenshot. %s', e)
580        else:
581            logging.warning('Too many failures, no screenshot taken')
582
583
584def send_keycode(keycode):
585    """Sends the given keycode to the container
586
587    @param keycode: keycode to send.
588    """
589    adb_shell('input keyevent {}'.format(keycode))
590
591
592def get_android_sdk_version():
593    """Returns the Android SDK version.
594
595    This function can be called before Android container boots.
596    """
597    with open('/etc/lsb-release') as f:
598        values = dict(line.split('=', 1) for line in f.read().splitlines())
599    try:
600        return int(values['CHROMEOS_ARC_ANDROID_SDK_VERSION'])
601    except (KeyError, ValueError):
602        raise error.TestError('Could not determine Android SDK version')
603
604
605def set_device_mode(device_mode, use_fake_sensor_with_lifetime_secs=0):
606    """Sets the device in either Clamshell or Tablet mode.
607
608    "inject_powerd_input_event" might fail if the DUT does not support Tablet
609    mode, and it will raise an |error.CmdError| exception. To prevent that, use
610    the |use_fake_sensor_with_lifetime_secs| parameter.
611
612    @param device_mode: string with either 'clamshell' or 'tablet'
613    @param use_fake_sensor_with_lifetime_secs: if > 0, it will create the
614           input device with the given lifetime in seconds
615    @raise ValueError: if passed invalid parameters
616    @raise error.CmdError: if inject_powerd_input_event fails
617    """
618    valid_value = ('tablet', 'clamshell')
619    if device_mode not in valid_value:
620        raise ValueError('Invalid device_mode parameter: %s' % device_mode)
621
622    value = 1 if device_mode == 'tablet' else 0
623
624    args = ['--code=tablet', '--value=%d' % value]
625
626    if use_fake_sensor_with_lifetime_secs > 0:
627        args.extend(['--create_dev', '--dev_lifetime=%d' %
628                     use_fake_sensor_with_lifetime_secs])
629
630    try:
631        utils.run('inject_powerd_input_event', args=args)
632    except error.CmdError as err:
633        # TODO: Fragile code ahead. Correct way to do it is to check
634        # if device is already in desired mode, and do nothing if so.
635        # ATM we don't have a way to check current device mode.
636
637        # Assuming that CmdError means that device does not support
638        # --code=tablet parameter, meaning that device only supports clamshell
639        # mode.
640        if device_mode == 'clamshell' and \
641                use_fake_sensor_with_lifetime_secs == 0:
642            return
643        raise err
644
645
646def wait_for_userspace_ready():
647    """Waits for userspace apps to be launchable.
648
649    Launches and then closes Android settings as a way to ensure all basic
650    services are ready. This goes a bit beyond waiting for boot-up to complete,
651    as being able to launch an activity requires more of the framework to have
652    started. The boot-complete signal happens fairly early, and the framework
653    system server is still starting services. By waiting for ActivityManager to
654    respond, we automatically wait on more services to be ready.
655    """
656    output = adb_shell('am start -W -a android.settings.SETTINGS',
657                       ignore_status=True)
658    if not output.endswith('Complete'):
659        logging.debug('Output was: %s', output)
660        raise error.TestError('Could not launch SETTINGS')
661    adb_shell('am force-stop com.android.settings', ignore_status=True)
662
663
664class ArcTest(test.test):
665    """ Base class of ARC Test.
666
667    This class could be used as super class of an ARC test for saving
668    redundant codes for container bringup, autotest-dep package(s) including
669    uiautomator setup if required, and apks install/remove during
670    arc_setup/arc_teardown, respectively. By default arc_setup() is called in
671    initialize() after Android have been brought up. It could also be
672    overridden to perform non-default tasks. For example, a simple
673    ArcHelloWorldTest can be just implemented with print 'HelloWorld' in its
674    run_once() and no other functions are required. We could expect
675    ArcHelloWorldTest would bring up browser and  wait for container up, then
676    print 'Hello World', and shutdown browser after. As a precaution, if you
677    overwrite initialize(), arc_setup(), or cleanup() function(s) in ARC test,
678    remember to call the corresponding function(s) in this base class as well.
679    """
680    version = 1
681    _PKG_UIAUTOMATOR = 'uiautomator'
682    _FULL_PKG_NAME_UIAUTOMATOR = 'com.github.uiautomator'
683
684    def __init__(self, *args, **kwargs):
685        """Initialize flag setting."""
686        super(ArcTest, self).__init__(*args, **kwargs)
687        self.initialized = False
688        # Set the flag run_once_finished to detect if a test is executed
689        # successfully without any exception thrown. Otherwise, generate
690        # a screenshot in /var/log for debugging.
691        self.run_once_finished = False
692        self.logcat_proc = None
693        self.dep_package = None
694        self.apks = None
695        self.full_pkg_names = []
696        self.uiautomator = False
697        self._should_reenable_play_store = False
698        self._chrome = None
699        if os.path.exists(_SCREENSHOT_DIR_PATH):
700            shutil.rmtree(_SCREENSHOT_DIR_PATH)
701        self.register_before_iteration_hook(_before_iteration_hook)
702        self.register_after_iteration_hook(_after_iteration_hook)
703        # Keep track of the number of debug screenshots taken and keep the
704        # total number valid to avoid issues.
705        self.num_screenshots = 0
706
707    def initialize(self, extension_path=None, username=None, password=None,
708                   arc_mode=arc_common.ARC_MODE_ENABLED, **chrome_kargs):
709        """Log in to a test account."""
710        extension_paths = [extension_path] if extension_path else []
711        self._chrome = chrome.Chrome(extension_paths=extension_paths,
712                                     username=username,
713                                     password=password,
714                                     arc_mode=arc_mode,
715                                     **chrome_kargs)
716        if extension_path:
717            self._extension = self._chrome.get_extension(extension_path)
718        else:
719            self._extension = None
720        # With ARC enabled, Chrome will wait until container to boot up
721        # before returning here, see chrome.py.
722        self.initialized = True
723        try:
724            if is_android_container_alive():
725                self.arc_setup()
726            else:
727                logging.error('Container is alive?')
728        except Exception as err:
729            raise error.TestFail(err)
730
731    def after_run_once(self):
732        """Executed after run_once() only if there were no errors.
733
734        This function marks the run as finished with a flag. If there was a
735        failure the flag won't be set and the failure can then be detected by
736        testing the run_once_finished flag.
737        """
738        logging.info('After run_once')
739        self.run_once_finished = True
740
741    def cleanup(self):
742        """Log out of Chrome."""
743        if not self.initialized:
744            logging.info('Skipping ARC cleanup: not initialized')
745            return
746        logging.info('Starting ARC cleanup')
747        try:
748            if is_android_container_alive():
749                self.arc_teardown()
750        except Exception as err:
751            raise error.TestFail(err)
752        finally:
753            try:
754                if self.logcat_proc:
755                    self.logcat_proc.close()
756            finally:
757                if self._chrome is not None:
758                    self._chrome.close()
759
760    def _install_apks(self, dep_package, apks, full_pkg_names):
761        """"Install apks fetched from the specified package folder.
762
763        @param dep_package: A dependent package directory
764        @param apks: List of apk names to be installed
765        @param full_pkg_names: List of packages to be uninstalled at teardown
766        """
767        apk_path = os.path.join(self.autodir, 'deps', dep_package)
768        if apks:
769            for apk in apks:
770                logging.info('Installing %s', apk)
771                out = adb_install('%s/%s' % (apk_path, apk), ignore_status=True)
772                logging.info('Install apk output: %s', str(out))
773            # Verify if package(s) are installed correctly.  We ignored
774            # individual install statuses above because some tests list apks for
775            # all arches and only need one installed.
776            if not full_pkg_names:
777                raise error.TestError('Package names of apks expected')
778            for pkg in full_pkg_names:
779                logging.info('Check if %s is installed', pkg)
780                if not is_package_installed(pkg):
781                    raise error.TestError('Package %s not found' % pkg)
782                # Make sure full_pkg_names contains installed packages only
783                # so arc_teardown() knows what packages to uninstall.
784                self.full_pkg_names.append(pkg)
785
786    def _count_nested_array_level(self, array):
787        """Count the level of a nested array."""
788        if isinstance(array, list):
789            return 1 + self._count_nested_array_level(array[0])
790        return 0
791
792    def _fix_nested_array_level(self, var_name, expected_level, array):
793        """Enclose array one level deeper if needed."""
794        level = self._count_nested_array_level(array)
795        if level == expected_level:
796            return array
797        if level == expected_level - 1:
798            return [array]
799
800        logging.error("Variable %s nested level is not fixable: "
801                      "Expecting %d, seeing %d",
802                      var_name, expected_level, level)
803        raise error.TestError('Format error with variable %s' % var_name)
804
805    def arc_setup(self, dep_packages=None, apks=None, full_pkg_names=None,
806                  uiautomator=False, disable_play_store=False):
807        """ARC test setup: Setup dependencies and install apks.
808
809        This function disables package verification and enables non-market
810        APK installation. Then, it installs specified APK(s) and uiautomator
811        package and path if required in a test.
812
813        @param dep_packages: Array of package names of autotest_deps APK
814                             packages.
815        @param apks: Array of APK name arrays to be installed in dep_package.
816        @param full_pkg_names: Array of full package name arrays to be removed
817                               in teardown.
818        @param uiautomator: uiautomator python package is required or not.
819        @param disable_play_store: Set this to True if you want to prevent
820                                   GMS Core from updating.
821        """
822        if not self.initialized:
823            logging.info('Skipping ARC setup: not initialized')
824            return
825        logging.info('Starting ARC setup')
826
827        # Sample parameters for multi-deps setup after fixup (if needed):
828        # dep_packages: ['Dep1-apk', 'Dep2-apk']
829        # apks: [['com.dep1.arch1.apk', 'com.dep2.arch2.apk'], ['com.dep2.apk']
830        # full_pkg_nmes: [['com.dep1.app'], ['com.dep2.app']]
831        # TODO(crbug/777787): once the parameters of all callers of arc_setup
832        # are refactored, we can delete the safety net here.
833        if dep_packages:
834            dep_packages = self._fix_nested_array_level(
835                'dep_packages', 1, dep_packages)
836            apks = self._fix_nested_array_level('apks', 2, apks)
837            full_pkg_names = self._fix_nested_array_level(
838                'full_pkg_names', 2, full_pkg_names)
839            if (len(dep_packages) != len(apks) or
840                    len(apks) != len(full_pkg_names)):
841                logging.info('dep_packages length is %d', len(dep_packages))
842                logging.info('apks length is %d', len(apks))
843                logging.info('full_pkg_names length is %d',
844                             len(full_pkg_names))
845                raise error.TestFail(
846                    'dep_packages/apks/full_pkg_names format error')
847
848        self.dep_packages = dep_packages
849        self.apks = apks
850        self.uiautomator = uiautomator or disable_play_store
851        # Setup dependent packages if required
852        packages = []
853        if dep_packages:
854            packages = dep_packages[:]
855        if self.uiautomator:
856            packages.append(self._PKG_UIAUTOMATOR)
857        if packages:
858            logging.info('Setting up dependent package(s) %s', packages)
859            self.job.setup_dep(packages)
860
861        self.logcat_proc = arc_common.Logcat()
862
863        wait_for_adb_ready()
864
865        # Setting verifier_verify_adb_installs to zero suppresses a dialog box
866        # that can appear asking for the user to consent to the install.
867        adb_shell('settings put global verifier_verify_adb_installs 0')
868
869        # Install apks based on dep_packages/apks/full_pkg_names tuples
870        if dep_packages:
871            for i in range(len(dep_packages)):
872                self._install_apks(dep_packages[i], apks[i], full_pkg_names[i])
873
874        if self.uiautomator:
875            path = os.path.join(self.autodir, 'deps', self._PKG_UIAUTOMATOR)
876            sys.path.append(path)
877            self._add_ui_object_not_found_handler()
878        if disable_play_store and not is_package_disabled(_PLAY_STORE_PKG):
879            self._disable_play_store()
880            if not is_package_disabled(_PLAY_STORE_PKG):
881                raise error.TestFail('Failed to disable Google Play Store.')
882            self._should_reenable_play_store = True
883
884    def arc_teardown(self):
885        """ARC test teardown.
886
887        This function removes all installed packages in arc_setup stage
888        first. Then, it restores package verification and disables non-market
889        APK installation.
890
891        """
892        if self.full_pkg_names:
893            for pkg in self.full_pkg_names:
894                logging.info('Uninstalling %s', pkg)
895                if not is_package_installed(pkg):
896                    raise error.TestError('Package %s was not installed' % pkg)
897                adb_uninstall(pkg)
898        if (self.uiautomator and
899            is_package_installed(self._FULL_PKG_NAME_UIAUTOMATOR)):
900            logging.info('Uninstalling %s', self._FULL_PKG_NAME_UIAUTOMATOR)
901            adb_uninstall(self._FULL_PKG_NAME_UIAUTOMATOR)
902        if self._should_reenable_play_store:
903            adb_shell('pm enable ' + _PLAY_STORE_PKG)
904        adb_shell('settings put secure install_non_market_apps 0')
905        adb_shell('settings put global package_verifier_enable 1')
906        adb_shell('settings put secure package_verifier_user_consent 0')
907
908        # Remove the adb keys without going through adb. This is because the
909        # 'rm' tool does not have permissions to remove the keys once they have
910        # been restorecon(8)ed.
911        utils.system_output('rm -f %s' %
912                            pipes.quote(os.path.join(
913                                get_android_data_root(),
914                                os.path.relpath(_ANDROID_ADB_KEYS_PATH, '/'))))
915        utils.system_output('adb kill-server')
916
917    def _add_ui_object_not_found_handler(self):
918        """Logs the device dump upon uiautomator.UiObjectNotFoundException."""
919        from uiautomator import device as d
920        d.handlers.on(lambda d: logging.debug('Device window dump:\n%s',
921                                              d.dump()))
922
923    def _disable_play_store(self):
924        """Disables the Google Play Store app."""
925        if is_package_disabled(_PLAY_STORE_PKG):
926            return
927        adb_shell('am force-stop ' + _PLAY_STORE_PKG)
928        adb_shell('am start -a android.settings.APPLICATION_DETAILS_SETTINGS '
929                  '-d package:' + _PLAY_STORE_PKG)
930
931        # Note: the straightforward "pm disable <package>" command would be
932        # better, but that requires root permissions, which aren't available on
933        # a pre-release image being tested. The only other way is through the
934        # Settings UI, but which might change.
935        from uiautomator import device as d
936        d(textMatches='(?i)DISABLE', packageName=_SETTINGS_PKG).wait.exists()
937        d(textMatches='(?i)DISABLE', packageName=_SETTINGS_PKG).click.wait()
938        d(textMatches='(?i)DISABLE APP').click.wait()
939        ok_button = d(textMatches='(?i)OK')
940        if ok_button.exists:
941            ok_button.click.wait()
942        adb_shell('am force-stop ' + _SETTINGS_PKG)
943