blob: 47bcf55d652a1653129c9394f4d6c318f9529611 [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
Borjan Tchakaloffd8445d32018-04-18 18:01:20 +020012VWS_CREDENTIALS = {
13 'user': 'fairphonetesting@gmail.com',
14 'password': 'aish3echi:uwaiSh'
15}
16
17
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010018PREBUILTS_PATH = '../../vendor/smartviser/viser/prebuilts/apk'
19
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +020020PREBUILT_PROXY_APKS = {
21 'gms': 'com.lunarlabs.panda.proxy.apk',
22 'sibon': 'com.lunarlabs.panda.proxy-sibon.apk',
23}
24
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010025PREBUILT_APKS = [
26 'com.smartviser.demogame.apk',
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010027 'com.lunarlabs.panda.apk',
28]
29
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +020030FAIRPHONE_WIFI_NETWORKS = {
31 'Fairphone Guest': {'security': 'WPA/WPA2 PSK', 'password': 'fairwifi'},
32 'Fairphone DEV (2.4 GHz)': {
33 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
34 'Fairphone DEV (5 GHz)': {
35 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
36}
37
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020038ADB_DEVICES_PATTERN = re.compile(r'^([a-z0-9-]+)\s+device$', flags=re.M)
39
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010040
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020041class HostCommandError(BaseException):
42 """An error happened while issuing a command on the host."""
43 def __init__(self, command, error_message):
44 self.command = command
45 self.error_message = error_message
46 message = 'Command `{}` failed: {}'.format(command, error_message)
47 super(HostCommandError, self).__init__(message)
48
49
50class DeviceCommandError(BaseException):
51 """An error happened while sending a command to a device."""
52 def __init__(self, serial, command, error_message):
53 self.serial = serial
54 self.command = command
55 self.error_message = error_message
56 message = 'Command `{}` failed on {}: {}'.format(
57 command, serial, error_message)
58 super(DeviceCommandError, self).__init__(message)
59
60
61def adb(*args, serial = None, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010062 """Run ADB command attached to serial.
63
64 Example:
65 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
66 >>> process.returncode
67 0
68 >>> process.stdout.strip()
69 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
70
71 :param *args:
72 List of options to ADB (including command).
73 :param str serial:
74 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020075 :param raise_on_error bool:
76 Whether to raise a DeviceCommandError exception if the return code is
77 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010078 :returns subprocess.CompletedProcess:
79 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020080 :raises DeviceCommandError:
81 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010082 """
83
84 # Make sure the adb server is started to avoid the infamous "out of date"
85 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020086 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010087 ['adb', 'start-server'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
88 universal_newlines=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020089 if ret.returncode < 0:
90 if raise_on_error:
91 raise DeviceCommandError(
92 serial if serial else '??', str(args), ret.stderr)
93 else:
94 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010095
96 command = ['adb']
97 if serial:
98 command += ['-s', serial]
99 if args:
100 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200101 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100102 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
103 universal_newlines=True)
104
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200105 if raise_on_error and ret.returncode < 0:
106 raise DeviceCommandError(
107 serial if serial else '??', str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100108
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200109 return ret
110
111
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200112def list_devices():
113 """List serial numbers of devices attached to adb.
114
115 Raises:
116 DeviceCommandError: If the underlying adb command failed.
117 """
118 process = adb('devices')
119 return ADB_DEVICES_PATTERN.findall(process.stdout)
120
121
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200122def aapt(*args, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100123 """Run an AAPT command.
124
125 :param *args:
126 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200127 :param raise_on_error bool:
128 Whether to raise a DeviceCommandError exception if the return code is
129 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100130 :returns subprocess.CompletedProcess:
131 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200132 :raises HostCommandError:
133 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100134 """
135 command = ['aapt']
136 if args:
137 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200138 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100139 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
140 universal_newlines=True)
141
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200142 if raise_on_error and ret.returncode < 0:
143 raise HostCommandError(str(args), ret.stderr)
144
145 return ret
146
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100147
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800148def force_awake(serial, always=True):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200149 """Force the device to stay awake.
150
151 Raises:
152 DeviceCommandError: If the underlying adb command failed.
153 """
154 adb('shell', 'svc power stayon {}'.format(
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800155 'true' if always else 'false'), serial=serial)
156
157
158def unlock(serial):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200159 """Wake-up the device and unlock it.
160
161 Raises:
162 DeviceCommandError: If the underlying adb commands failed.
163 """
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800164 adb('shell', 'input keyevent KEYCODE_POWER', serial=serial)
165 time.sleep(1)
166 adb('shell', 'input keyevent KEYCODE_MENU', serial=serial)
167 time.sleep(1)
168 adb('shell', 'input keyevent KEYCODE_HOME', serial=serial)
169
170
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200171def getprop(serial, key):
172 """Get system property of device.
173
174 Example:
175 >>> getprop('167eb6e8', 'ro.build.id')
176 'FP2-gms-18.02.0'
177
178 :param str serial:
179 Identifier for ADB connection to device.
180 :param str key:
181 Key of property to get.
182 :returns str:
183 Value of system property.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200184 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200185 """
186 process = adb('shell', 'getprop', key, serial=serial)
187 return process.stdout.strip()
188
189
190def is_gms_device(serial):
191 """Test if device runs GMS or sibon.
192
193 Example:
194 >>> is_gms_device('167eb6e8')
195 True
196
197 :param str serial:
198 Identifier for ADB connection to device.
199 :returns bool:
200 True if device runs GMS, false otherwise.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200201 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200202 """
203 return getprop(serial, 'ro.build.id').startswith('FP2-gms-')
204
205
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100206def uninstall_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200207 """Uninstall apk from prebuilts_dir on device.
208
209 Raises:
210 ValueError: If the package name could not be read from the apk.
211 DeviceCommandError: If the uninstall command failed.
212 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100213 ret = aapt('dump', 'badging', '{}/{}'.format(prebuilts_dir, filename))
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200214 package = None
215 for line in ret.stdout.splitlines():
216 if line.startswith('package'):
217 for token in line.split(' '):
218 if token.startswith('name='):
219 # Extract the package name out of the token
220 # (name='some.package.name')
221 package = token[6:-1]
222 break
223 if not package:
224 raise ValueError('Could not find package of app `{}`'.format(
225 filename))
226
227 adb('uninstall', package, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100228
229
230def install_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200231 """Install apk from prebuilts_dir on device.
232
233 Raises:
234 DeviceCommandError: If the install command failed.
235 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100236 path = os.path.join(prebuilts_dir, filename)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200237 adb('install', '-r', path, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100238
239
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200240def configure_wifi_networks(dut, networks):
241 """Configure Wi-Fi networks.
242
243 The `networks` parameters is a list of networks to configure hashed by
244 their SSID. Each network value should have the following format:
245 - security (str): The security value as can be found in the Wi-Fi
246 settings dialog. Common values are 'None' and 'WPA/WPA2 PSK'.
247 - password (str, optional): The network password if the security is
248 'WPA/WPA2 PSK'.
249
250 Parameters:
251 dut (Device): The device object.
252 networks (dict(dict(str))): The list of networks to configure.
253 Raises:
254 DeviceCommandError: If the UI automation fails.
255 """
256 # Open the Wi-Fi settings
257 adb('shell', ('am start -a android.settings.WIFI_SETTINGS '
258 '--activity-clear-task'), serial=dut.serial)
259
260 # Make sure Wi-Fi is enabled
261 wifi_enabler = dut(text='OFF',
262 resourceId='com.android.settings:id/switch_widget')
263 if wifi_enabler.exists:
264 wifi_enabler.click()
265
266 # Check for registered networks
267 registered_networks = set()
268 dut(description='More options').click()
269 time.sleep(1)
270 saved_networks = dut(text='Saved networks')
271 if saved_networks.exists:
272 saved_networks.click()
273 for ssid in networks.keys():
274 if dut(text=ssid).exists:
275 registered_networks.add(ssid)
276 dut.press.back()
277
278 missing_networks = networks.keys() - registered_networks
279
280 for ssid in registered_networks:
281 print('Ignoring `{}` Wi-Fi network, already configured.'.format(ssid))
282
283 for ssid in missing_networks:
284 print('Configuring `{}` Wi-Fi network…'.format(ssid))
285
286 dut(description='More options').click()
287 dut(text='Add network').click()
288 dut(resourceId='com.android.settings:id/ssid') \
289 .set_text(ssid)
290 dut(resourceId='com.android.settings:id/security') \
291 .click()
292 dut(text=networks[ssid]['security']) \
293 .click()
294 password_field = dut(resourceId='com.android.settings:id/password')
295 if 'password' in networks[ssid] and networks[ssid]['password']:
296 if not password_field.exists:
297 dut(text='Cancel').click()
298 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
299 'missing password field')
300 password_field.set_text(networks[ssid]['password'])
301 elif password_field.exists:
302 dut(text='Cancel').click()
303 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
304 'missing password data')
305 save_button = dut(text='Save')
306 if not save_button.click():
307 dut(text='Cancel').click()
308 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
309 'could not save network')
310
311 # Force the Wi-Fi on and off to pick the best network available
312 dut(text='ON', resourceId='com.android.settings:id/switch_widget').click()
313 dut(text='OFF', resourceId='com.android.settings:id/switch_widget').click()
314
315 # Leave the settings
316 dut.press.back()
317
318
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100319# Prepare the DUT
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100320def prepare_dut(serial, scenarios_dir, data_dir, prebuilts_dir):
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200321 flavour = 'gms' if is_gms_device(serial) else 'sibon'
322
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100323 # Uninstall the smartviser apps
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200324 for app in PREBUILT_APKS + [PREBUILT_PROXY_APKS[flavour]]:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200325 print('Uninstalling `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100326 uninstall_apk(serial, app, prebuilts_dir)
327
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100328 # Copy the scenarios
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200329 print('Pushing scenarios from `{}`…'.format(scenarios_dir))
330 adb('push', scenarios_dir, '/sdcard/viser', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100331
332 # Copy the scenarios data
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200333 print('Pushing scenarios data from `{}`…'.format(data_dir))
334 adb('push', data_dir, '/sdcard/viser/data', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100335
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200336 # Install the smartviser apps (starting with the proxy app)
337 for app in [PREBUILT_PROXY_APKS[flavour]] + PREBUILT_APKS:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200338 print('Installing `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100339 install_apk(serial, app, prebuilts_dir)
340
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100341
342# Grant the permissions through the UI
343def configure_perms(dut):
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200344 dut() \
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100345 .child_by_text('You need to grant access for the Permissions Group '
346 'Calendar Camera Contacts Microphone SMS Storage Telephone Location',
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200347 resourceId='android:id/content') \
348 .child(text='OK', className='android.widget.Button') \
349 .click()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100350
351 # Accept any permission that is asked for
352 prompt = dut(resourceId='com.android.packageinstaller:id/permission_allow_button',
353 className='android.widget.Button')
354 while prompt.exists:
355 prompt.click()
356
357 # Input the credentials
358 dut(resourceId='android:id/content') \
359 .child(text='Username') \
360 .child(className='android.widget.EditText') \
Borjan Tchakaloffd8445d32018-04-18 18:01:20 +0200361 .set_text(VWS_CREDENTIALS['user'])
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100362 dut(resourceId='android:id/content') \
363 .child(text='Password') \
364 .child(className='android.widget.EditText') \
Borjan Tchakaloffd8445d32018-04-18 18:01:20 +0200365 .set_text(VWS_CREDENTIALS['password'])
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100366
367 # Sign in
368 dut(resourceId='android:id/content') \
369 .child(text='Sign in', className='android.widget.Button') \
370 .click()
371
372def configure_sms(dut):
373 # TODO wait for the connection to be established and time-out
374 prompt = dut(resourceId='android:id/content') \
375 .child(text='Viser must be your SMS app to send messages')
376 while not prompt.exists:
377 time.sleep(1)
378
379 # Make viser the default SMS app
380 dut(resourceId='android:id/content') \
381 .child_by_text('Viser must be your SMS app to send messages',
382 className='android.widget.LinearLayout') \
383 .child(text='OK', className='android.widget.Button') \
384 .click()
385
386 dut(resourceId='android:id/content') \
387 .child_by_text('Change SMS app?', className='android.widget.LinearLayout') \
388 .child(text='Yes', className='android.widget.Button') \
389 .click()
390
391def configure_settings(dut):
392 # Set the e-mail account
393 dut(text='Settings', className='android.widget.TextView') \
394 .click()
395 dut(resourceId='android:id/list') \
396 .child_by_text('User settings', className='android.widget.LinearLayout') \
397 .click()
398 dut(resourceId='android:id/list') \
399 .child_by_text('Email account', className='android.widget.LinearLayout') \
400 .click()
401 prompt = dut(resourceId='android:id/content') \
402 .child_by_text('Email account', className='android.widget.LinearLayout')
403 prompt.child(resourceId='android:id/edit') \
404 .set_text('fairphone.viser@gmail.com')
405 prompt.child(text='OK', className='android.widget.Button') \
406 .click()
407 dut(resourceId='android:id/list') \
408 .child_by_text('Email password', className='android.widget.LinearLayout') \
409 .click()
410 dut(text='Password :') \
411 .child(className='android.widget.EditText') \
412 .set_text('fairphoneviser2017')
413 dut(description='OK', className='android.widget.TextView') \
414 .click()
415 dut.press.back()
416 dut.press.back()
417
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100418
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100419def deploy():
420 serials = []
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100421
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100422 if len(sys.argv) > 1:
423 serials.append(sys.argv[1])
424 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200425 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100426
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100427 for serial in serials:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200428 print('Configuring device {}…'.format(serial))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100429
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100430 dut = Device(serial)
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200431 # Work around the not-so-easy Device class
432 dut.serial = serial
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100433
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200434 try:
435 # Make sure the screen stays on - we're going to use UI automation
436 force_awake(serial)
437 unlock(serial)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800438
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200439 # Configure common Fairphone Wi-Fi networks
440 configure_wifi_networks(dut, FAIRPHONE_WIFI_NETWORKS)
441
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200442 # Push the scenarios, their data, and install the apps
443 prepare_dut(
444 serial, '../scenarios', '../scenarios-data', PREBUILTS_PATH)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100445
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200446 # Start the viser app
447 adb(
Franz-Xaver Geiger223ae752018-02-19 14:54:08 +0100448 'shell', 'monkey', '-p', 'com.lunarlabs.panda', '-c',
449 'android.intent.category.LAUNCHER', '1', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100450
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200451 configure_perms(dut)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100452
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200453 # TODO DO NOT DO THE FOLLOWING IF NO SIM CARD IS IN THE DUT
454 # time.sleep(10)
455 # configure_sms(dut)
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100456
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200457 configure_settings(dut)
458 except (HostCommandError, DeviceCommandError) as e:
459 print('ERROR {}'.format(e), file=sys.stderr)
460 finally:
461 try:
462 # Leave the device alone now
463 force_awake(serial, always=False)
464 except DeviceCommandError as e:
465 print('WARNING {}'.format(e), file=sys.stderr)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800466
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100467
468if __name__ == '__main__':
469 deploy()