xref: /aosp_15_r20/external/autotest/server/hosts/android_host.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2022 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li#
6*9c5db199SXin Li# Expects to be run in an environment with sudo and no interactive password
7*9c5db199SXin Li# prompt, such as within the Chromium OS development chroot.
8*9c5db199SXin Li
9*9c5db199SXin Liimport logging
10*9c5db199SXin Liimport socket
11*9c5db199SXin Li
12*9c5db199SXin Liimport common
13*9c5db199SXin Li
14*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
15*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info
16*9c5db199SXin Lifrom autotest_lib.server.hosts import attached_device_host
17*9c5db199SXin Lifrom autotest_lib.server.hosts import android_constants
18*9c5db199SXin Lifrom autotest_lib.server.hosts import base_classes
19*9c5db199SXin Li
20*9c5db199SXin Li
21*9c5db199SXin Liclass AndroidHost(base_classes.Host):
22*9c5db199SXin Li    """Host class for Android devices"""
23*9c5db199SXin Li    PHONE_STATION_LABEL_PREFIX = "associated_hostname"
24*9c5db199SXin Li    SERIAL_NUMBER_LABEL_PREFIX = "serial_number"
25*9c5db199SXin Li    # adb auth key path on the phone_station.
26*9c5db199SXin Li    ADB_KEY_PATH = '/var/lib/android_keys'
27*9c5db199SXin Li
28*9c5db199SXin Li    def __init__(self,
29*9c5db199SXin Li                 hostname,
30*9c5db199SXin Li                 host_info_store=None,
31*9c5db199SXin Li                 android_args=None,
32*9c5db199SXin Li                 *args,
33*9c5db199SXin Li                 **dargs):
34*9c5db199SXin Li        """Construct a AndroidHost object.
35*9c5db199SXin Li
36*9c5db199SXin Li        Args:
37*9c5db199SXin Li            hostname: Hostname of the Android phone.
38*9c5db199SXin Li            host_info_store: Optional host_info.CachingHostInfoStore object
39*9c5db199SXin Li                             to obtain / update host information.
40*9c5db199SXin Li            android_args: Android args for local test run.
41*9c5db199SXin Li        """
42*9c5db199SXin Li        self.hostname = hostname
43*9c5db199SXin Li        super(AndroidHost, self).__init__(*args, **dargs)
44*9c5db199SXin Li        self.host_info_store = (host_info_store
45*9c5db199SXin Li                                or host_info.InMemoryHostInfoStore())
46*9c5db199SXin Li        self.associated_hostname = None
47*9c5db199SXin Li        self.serial_number = None
48*9c5db199SXin Li        self.phone_station_ssh_port = None
49*9c5db199SXin Li        # For local test, android_args are passed in.
50*9c5db199SXin Li        if android_args:
51*9c5db199SXin Li            self._read_essential_data_from_args_dict(android_args)
52*9c5db199SXin Li        else:
53*9c5db199SXin Li            self._read_essential_data_from_host_info_store()
54*9c5db199SXin Li        # Since we won't be ssh into an Android device directly, all the
55*9c5db199SXin Li        # communication will be handled by run ADB CLI on the phone
56*9c5db199SXin Li        # station(chromebox or linux machine) that physically connected
57*9c5db199SXin Li        # to the Android devices via USB cable. So we need to setup an
58*9c5db199SXin Li        # AttachedDeviceHost for phone station as ssh proxy.
59*9c5db199SXin Li        self.phone_station = self._create_phone_station_host_proxy()
60*9c5db199SXin Li        self.adb_tcp_mode = False
61*9c5db199SXin Li        self.usb_dev_path = None
62*9c5db199SXin Li        self.closed = False
63*9c5db199SXin Li
64*9c5db199SXin Li    def _create_phone_station_host_proxy(self):
65*9c5db199SXin Li        logging.info('Creating host for phone station %s',
66*9c5db199SXin Li                     self.associated_hostname)
67*9c5db199SXin Li        return attached_device_host.AttachedDeviceHost(
68*9c5db199SXin Li                hostname=self.associated_hostname,
69*9c5db199SXin Li                serial_number=self.serial_number,
70*9c5db199SXin Li                phone_station_ssh_port=self.phone_station_ssh_port)
71*9c5db199SXin Li
72*9c5db199SXin Li    def _read_essential_data_from_args_dict(self, android_args):
73*9c5db199SXin Li        self.associated_hostname = android_args.get(
74*9c5db199SXin Li                android_constants.ANDROID_PHONE_STATION_ATTR)
75*9c5db199SXin Li        self.phone_station_ssh_port = android_args.get(
76*9c5db199SXin Li                android_constants.ANDROID_PHONE_STATION_SSH_PORT_ATTR)
77*9c5db199SXin Li        self.serial_number = android_args.get(
78*9c5db199SXin Li                android_constants.ANDROID_SERIAL_NUMBER_ATTR)
79*9c5db199SXin Li
80*9c5db199SXin Li    def _read_essential_data_from_host_info_store(self):
81*9c5db199SXin Li        info = self.host_info_store.get()
82*9c5db199SXin Li        self.associated_hostname = info.get_label_value(
83*9c5db199SXin Li                self.PHONE_STATION_LABEL_PREFIX)
84*9c5db199SXin Li        if not self.associated_hostname:
85*9c5db199SXin Li            raise error.AutoservError(
86*9c5db199SXin Li                    'Failed to initialize Android host due to'
87*9c5db199SXin Li                    ' associated_hostname is not found in host_info_store.')
88*9c5db199SXin Li        self.serial_number = info.get_label_value(
89*9c5db199SXin Li                self.SERIAL_NUMBER_LABEL_PREFIX)
90*9c5db199SXin Li        if not self.serial_number:
91*9c5db199SXin Li            raise error.AutoservError(
92*9c5db199SXin Li                    'Failed to initialize Android host due to'
93*9c5db199SXin Li                    ' serial_number is not found in host_info_store.')
94*9c5db199SXin Li
95*9c5db199SXin Li    def adb_over_tcp(self, port=5555, persist_reboot=False):
96*9c5db199SXin Li        """Restart adb server listening on a TCP port.
97*9c5db199SXin Li
98*9c5db199SXin Li        Args:
99*9c5db199SXin Li            port: Tcp port for adb server to listening on, default value
100*9c5db199SXin Li                  is 5555 which is the default TCP/IP port for adb.
101*9c5db199SXin Li            persist_reboot: True for adb over tcp to continue listening
102*9c5db199SXin Li                            after the device reboots.
103*9c5db199SXin Li        """
104*9c5db199SXin Li        port = str(port)
105*9c5db199SXin Li        if persist_reboot:
106*9c5db199SXin Li            self.run_adb_command('shell setprop persist.adb.tcp.port %s' %
107*9c5db199SXin Li                                 port)
108*9c5db199SXin Li            self.run_adb_command('shell setprop ctl.restart adbd')
109*9c5db199SXin Li            self.wait_for_transport_state()
110*9c5db199SXin Li
111*9c5db199SXin Li        self.run_adb_command('tcpip %s' % port)
112*9c5db199SXin Li        self.adb_tcp_mode = True
113*9c5db199SXin Li
114*9c5db199SXin Li    def cache_usb_dev_path(self):
115*9c5db199SXin Li        """
116*9c5db199SXin Li        Read and cache usb devpath for the Android device.
117*9c5db199SXin Li        """
118*9c5db199SXin Li        cmd = 'adb devices -l | grep %s' % self.serial_number
119*9c5db199SXin Li        res = self.phone_station.run(cmd)
120*9c5db199SXin Li        for line in res.stdout.strip().split('\n'):
121*9c5db199SXin Li            if len(line.split()) > 2 and line.split()[1] == 'device':
122*9c5db199SXin Li                self.usb_dev_path = line.split()[2]
123*9c5db199SXin Li                logging.info('USB devpath: %s', self.usb_dev_path)
124*9c5db199SXin Li                break
125*9c5db199SXin Li        if not self.usb_dev_path:
126*9c5db199SXin Li            logging.warning(
127*9c5db199SXin Li                    'Failed to collect usbdev path of the Android device.')
128*9c5db199SXin Li
129*9c5db199SXin Li    def ensure_device_connectivity(self):
130*9c5db199SXin Li        """Ensure we can interact with the Android device via adb and
131*9c5db199SXin Li        the device is in the expected state.
132*9c5db199SXin Li        """
133*9c5db199SXin Li        res = self.run_adb_command('get-state')
134*9c5db199SXin Li        state = res.stdout.strip()
135*9c5db199SXin Li        logging.info('Android device state from adb: %s', state)
136*9c5db199SXin Li        return state == 'device'
137*9c5db199SXin Li
138*9c5db199SXin Li    def get_wifi_ip_address(self):
139*9c5db199SXin Li        """Get ipv4 address from the Android device"""
140*9c5db199SXin Li        res = self.run_adb_command('shell ip route')
141*9c5db199SXin Li        # An example response would looks like: "192.168.86.0/24 dev wlan0"
142*9c5db199SXin Li        # " proto kernel scope link src 192.168.86.22 \n"
143*9c5db199SXin Li        ip_string = res.stdout.strip().split(' ')[-1]
144*9c5db199SXin Li        logging.info('Ip address collected from the Android device: %s',
145*9c5db199SXin Li                     ip_string)
146*9c5db199SXin Li        try:
147*9c5db199SXin Li            socket.inet_aton(ip_string)
148*9c5db199SXin Li        except (OSError, ValueError, socket.error):
149*9c5db199SXin Li            raise error.AutoservError(
150*9c5db199SXin Li                    'Failed to get ip address from the Android device.')
151*9c5db199SXin Li        return ip_string
152*9c5db199SXin Li
153*9c5db199SXin Li    def job_start(self):
154*9c5db199SXin Li        """This method is called from create_host factory when
155*9c5db199SXin Li        construct the host object. We need to override it since actions
156*9c5db199SXin Li        like copy /var/log/messages are not applicable on Android devices.
157*9c5db199SXin Li        """
158*9c5db199SXin Li        logging.info('Skip standard job_start actions for Android host.')
159*9c5db199SXin Li
160*9c5db199SXin Li    def restart_adb_server(self):
161*9c5db199SXin Li        """Restart adb server from the phone station"""
162*9c5db199SXin Li        self.stop_adb_server()
163*9c5db199SXin Li        self.start_adb_server()
164*9c5db199SXin Li
165*9c5db199SXin Li    def run_adb_command(self, adb_command, ignore_status=False):
166*9c5db199SXin Li        """Run adb command on the Android device.
167*9c5db199SXin Li
168*9c5db199SXin Li        Args:
169*9c5db199SXin Li            adb_command: adb commands to execute on the Android device.
170*9c5db199SXin Li
171*9c5db199SXin Li        Returns:
172*9c5db199SXin Li            An autotest_lib.client.common_lib.utils.CmdResult object.
173*9c5db199SXin Li        """
174*9c5db199SXin Li        # When use adb to interact with an Android device, we prefer to use
175*9c5db199SXin Li        # devpath to distinguish the particular device as the serial number
176*9c5db199SXin Li        # is not guaranteed to be unique.
177*9c5db199SXin Li        if self.usb_dev_path:
178*9c5db199SXin Li            command = 'adb -s %s %s' % (self.usb_dev_path, adb_command)
179*9c5db199SXin Li        else:
180*9c5db199SXin Li            command = 'adb -s %s %s' % (self.serial_number, adb_command)
181*9c5db199SXin Li        return self.phone_station.run(command, ignore_status=ignore_status)
182*9c5db199SXin Li
183*9c5db199SXin Li    def wait_for_transport_state(self, transport='usb', state='device'):
184*9c5db199SXin Li        """
185*9c5db199SXin Li        Wait for a device to reach a desired state.
186*9c5db199SXin Li
187*9c5db199SXin Li        Args:
188*9c5db199SXin Li            transport: usb, local, any
189*9c5db199SXin Li            state: device, recovery, sideload, bootloader
190*9c5db199SXin Li
191*9c5db199SXin Li        """
192*9c5db199SXin Li        self.run_adb_command('wait-for-%s-%s' % (transport, state))
193*9c5db199SXin Li
194*9c5db199SXin Li    def start_adb_server(self):
195*9c5db199SXin Li        """Start adb server from the phone station."""
196*9c5db199SXin Li        # Adb home is created upon CrOS login, however on labstation we
197*9c5db199SXin Li        # never login so we'll need to ensure the adb home is exist before
198*9c5db199SXin Li        # starting adb server.
199*9c5db199SXin Li        self.phone_station.run("mkdir -p /run/arc/adb")
200*9c5db199SXin Li        self.phone_station.run("ADB_VENDOR_KEYS=%s adb start-server" %
201*9c5db199SXin Li                               self.ADB_KEY_PATH)
202*9c5db199SXin Li        # Logging states of all attached devices.
203*9c5db199SXin Li        self.phone_station.run('adb devices')
204*9c5db199SXin Li
205*9c5db199SXin Li    def stop_adb_server(self):
206*9c5db199SXin Li        """Stop adb server from the phone station."""
207*9c5db199SXin Li        self.phone_station.run("adb kill-server")
208*9c5db199SXin Li
209*9c5db199SXin Li    def setup_for_cross_device_tests(self, adb_persist_reboot=False):
210*9c5db199SXin Li        """
211*9c5db199SXin Li        Setup the Android phone for Cross Device tests.
212*9c5db199SXin Li
213*9c5db199SXin Li        Ensures the phone can connect to its labstation and sets up
214*9c5db199SXin Li        adb-over-tcp.
215*9c5db199SXin Li
216*9c5db199SXin Li        Returns:
217*9c5db199SXin Li            IP Address of Phone.
218*9c5db199SXin Li        """
219*9c5db199SXin Li        dut_out = self.phone_station.run('echo True').stdout.strip()
220*9c5db199SXin Li        if dut_out != 'True':
221*9c5db199SXin Li            raise error.TestError('phone station stdout != True (got: %s)',
222*9c5db199SXin Li                                  dut_out)
223*9c5db199SXin Li
224*9c5db199SXin Li        self.restart_adb_server()
225*9c5db199SXin Li        self.cache_usb_dev_path()
226*9c5db199SXin Li        self.ensure_device_connectivity()
227*9c5db199SXin Li        ip_address = self.get_wifi_ip_address()
228*9c5db199SXin Li        self.adb_over_tcp(persist_reboot=adb_persist_reboot)
229*9c5db199SXin Li        return ip_address
230*9c5db199SXin Li
231*9c5db199SXin Li    def close(self):
232*9c5db199SXin Li        """Clean up Android host and its phone station proxy host."""
233*9c5db199SXin Li        if self.closed:
234*9c5db199SXin Li            logging.debug('Android host %s already closed.', self.hostname)
235*9c5db199SXin Li            return
236*9c5db199SXin Li        try:
237*9c5db199SXin Li            if self.adb_tcp_mode:
238*9c5db199SXin Li                # In some rare cases, leave the Android device in adb over tcp
239*9c5db199SXin Li                # mode may break USB connection so we want to always reset adb
240*9c5db199SXin Li                # to usb mode before teardown.
241*9c5db199SXin Li                self.run_adb_command('usb', ignore_status=True)
242*9c5db199SXin Li            self.stop_adb_server()
243*9c5db199SXin Li            if self.phone_station:
244*9c5db199SXin Li                self.phone_station.close()
245*9c5db199SXin Li            self.closed = True
246*9c5db199SXin Li        finally:
247*9c5db199SXin Li            super(AndroidHost, self).close()
248*9c5db199SXin Li
249*9c5db199SXin Li    @staticmethod
250*9c5db199SXin Li    def get_android_arguments(args_dict):
251*9c5db199SXin Li        """Extract android args from `args_dict` and return the result.
252*9c5db199SXin Li
253*9c5db199SXin Li        Recommended usage in control file:
254*9c5db199SXin Li            args_dict = utils.args_to_dict(args)
255*9c5db199SXin Li            android_args = hosts.Android.get_android_arguments(args_dict)
256*9c5db199SXin Li            host = hosts.create_host(machine, android_args=android_args)
257*9c5db199SXin Li
258*9c5db199SXin Li        Args:
259*9c5db199SXin Li            args_dict: A dict of test args.
260*9c5db199SXin Li
261*9c5db199SXin Li        Returns:
262*9c5db199SXin Li            An dict of android related args.
263*9c5db199SXin Li        """
264*9c5db199SXin Li        android_args = {
265*9c5db199SXin Li                key: args_dict[key]
266*9c5db199SXin Li                for key in android_constants.ALL_ANDROID_ATTRS
267*9c5db199SXin Li                if key in args_dict
268*9c5db199SXin Li        }
269*9c5db199SXin Li        for attr in android_constants.CRITICAL_ANDROID_ATTRS:
270*9c5db199SXin Li            if attr not in android_args or not android_args.get(attr):
271*9c5db199SXin Li                raise error.AutoservError("Critical attribute %s is missing"
272*9c5db199SXin Li                                          " from android_args." % attr)
273*9c5db199SXin Li        return android_args
274