blob: 559fe8c1096cae2ab3a1141fd49087b3ce7162a0 [file] [log] [blame]
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01001#!/usr/bin/env python3
2
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +01003import os
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01004import time
5from uiautomator import Device
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +02006import re
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01007import sys
8import subprocess
9import pathlib
10
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010011
12PREBUILTS_PATH = '../../vendor/smartviser/viser/prebuilts/apk'
13
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +020014PREBUILT_PROXY_APKS = {
15 'gms': 'com.lunarlabs.panda.proxy.apk',
16 'sibon': 'com.lunarlabs.panda.proxy-sibon.apk',
17}
18
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010019PREBUILT_APKS = [
20 'com.smartviser.demogame.apk',
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010021 'com.lunarlabs.panda.apk',
22]
23
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +020024FAIRPHONE_WIFI_NETWORKS = {
25 'Fairphone Guest': {'security': 'WPA/WPA2 PSK', 'password': 'fairwifi'},
26 'Fairphone DEV (2.4 GHz)': {
27 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
28 'Fairphone DEV (5 GHz)': {
29 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
30}
31
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020032ADB_DEVICES_PATTERN = re.compile(r'^([a-z0-9-]+)\s+device$', flags=re.M)
33
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010034
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020035class HostCommandError(BaseException):
36 """An error happened while issuing a command on the host."""
37 def __init__(self, command, error_message):
38 self.command = command
39 self.error_message = error_message
40 message = 'Command `{}` failed: {}'.format(command, error_message)
41 super(HostCommandError, self).__init__(message)
42
43
44class DeviceCommandError(BaseException):
45 """An error happened while sending a command to a device."""
46 def __init__(self, serial, command, error_message):
47 self.serial = serial
48 self.command = command
49 self.error_message = error_message
50 message = 'Command `{}` failed on {}: {}'.format(
51 command, serial, error_message)
52 super(DeviceCommandError, self).__init__(message)
53
54
55def adb(*args, serial = None, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010056 """Run ADB command attached to serial.
57
58 Example:
59 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
60 >>> process.returncode
61 0
62 >>> process.stdout.strip()
63 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
64
65 :param *args:
66 List of options to ADB (including command).
67 :param str serial:
68 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020069 :param raise_on_error bool:
70 Whether to raise a DeviceCommandError exception if the return code is
71 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010072 :returns subprocess.CompletedProcess:
73 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020074 :raises DeviceCommandError:
75 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010076 """
77
78 # Make sure the adb server is started to avoid the infamous "out of date"
79 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020080 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010081 ['adb', 'start-server'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
82 universal_newlines=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020083 if ret.returncode < 0:
84 if raise_on_error:
85 raise DeviceCommandError(
86 serial if serial else '??', str(args), ret.stderr)
87 else:
88 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010089
90 command = ['adb']
91 if serial:
92 command += ['-s', serial]
93 if args:
94 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020095 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010096 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
97 universal_newlines=True)
98
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020099 if raise_on_error and ret.returncode < 0:
100 raise DeviceCommandError(
101 serial if serial else '??', str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100102
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200103 return ret
104
105
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200106def list_devices():
107 """List serial numbers of devices attached to adb.
108
109 Raises:
110 DeviceCommandError: If the underlying adb command failed.
111 """
112 process = adb('devices')
113 return ADB_DEVICES_PATTERN.findall(process.stdout)
114
115
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200116def aapt(*args, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100117 """Run an AAPT command.
118
119 :param *args:
120 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200121 :param raise_on_error bool:
122 Whether to raise a DeviceCommandError exception if the return code is
123 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100124 :returns subprocess.CompletedProcess:
125 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200126 :raises HostCommandError:
127 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100128 """
129 command = ['aapt']
130 if args:
131 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200132 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100133 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
134 universal_newlines=True)
135
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200136 if raise_on_error and ret.returncode < 0:
137 raise HostCommandError(str(args), ret.stderr)
138
139 return ret
140
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100141
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800142def force_awake(serial, always=True):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200143 """Force the device to stay awake.
144
145 Raises:
146 DeviceCommandError: If the underlying adb command failed.
147 """
148 adb('shell', 'svc power stayon {}'.format(
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800149 'true' if always else 'false'), serial=serial)
150
151
152def unlock(serial):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200153 """Wake-up the device and unlock it.
154
155 Raises:
156 DeviceCommandError: If the underlying adb commands failed.
157 """
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800158 adb('shell', 'input keyevent KEYCODE_POWER', serial=serial)
159 time.sleep(1)
160 adb('shell', 'input keyevent KEYCODE_MENU', serial=serial)
161 time.sleep(1)
162 adb('shell', 'input keyevent KEYCODE_HOME', serial=serial)
163
164
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200165def getprop(serial, key):
166 """Get system property of device.
167
168 Example:
169 >>> getprop('167eb6e8', 'ro.build.id')
170 'FP2-gms-18.02.0'
171
172 :param str serial:
173 Identifier for ADB connection to device.
174 :param str key:
175 Key of property to get.
176 :returns str:
177 Value of system property.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200178 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200179 """
180 process = adb('shell', 'getprop', key, serial=serial)
181 return process.stdout.strip()
182
183
184def is_gms_device(serial):
185 """Test if device runs GMS or sibon.
186
187 Example:
188 >>> is_gms_device('167eb6e8')
189 True
190
191 :param str serial:
192 Identifier for ADB connection to device.
193 :returns bool:
194 True if device runs GMS, false otherwise.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200195 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200196 """
197 return getprop(serial, 'ro.build.id').startswith('FP2-gms-')
198
199
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100200def uninstall_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200201 """Uninstall apk from prebuilts_dir on device.
202
203 Raises:
204 ValueError: If the package name could not be read from the apk.
205 DeviceCommandError: If the uninstall command failed.
206 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100207 ret = aapt('dump', 'badging', '{}/{}'.format(prebuilts_dir, filename))
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200208 package = None
209 for line in ret.stdout.splitlines():
210 if line.startswith('package'):
211 for token in line.split(' '):
212 if token.startswith('name='):
213 # Extract the package name out of the token
214 # (name='some.package.name')
215 package = token[6:-1]
216 break
217 if not package:
218 raise ValueError('Could not find package of app `{}`'.format(
219 filename))
220
221 adb('uninstall', package, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100222
223
224def install_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200225 """Install apk from prebuilts_dir on device.
226
227 Raises:
228 DeviceCommandError: If the install command failed.
229 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100230 path = os.path.join(prebuilts_dir, filename)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200231 adb('install', '-r', path, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100232
233
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200234def configure_wifi_networks(dut, networks):
235 """Configure Wi-Fi networks.
236
237 The `networks` parameters is a list of networks to configure hashed by
238 their SSID. Each network value should have the following format:
239 - security (str): The security value as can be found in the Wi-Fi
240 settings dialog. Common values are 'None' and 'WPA/WPA2 PSK'.
241 - password (str, optional): The network password if the security is
242 'WPA/WPA2 PSK'.
243
244 Parameters:
245 dut (Device): The device object.
246 networks (dict(dict(str))): The list of networks to configure.
247 Raises:
248 DeviceCommandError: If the UI automation fails.
249 """
250 # Open the Wi-Fi settings
251 adb('shell', ('am start -a android.settings.WIFI_SETTINGS '
252 '--activity-clear-task'), serial=dut.serial)
253
254 # Make sure Wi-Fi is enabled
255 wifi_enabler = dut(text='OFF',
256 resourceId='com.android.settings:id/switch_widget')
257 if wifi_enabler.exists:
258 wifi_enabler.click()
259
260 # Check for registered networks
261 registered_networks = set()
262 dut(description='More options').click()
263 time.sleep(1)
264 saved_networks = dut(text='Saved networks')
265 if saved_networks.exists:
266 saved_networks.click()
267 for ssid in networks.keys():
268 if dut(text=ssid).exists:
269 registered_networks.add(ssid)
270 dut.press.back()
271
272 missing_networks = networks.keys() - registered_networks
273
274 for ssid in registered_networks:
275 print('Ignoring `{}` Wi-Fi network, already configured.'.format(ssid))
276
277 for ssid in missing_networks:
278 print('Configuring `{}` Wi-Fi network…'.format(ssid))
279
280 dut(description='More options').click()
281 dut(text='Add network').click()
282 dut(resourceId='com.android.settings:id/ssid') \
283 .set_text(ssid)
284 dut(resourceId='com.android.settings:id/security') \
285 .click()
286 dut(text=networks[ssid]['security']) \
287 .click()
288 password_field = dut(resourceId='com.android.settings:id/password')
289 if 'password' in networks[ssid] and networks[ssid]['password']:
290 if not password_field.exists:
291 dut(text='Cancel').click()
292 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
293 'missing password field')
294 password_field.set_text(networks[ssid]['password'])
295 elif password_field.exists:
296 dut(text='Cancel').click()
297 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
298 'missing password data')
299 save_button = dut(text='Save')
300 if not save_button.click():
301 dut(text='Cancel').click()
302 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
303 'could not save network')
304
305 # Force the Wi-Fi on and off to pick the best network available
306 dut(text='ON', resourceId='com.android.settings:id/switch_widget').click()
307 dut(text='OFF', resourceId='com.android.settings:id/switch_widget').click()
308
309 # Leave the settings
310 dut.press.back()
311
312
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100313# Prepare the DUT
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100314def prepare_dut(serial, scenarios_dir, data_dir, prebuilts_dir):
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200315 flavour = 'gms' if is_gms_device(serial) else 'sibon'
316
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100317 # Uninstall the smartviser apps
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200318 for app in PREBUILT_APKS + [PREBUILT_PROXY_APKS[flavour]]:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200319 print('Uninstalling `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100320 uninstall_apk(serial, app, prebuilts_dir)
321
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100322 # Copy the scenarios
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200323 print('Pushing scenarios from `{}`…'.format(scenarios_dir))
324 adb('push', scenarios_dir, '/sdcard/viser', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100325
326 # Copy the scenarios data
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200327 print('Pushing scenarios data from `{}`…'.format(data_dir))
328 adb('push', data_dir, '/sdcard/viser/data', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100329
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200330 # Install the smartviser apps (starting with the proxy app)
331 for app in [PREBUILT_PROXY_APKS[flavour]] + PREBUILT_APKS:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200332 print('Installing `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100333 install_apk(serial, app, prebuilts_dir)
334
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100335
336# Grant the permissions through the UI
337def configure_perms(dut):
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200338 dut() \
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100339 .child_by_text('You need to grant access for the Permissions Group '
340 'Calendar Camera Contacts Microphone SMS Storage Telephone Location',
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200341 resourceId='android:id/content') \
342 .child(text='OK', className='android.widget.Button') \
343 .click()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100344
345 # Accept any permission that is asked for
346 prompt = dut(resourceId='com.android.packageinstaller:id/permission_allow_button',
347 className='android.widget.Button')
348 while prompt.exists:
349 prompt.click()
350
351 # Input the credentials
352 dut(resourceId='android:id/content') \
353 .child(text='Username') \
354 .child(className='android.widget.EditText') \
355 .set_text('borjan@fairphone.com')
356 dut(resourceId='android:id/content') \
357 .child(text='Password') \
358 .child(className='android.widget.EditText') \
359 .set_text('Rickisalso1niceguy!')
360
361 # Sign in
362 dut(resourceId='android:id/content') \
363 .child(text='Sign in', className='android.widget.Button') \
364 .click()
365
366def configure_sms(dut):
367 # TODO wait for the connection to be established and time-out
368 prompt = dut(resourceId='android:id/content') \
369 .child(text='Viser must be your SMS app to send messages')
370 while not prompt.exists:
371 time.sleep(1)
372
373 # Make viser the default SMS app
374 dut(resourceId='android:id/content') \
375 .child_by_text('Viser must be your SMS app to send messages',
376 className='android.widget.LinearLayout') \
377 .child(text='OK', className='android.widget.Button') \
378 .click()
379
380 dut(resourceId='android:id/content') \
381 .child_by_text('Change SMS app?', className='android.widget.LinearLayout') \
382 .child(text='Yes', className='android.widget.Button') \
383 .click()
384
385def configure_settings(dut):
386 # Set the e-mail account
387 dut(text='Settings', className='android.widget.TextView') \
388 .click()
389 dut(resourceId='android:id/list') \
390 .child_by_text('User settings', className='android.widget.LinearLayout') \
391 .click()
392 dut(resourceId='android:id/list') \
393 .child_by_text('Email account', className='android.widget.LinearLayout') \
394 .click()
395 prompt = dut(resourceId='android:id/content') \
396 .child_by_text('Email account', className='android.widget.LinearLayout')
397 prompt.child(resourceId='android:id/edit') \
398 .set_text('fairphone.viser@gmail.com')
399 prompt.child(text='OK', className='android.widget.Button') \
400 .click()
401 dut(resourceId='android:id/list') \
402 .child_by_text('Email password', className='android.widget.LinearLayout') \
403 .click()
404 dut(text='Password :') \
405 .child(className='android.widget.EditText') \
406 .set_text('fairphoneviser2017')
407 dut(description='OK', className='android.widget.TextView') \
408 .click()
409 dut.press.back()
410 dut.press.back()
411
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100412
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100413def deploy():
414 serials = []
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100415
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100416 if len(sys.argv) > 1:
417 serials.append(sys.argv[1])
418 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200419 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100420
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100421 for serial in serials:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200422 print('Configuring device {}…'.format(serial))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100423
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100424 dut = Device(serial)
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200425 # Work around the not-so-easy Device class
426 dut.serial = serial
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100427
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200428 try:
429 # Make sure the screen stays on - we're going to use UI automation
430 force_awake(serial)
431 unlock(serial)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800432
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200433 # Configure common Fairphone Wi-Fi networks
434 configure_wifi_networks(dut, FAIRPHONE_WIFI_NETWORKS)
435
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200436 # Push the scenarios, their data, and install the apps
437 prepare_dut(
438 serial, '../scenarios', '../scenarios-data', PREBUILTS_PATH)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100439
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200440 # Start the viser app
441 adb(
Franz-Xaver Geiger223ae752018-02-19 14:54:08 +0100442 'shell', 'monkey', '-p', 'com.lunarlabs.panda', '-c',
443 'android.intent.category.LAUNCHER', '1', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100444
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200445 configure_perms(dut)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100446
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200447 # TODO DO NOT DO THE FOLLOWING IF NO SIM CARD IS IN THE DUT
448 # time.sleep(10)
449 # configure_sms(dut)
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100450
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200451 configure_settings(dut)
452 except (HostCommandError, DeviceCommandError) as e:
453 print('ERROR {}'.format(e), file=sys.stderr)
454 finally:
455 try:
456 # Leave the device alone now
457 force_awake(serial, always=False)
458 except DeviceCommandError as e:
459 print('WARNING {}'.format(e), file=sys.stderr)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800460
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100461
462if __name__ == '__main__':
463 deploy()