blob: f08920cd6b96daec3ca63253daea31732a4f22d5 [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
6import sys
7import subprocess
8import pathlib
9
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010010
11PREBUILTS_PATH = '../../vendor/smartviser/viser/prebuilts/apk'
12
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +020013PREBUILT_PROXY_APKS = {
14 'gms': 'com.lunarlabs.panda.proxy.apk',
15 'sibon': 'com.lunarlabs.panda.proxy-sibon.apk',
16}
17
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010018PREBUILT_APKS = [
19 'com.smartviser.demogame.apk',
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010020 'com.lunarlabs.panda.apk',
21]
22
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +020023FAIRPHONE_WIFI_NETWORKS = {
24 'Fairphone Guest': {'security': 'WPA/WPA2 PSK', 'password': 'fairwifi'},
25 'Fairphone DEV (2.4 GHz)': {
26 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
27 'Fairphone DEV (5 GHz)': {
28 'security': 'WPA/WPA2 PSK', 'password': 'fdev@adm'},
29}
30
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010031
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020032class HostCommandError(BaseException):
33 """An error happened while issuing a command on the host."""
34 def __init__(self, command, error_message):
35 self.command = command
36 self.error_message = error_message
37 message = 'Command `{}` failed: {}'.format(command, error_message)
38 super(HostCommandError, self).__init__(message)
39
40
41class DeviceCommandError(BaseException):
42 """An error happened while sending a command to a device."""
43 def __init__(self, serial, command, error_message):
44 self.serial = serial
45 self.command = command
46 self.error_message = error_message
47 message = 'Command `{}` failed on {}: {}'.format(
48 command, serial, error_message)
49 super(DeviceCommandError, self).__init__(message)
50
51
52def adb(*args, serial = None, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010053 """Run ADB command attached to serial.
54
55 Example:
56 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
57 >>> process.returncode
58 0
59 >>> process.stdout.strip()
60 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
61
62 :param *args:
63 List of options to ADB (including command).
64 :param str serial:
65 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020066 :param raise_on_error bool:
67 Whether to raise a DeviceCommandError exception if the return code is
68 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010069 :returns subprocess.CompletedProcess:
70 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020071 :raises DeviceCommandError:
72 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010073 """
74
75 # Make sure the adb server is started to avoid the infamous "out of date"
76 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020077 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010078 ['adb', 'start-server'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
79 universal_newlines=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020080 if ret.returncode < 0:
81 if raise_on_error:
82 raise DeviceCommandError(
83 serial if serial else '??', str(args), ret.stderr)
84 else:
85 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010086
87 command = ['adb']
88 if serial:
89 command += ['-s', serial]
90 if args:
91 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020092 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010093 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
94 universal_newlines=True)
95
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020096 if raise_on_error and ret.returncode < 0:
97 raise DeviceCommandError(
98 serial if serial else '??', str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010099
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200100 return ret
101
102
103def aapt(*args, raise_on_error = True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100104 """Run an AAPT command.
105
106 :param *args:
107 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200108 :param raise_on_error bool:
109 Whether to raise a DeviceCommandError exception if the return code is
110 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100111 :returns subprocess.CompletedProcess:
112 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200113 :raises HostCommandError:
114 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100115 """
116 command = ['aapt']
117 if args:
118 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200119 ret = subprocess.run(
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100120 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
121 universal_newlines=True)
122
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200123 if raise_on_error and ret.returncode < 0:
124 raise HostCommandError(str(args), ret.stderr)
125
126 return ret
127
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100128
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800129def force_awake(serial, always=True):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200130 """Force the device to stay awake.
131
132 Raises:
133 DeviceCommandError: If the underlying adb command failed.
134 """
135 adb('shell', 'svc power stayon {}'.format(
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800136 'true' if always else 'false'), serial=serial)
137
138
139def unlock(serial):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200140 """Wake-up the device and unlock it.
141
142 Raises:
143 DeviceCommandError: If the underlying adb commands failed.
144 """
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800145 adb('shell', 'input keyevent KEYCODE_POWER', serial=serial)
146 time.sleep(1)
147 adb('shell', 'input keyevent KEYCODE_MENU', serial=serial)
148 time.sleep(1)
149 adb('shell', 'input keyevent KEYCODE_HOME', serial=serial)
150
151
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200152def getprop(serial, key):
153 """Get system property of device.
154
155 Example:
156 >>> getprop('167eb6e8', 'ro.build.id')
157 'FP2-gms-18.02.0'
158
159 :param str serial:
160 Identifier for ADB connection to device.
161 :param str key:
162 Key of property to get.
163 :returns str:
164 Value of system property.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200165 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200166 """
167 process = adb('shell', 'getprop', key, serial=serial)
168 return process.stdout.strip()
169
170
171def is_gms_device(serial):
172 """Test if device runs GMS or sibon.
173
174 Example:
175 >>> is_gms_device('167eb6e8')
176 True
177
178 :param str serial:
179 Identifier for ADB connection to device.
180 :returns bool:
181 True if device runs GMS, false otherwise.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200182 :raise DeviceCommandError: If the underlying adb command failed.
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200183 """
184 return getprop(serial, 'ro.build.id').startswith('FP2-gms-')
185
186
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100187def uninstall_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200188 """Uninstall apk from prebuilts_dir on device.
189
190 Raises:
191 ValueError: If the package name could not be read from the apk.
192 DeviceCommandError: If the uninstall command failed.
193 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100194 ret = aapt('dump', 'badging', '{}/{}'.format(prebuilts_dir, filename))
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200195 package = None
196 for line in ret.stdout.splitlines():
197 if line.startswith('package'):
198 for token in line.split(' '):
199 if token.startswith('name='):
200 # Extract the package name out of the token
201 # (name='some.package.name')
202 package = token[6:-1]
203 break
204 if not package:
205 raise ValueError('Could not find package of app `{}`'.format(
206 filename))
207
208 adb('uninstall', package, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100209
210
211def install_apk(serial, filename, prebuilts_dir):
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200212 """Install apk from prebuilts_dir on device.
213
214 Raises:
215 DeviceCommandError: If the install command failed.
216 """
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100217 path = os.path.join(prebuilts_dir, filename)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200218 adb('install', '-r', path, serial=serial)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100219
220
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200221def configure_wifi_networks(dut, networks):
222 """Configure Wi-Fi networks.
223
224 The `networks` parameters is a list of networks to configure hashed by
225 their SSID. Each network value should have the following format:
226 - security (str): The security value as can be found in the Wi-Fi
227 settings dialog. Common values are 'None' and 'WPA/WPA2 PSK'.
228 - password (str, optional): The network password if the security is
229 'WPA/WPA2 PSK'.
230
231 Parameters:
232 dut (Device): The device object.
233 networks (dict(dict(str))): The list of networks to configure.
234 Raises:
235 DeviceCommandError: If the UI automation fails.
236 """
237 # Open the Wi-Fi settings
238 adb('shell', ('am start -a android.settings.WIFI_SETTINGS '
239 '--activity-clear-task'), serial=dut.serial)
240
241 # Make sure Wi-Fi is enabled
242 wifi_enabler = dut(text='OFF',
243 resourceId='com.android.settings:id/switch_widget')
244 if wifi_enabler.exists:
245 wifi_enabler.click()
246
247 # Check for registered networks
248 registered_networks = set()
249 dut(description='More options').click()
250 time.sleep(1)
251 saved_networks = dut(text='Saved networks')
252 if saved_networks.exists:
253 saved_networks.click()
254 for ssid in networks.keys():
255 if dut(text=ssid).exists:
256 registered_networks.add(ssid)
257 dut.press.back()
258
259 missing_networks = networks.keys() - registered_networks
260
261 for ssid in registered_networks:
262 print('Ignoring `{}` Wi-Fi network, already configured.'.format(ssid))
263
264 for ssid in missing_networks:
265 print('Configuring `{}` Wi-Fi network…'.format(ssid))
266
267 dut(description='More options').click()
268 dut(text='Add network').click()
269 dut(resourceId='com.android.settings:id/ssid') \
270 .set_text(ssid)
271 dut(resourceId='com.android.settings:id/security') \
272 .click()
273 dut(text=networks[ssid]['security']) \
274 .click()
275 password_field = dut(resourceId='com.android.settings:id/password')
276 if 'password' in networks[ssid] and networks[ssid]['password']:
277 if not password_field.exists:
278 dut(text='Cancel').click()
279 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
280 'missing password field')
281 password_field.set_text(networks[ssid]['password'])
282 elif password_field.exists:
283 dut(text='Cancel').click()
284 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
285 'missing password data')
286 save_button = dut(text='Save')
287 if not save_button.click():
288 dut(text='Cancel').click()
289 raise DeviceCommandError(dut, 'UI: add Wi-Fi',
290 'could not save network')
291
292 # Force the Wi-Fi on and off to pick the best network available
293 dut(text='ON', resourceId='com.android.settings:id/switch_widget').click()
294 dut(text='OFF', resourceId='com.android.settings:id/switch_widget').click()
295
296 # Leave the settings
297 dut.press.back()
298
299
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100300# Prepare the DUT
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100301def prepare_dut(serial, scenarios_dir, data_dir, prebuilts_dir):
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200302 flavour = 'gms' if is_gms_device(serial) else 'sibon'
303
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100304 # Uninstall the smartviser apps
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200305 for app in PREBUILT_APKS + [PREBUILT_PROXY_APKS[flavour]]:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200306 print('Uninstalling `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100307 uninstall_apk(serial, app, prebuilts_dir)
308
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100309 # Copy the scenarios
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200310 print('Pushing scenarios from `{}`…'.format(scenarios_dir))
311 adb('push', scenarios_dir, '/sdcard/viser', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100312
313 # Copy the scenarios data
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200314 print('Pushing scenarios data from `{}`…'.format(data_dir))
315 adb('push', data_dir, '/sdcard/viser/data', serial=serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100316
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200317 # Install the smartviser apps (starting with the proxy app)
318 for app in [PREBUILT_PROXY_APKS[flavour]] + PREBUILT_APKS:
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200319 print('Installing `{}`…'.format(app))
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100320 install_apk(serial, app, prebuilts_dir)
321
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100322
323# Grant the permissions through the UI
324def configure_perms(dut):
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200325 dut() \
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100326 .child_by_text('You need to grant access for the Permissions Group '
327 'Calendar Camera Contacts Microphone SMS Storage Telephone Location',
Borjan Tchakaloff6dad85d2018-04-11 13:55:25 +0200328 resourceId='android:id/content') \
329 .child(text='OK', className='android.widget.Button') \
330 .click()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100331
332 # Accept any permission that is asked for
333 prompt = dut(resourceId='com.android.packageinstaller:id/permission_allow_button',
334 className='android.widget.Button')
335 while prompt.exists:
336 prompt.click()
337
338 # Input the credentials
339 dut(resourceId='android:id/content') \
340 .child(text='Username') \
341 .child(className='android.widget.EditText') \
342 .set_text('borjan@fairphone.com')
343 dut(resourceId='android:id/content') \
344 .child(text='Password') \
345 .child(className='android.widget.EditText') \
346 .set_text('Rickisalso1niceguy!')
347
348 # Sign in
349 dut(resourceId='android:id/content') \
350 .child(text='Sign in', className='android.widget.Button') \
351 .click()
352
353def configure_sms(dut):
354 # TODO wait for the connection to be established and time-out
355 prompt = dut(resourceId='android:id/content') \
356 .child(text='Viser must be your SMS app to send messages')
357 while not prompt.exists:
358 time.sleep(1)
359
360 # Make viser the default SMS app
361 dut(resourceId='android:id/content') \
362 .child_by_text('Viser must be your SMS app to send messages',
363 className='android.widget.LinearLayout') \
364 .child(text='OK', className='android.widget.Button') \
365 .click()
366
367 dut(resourceId='android:id/content') \
368 .child_by_text('Change SMS app?', className='android.widget.LinearLayout') \
369 .child(text='Yes', className='android.widget.Button') \
370 .click()
371
372def configure_settings(dut):
373 # Set the e-mail account
374 dut(text='Settings', className='android.widget.TextView') \
375 .click()
376 dut(resourceId='android:id/list') \
377 .child_by_text('User settings', className='android.widget.LinearLayout') \
378 .click()
379 dut(resourceId='android:id/list') \
380 .child_by_text('Email account', className='android.widget.LinearLayout') \
381 .click()
382 prompt = dut(resourceId='android:id/content') \
383 .child_by_text('Email account', className='android.widget.LinearLayout')
384 prompt.child(resourceId='android:id/edit') \
385 .set_text('fairphone.viser@gmail.com')
386 prompt.child(text='OK', className='android.widget.Button') \
387 .click()
388 dut(resourceId='android:id/list') \
389 .child_by_text('Email password', className='android.widget.LinearLayout') \
390 .click()
391 dut(text='Password :') \
392 .child(className='android.widget.EditText') \
393 .set_text('fairphoneviser2017')
394 dut(description='OK', className='android.widget.TextView') \
395 .click()
396 dut.press.back()
397 dut.press.back()
398
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100399
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100400def deploy():
401 serials = []
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100402
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100403 if len(sys.argv) > 1:
404 serials.append(sys.argv[1])
405 else:
406 # List devices attached to adb
407 ret = subprocess.run(
408 "adb devices | grep -E '^[a-z0-9-]+\s+device$' | awk '{{print $1}}'",
409 shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
410 universal_newlines=True)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100411
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100412 if 0 < ret.returncode:
413 # TODO handle the error
414 raise Exception('Failed to list adb devices')
415 elif ret.stdout:
416 for line in ret.stdout.splitlines():
417 serial = line.strip()
418 if serial:
419 serials.append(serial)
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()