blob: cd532584b1d30e60f6e8acedac4e1a15ae337356 [file] [log] [blame]
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01001#!/usr/bin/env python3
2
Borjan Tchakaloff7283b552020-02-07 16:40:47 +01003import contextlib
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +01004import dataclasses
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +01005import logging
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01006import pathlib
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +01007import re
8import subprocess
9import sys
10import time
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +010011from typing import Sequence
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010012
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +010013import uiautomator
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010014
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +010015_LOG = logging.getLogger(__name__)
16
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010017ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M)
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020018
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010019
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020020class HostCommandError(BaseException):
21 """An error happened while issuing a command on the host."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010022
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020023 def __init__(self, command, error_message):
24 self.command = command
25 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010026 message = "Command `{}` failed: {}".format(command, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020027 super(HostCommandError, self).__init__(message)
28
29
30class DeviceCommandError(BaseException):
31 """An error happened while sending a command to a device."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010032
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020033 def __init__(self, serial, command, error_message):
34 self.serial = serial
35 self.command = command
36 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010037 message = "Command `{}` failed on {}: {}".format(command, serial, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020038 super(DeviceCommandError, self).__init__(message)
39
40
Borjan Tchakaloff7283b552020-02-07 16:40:47 +010041class InvalidIntentError(DeviceCommandError):
42 """An intent was rejected by a device."""
43
44
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010045def adb(*args, serial=None, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010046 """Run ADB command attached to serial.
47
48 Example:
49 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
50 >>> process.returncode
51 0
52 >>> process.stdout.strip()
53 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
54
55 :param *args:
56 List of options to ADB (including command).
57 :param str serial:
58 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020059 :param raise_on_error bool:
60 Whether to raise a DeviceCommandError exception if the return code is
61 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010062 :returns subprocess.CompletedProcess:
63 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020064 :raises DeviceCommandError:
65 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010066 """
67
68 # Make sure the adb server is started to avoid the infamous "out of date"
69 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020070 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010071 ["adb", "start-server"],
72 stdout=subprocess.PIPE,
73 stderr=subprocess.PIPE,
74 universal_newlines=True,
75 )
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020076 if ret.returncode < 0:
77 if raise_on_error:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010078 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020079 else:
80 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010081
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010082 command = ["adb"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010083 if serial:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010084 command += ["-s", serial]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010085 if args:
86 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020087 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010088 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
89 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010090
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020091 if raise_on_error and ret.returncode < 0:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010092 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010093
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020094 return ret
95
96
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020097def list_devices():
98 """List serial numbers of devices attached to adb.
99
100 Raises:
101 DeviceCommandError: If the underlying adb command failed.
102 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100103 process = adb("devices")
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200104 return ADB_DEVICES_PATTERN.findall(process.stdout)
105
106
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100107def aapt(*args, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100108 """Run an AAPT command.
109
110 :param *args:
111 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200112 :param raise_on_error bool:
113 Whether to raise a DeviceCommandError exception if the return code is
114 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100115 :returns subprocess.CompletedProcess:
116 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200117 :raises HostCommandError:
118 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100119 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100120 command = ["aapt"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100121 if args:
122 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200123 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100124 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
125 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100126
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200127 if raise_on_error and ret.returncode < 0:
128 raise HostCommandError(str(args), ret.stderr)
129
130 return ret
131
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100132
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100133def disable_privacy_impact_popup(device):
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100134 """Disable Privacy Impact popup on Android 5.
135
136 This simplifies UI automation. Disabling the feature globally is more robust
137 than clicking through the the Privacy Impact screen per app.
138 """
Borjan Tchakaloff7283b552020-02-07 16:40:47 +0100139 _LOG.info("Disable the Privacy Impact screen")
140 with device.launch("com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"):
141 disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
142 if not disable_privacy_impact_checkbox.checked:
143 disable_privacy_impact_checkbox.click()
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100144
145
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100146class DeviceUnderTest:
147 """An Android device under test."""
148
149 serial: str
150 """The device serial number (adb/fastboot)."""
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100151 os_flavour: str
152 """The Fairphone-specific OS flavour."""
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100153 sdk: int
154 """The Android SDK version number."""
155 ui: uiautomator.Device
156 """The UI Automator handle piloting the device."""
157
158 def __init__(self, serial: str):
159 self.serial = serial
160 self.ui = uiautomator.Device(serial)
161
162 # Cache the Android SDK version
163 self.sdk = self.ui.info["sdkInt"]
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100164 # Cache the OS flavour
165 self.os_flavour = "gms" if self.is_gms_device() else "sibon"
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100166
167 def adb(self, *args) -> subprocess.CompletedProcess:
168 """Execute an adb command on this device.
169
170 :returns: The completed process.
171 :raise DeviceCommandError: If the underlying adb command failed.
172 """
173 return adb(*args, serial=self.serial)
174
175 def force_awake(self, always=True) -> None:
176 """Force the device to stay awake.
177
178 :raise DeviceCommandError: If the underlying adb command failed.
179 """
180 self.adb("shell", "svc power stayon {}".format("true" if always else "false"))
181
182 def unlock(self) -> None:
183 """Wake-up the device and unlock it.
184
185 :raise DeviceCommandError: If the underlying adb commands failed.
186 """
187 if not self.ui.info["screenOn"]:
188 self.adb("shell", "input keyevent KEYCODE_POWER")
189 time.sleep(1)
190 # The KEYCODE_MENU input is enough to unlock a "swipe up to unlock"
191 # lockscreen on Android 6, but unfortunately not Android 7. So we use a
192 # swipe up (that depends on the screen resolution) instead.
193 self.adb("shell", "input touchscreen swipe 930 880 930 380")
194 time.sleep(1)
195 self.adb("shell", "input keyevent KEYCODE_HOME")
196
197 def getprop(self, key: str) -> str:
198 """Get a system property.
199
200 Example:
201 >>> self.getprop('ro.build.id')
202 'FP2-gms-18.02.0'
203
204 :param key: Key of property to get.
205 :returns: Value of system property.
206 :raise DeviceCommandError: If the underlying adb command failed.
207 """
208 process = self.adb("shell", "getprop", key)
209 return process.stdout.strip()
210
211 def is_gms_device(self) -> bool:
212 """Whether the device runs GMS or sibon.
213
214 Example:
215 >>> self.is_gms_device()
216 True
217
218 :returns: True if device runs GMS, false otherwise.
219 :raise DeviceCommandError: If the underlying adb command failed.
220 """
221 return self.getprop("ro.build.id").startswith("FP2-gms-") or self.getprop(
222 "ro.build.version.incremental"
223 ).startswith("gms-")
224
225 def uninstall(self, apk: pathlib.Path) -> None:
226 """Uninstall an app from an APK file.
227
228 :raise ValueError: If the package name could not be read from the apk.
229 :raise DeviceCommandError: If the uninstall command failed.
230 """
231 ret = aapt("dump", "badging", str(apk))
232 package = None
233 for line in ret.stdout.splitlines():
234 if line.startswith("package"):
235 for token in line.split(" "):
236 if token.startswith("name="):
237 # Extract the package name out of the token
238 # (name='some.package.name')
239 package = token[6:-1]
240 break
241 if not package:
242 raise ValueError("Could not find package of app `{}`".format(apk.name))
243
244 self.adb("uninstall", package)
245
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100246 def uninstall_package(self, package_name: str) -> None:
247 """Uninstall an app from its package name.
248
249 :raise DeviceCommandError: If the uninstall command failed.
250 """
251 self.adb("uninstall", package_name)
252
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100253 def install(self, apk: pathlib.Path) -> None:
254 """Install an app from an APK file.
255
256 :raise DeviceCommandError: If the install command failed.
257 """
258 command = ["install", "-r"]
259 if self.sdk >= 23:
260 # From Marshmallow onwards, adb has a flag to grant default permissions
261 command.append("-g")
262
263 self.adb(*command, str(apk))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100264
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100265 def push(self, source: pathlib.Path, target: pathlib.Path) -> None:
266 """Push a file or directory to the device.
267
268 The target directory (if the source is a directory), or its
269 parent (if the source if a file) will be created on the device,
270 always.
271
272 :raise DeviceCommandError: If the push command failed.
273 """
274 if source.is_dir():
275 # `adb push` skips empty directories so we have to manually create the
276 # target directory itself.
277 self.adb("shell", "mkdir", "-p", str(target))
278 # `adb push` expects the target to be the host directory not the target
279 # directory itself. So we'll just copy the source directory content instead.
280 sources = [str(child) for child in source.iterdir()]
281 self.adb("push", *sources, str(target))
282 else:
283 self.adb("shell", "mkdir", "-p", str(target.parent))
284 self.adb("push", str(source), str(target))
285
286 def remove(self, target: pathlib.Path, *, recurse: bool = False) -> None:
287 """Remove a file or directory from the device.
288
289 :raise DeviceCommandError: If the remove command failed.
290 """
291 command = ["shell", "rm", "-f"]
292 if recurse:
293 command.append("-r")
294 command.append(str(target))
295 self.adb(*command)
296
Borjan Tchakaloff7283b552020-02-07 16:40:47 +0100297 @contextlib.contextmanager
298 def launch(self, package_or_activity: str):
299 """Launch an app and eventually goes back to the home screen.
300
301 There are two ways to start an application:
302
303 1. Start the main activity (advertised in the launcher), only
304 the package name is needed (e.g. `com.android.settings`);
305 2. Start a specific activity, the qualified activity name is
306 needed (e.g. `com.android.settings/.Settings`).
307
308 Example:
309
310 >>> device = DeviceUnderTest(...)
311 >>> with device.launch("com.android.settings"):
312 ... device.ui(text="Bluetooth").exists
313 True
314 >>> device.ui(text="Bluetooth").exists
315 False
316
317 :param package_or_activity: The package name or the qualified
318 activity name.
319 :raise DeviceCommandError: If the app manager command failed.
320 :raise InvalidIntentError: If the package or activity could not
321 be resolved.
322 """
323 if "/" in package_or_activity:
324 [package, activity] = package_or_activity.split("/", maxsplit=1)
325 else:
326 package, activity = package_or_activity, ""
327
328 if activity:
329 command = [
330 "shell",
331 "am",
332 "start",
333 "-a",
334 "android.intent.action.MAIN",
335 package_or_activity,
336 ]
337 else:
338 # Use the monkey to avoid specifying the exact activity name.
339 command = [
340 "shell",
341 "monkey",
342 "-p",
343 package,
344 "-c",
345 "android.intent.category.LAUNCHER",
346 "1",
347 ]
348
349 try:
350 process = self.adb(*command)
351 if process.stderr:
352 raise InvalidIntentError(self.serial, " ".join(command), process.stderr)
353 yield
354 finally:
355 self.ui.press.home()
356
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100357
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100358@dataclasses.dataclass(frozen=True)
359class AndroidApp:
360 """An installable Android app."""
361
362 package: str
363 """The app package name."""
364 apk: str
365 """The app package (APK) filename."""
366
367 def __str__(self) -> str:
368 return self.package
369
370
371@dataclasses.dataclass(frozen=True)
372class ViserProxyApp:
373 """A template for the viSer Proxy app."""
374
375 package: str
376 """The app package name."""
377 apk_filename_template: str
378 """The string template of the APK filename, in the `str.format()` style (`{}`).
379
380 The following place-holders can be used and will be replaced at runtime:
381 - `{sdk}`: The Android SDK number, e.g. `25` for Android 7.1.
382 - `{flavour}`: The Fairphone-specific Android build flavour, e.g. `gms`
383 (Fairphone OS) or `sibon` (Fairphone Open).
384 """
385
386 def resolve(self, *, sdk: int, flavour: str) -> AndroidApp:
387 """Resolve the app template into a Viser App."""
388 if sdk >= 24:
389 sdk = 24
390 else:
391 sdk = 19
392 return AndroidApp(
393 self.package, self.apk_filename_template.format(sdk=sdk, flavour=flavour)
394 )
395
396
397@dataclasses.dataclass(frozen=True)
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100398class Credentials:
399 """Credentials to access a service."""
400
401 username: str
402 password: str
403
404
405@dataclasses.dataclass(frozen=True)
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100406class ViserSuite:
407 """A SmartViser viSer app suite.
408
409 Example:
410
411 >>> device = DeviceUnderTest(...)
412 >>> suite = ViserSuite(...)
413 >>> suite.deploy(device)
414
415 """
416
417 VISER_PROXY_APP_TEMPLATE = ViserProxyApp(
418 "com.lunarlabs.panda.proxy",
419 "com.lunarlabs.panda.proxy-latest-sdk{sdk}-{flavour}.apk",
420 )
421 ANDROID_APPS = [
422 AndroidApp("com.smartviser.demogame", "com.smartviser.demogame-latest.apk"),
423 AndroidApp("com.lunarlabs.panda", "com.lunarlabs.panda-latest.apk"),
424 ]
425
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100426 credentials: Credentials
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100427 prebuilts_path: pathlib.Path
428 scenarios_path: pathlib.Path
429 scenarios_data_path: pathlib.Path
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100430 settings_file: pathlib.Path
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100431 target_path: pathlib.Path = pathlib.Path("/sdcard/Viser")
432
433 @property
434 def target_scenarios_path(self) -> pathlib.Path:
435 return self.target_path
436
437 @property
438 def target_scenarios_data_path(self) -> pathlib.Path:
439 return self.target_path / "Data"
440
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100441 @property
442 def target_settings_file(self) -> pathlib.Path:
443 return self.target_path / "Settings" / self.settings_file.name
444
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100445 def resolve_apps(self, device: DeviceUnderTest) -> Sequence[AndroidApp]:
446 """Resolve the apps based on the target device properties.
447
448 :param device: The device to target.
449 :returns: The sequence of apps suitable for the target device.
450 The sequence is ordered to satisfy the dependency graph
451 (i.e. required apps come first in the sequence).
452 """
453 return [
454 self.VISER_PROXY_APP_TEMPLATE.resolve(
455 sdk=device.sdk, flavour=device.os_flavour
456 )
457 ] + self.ANDROID_APPS
458
459 def deploy(self, device: DeviceUnderTest) -> None:
460 """Deploy the suite on a device.
461
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100462 Copy the test scenarios and their data, install the
463 different apps composing the suite, and configure the suite.
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100464
465 The previous configuration and data (i.e. reports) tied to the
466 app suite, if any, is deleted before hand.
467
468 :param device: The device to deploy the suite to.
469 """
470 self.cleanup_previous_deployment(device)
471 self.copy_scenarios(device)
472 self.install_suite(device)
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100473 self.configure_suite(device)
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100474
475 def cleanup_previous_deployment(self, device: DeviceUnderTest) -> None:
476 """Clean-up a previous deployment of the suite on a device.
477
478 :param device: The device to clean-up.
479 """
480 # Uninstall the apps in the reverse order to cater for dependencies.
481 for app in reversed(self.resolve_apps(device)):
482 _LOG.info("Uninstall %s", app)
483 device.uninstall_package(app.package)
484
485 _LOG.info("Delete data from previous deployment")
486 device.remove(self.target_path, recurse=True)
487
488 def copy_scenarios(self, device: DeviceUnderTest) -> None:
489 """Copy the suite scenarios and their data on a device.
490
491 :param device: The device to copy the scenarios to.
492 """
493 _LOG.info(
494 "Copy scenarios: %s → %s", self.scenarios_path, self.target_scenarios_path
495 )
496 device.push(self.scenarios_path, self.target_scenarios_path)
497
498 _LOG.info(
499 "Copy scenarios data: %s → %s",
500 self.scenarios_data_path,
501 self.target_scenarios_data_path,
502 )
503 device.push(self.scenarios_path, self.target_scenarios_data_path)
504
505 def install_suite(self, device: DeviceUnderTest) -> None:
506 """Install the suite apps on a device.
507
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100508 Input the credentials in the viSer app to authenticate the
509 device.
510
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100511 :param device: The device to install the suite apps on.
512 """
513 for app in self.resolve_apps(device):
514 _LOG.info("Install %s", app)
515 device.install(self.prebuilts_path / app.apk)
516
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100517 with device.launch("com.lunarlabs.panda"):
518 # Input the credentials
519 device.ui(resourceId="android:id/content").child(text="Username").child(
520 className="android.widget.EditText"
521 ).set_text(self.credentials.username)
522 device.ui(resourceId="android:id/content").child(text="Password").child(
523 className="android.widget.EditText"
524 ).set_text(self.credentials.password)
525
526 # Sign in
527 signin_label = "SIGN IN" if device.sdk >= 24 else "Sign in"
528 device.ui(resourceId="android:id/content").child(
529 text=signin_label, className="android.widget.Button"
530 ).click()
531
532 def configure_suite(self, device: DeviceUnderTest) -> None:
533 """Configure the suite on a device.
534
535 :param device: The device to configure the suite on.
536 """
537 _LOG.info(
538 "Copy settings: %s → %s", self.settings_file, self.target_settings_file
539 )
540 device.push(self.settings_file, self.target_settings_file)
541
542 with device.launch("com.lunarlabs.panda"):
543 device.ui(text="Settings", className="android.widget.TextView").click()
544 device.ui(resourceId="android:id/list").child_by_text(
545 "Settings management", className="android.widget.LinearLayout"
546 ).click()
547 device.ui(resourceId="android:id/list").child_by_text(
548 "Load settings", className="android.widget.LinearLayout"
549 ).click()
550 device.ui(resourceId="android:id/list").child_by_text(
551 self.settings_file.name, className="android.widget.TextView"
552 ).click()
553 device.ui(
554 textMatches="(?i)Select", className="android.widget.Button"
555 ).click()
556
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100557
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100558def deploy():
559 serials = []
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100560 suite = ViserSuite(
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100561 Credentials("fairphonetesting@gmail.com", "aish3echi:uwaiSh"),
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100562 pathlib.Path("../../vendor/smartviser/viser/prebuilts/apk"),
563 pathlib.Path("../scenarios"),
564 pathlib.Path("../scenarios-data"),
Borjan Tchakaloff275978c2020-02-07 14:01:36 +0100565 pathlib.Path("settings/fairphone-bot_settings"),
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100566 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100567
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100568 if len(sys.argv) > 1:
569 serials.append(sys.argv[1])
570 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200571 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100572
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100573 for serial in serials:
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100574 _LOG.info("Deploy to device %s", serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100575
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100576 device = DeviceUnderTest(serial)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200577 try:
578 # Make sure the screen stays on - we're going to use UI automation
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100579 device.force_awake()
580 device.unlock()
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800581
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100582 # Disable Privacy Impact popup on Android 5.
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100583 if device.sdk <= 22:
584 disable_privacy_impact_popup(device)
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100585
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200586 # Push the scenarios, their data, and install the apps
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100587 suite.deploy(device)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100588 except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
589 _LOG.error("Failed to execute deployment", exc_info=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200590 finally:
591 try:
592 # Leave the device alone now
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100593 device.force_awake(always=False)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100594 except DeviceCommandError:
595 _LOG.warning("Failed to tear down device", exc_info=True)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800596
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100597
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100598if __name__ == "__main__":
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100599 logging.basicConfig(
600 format="%(asctime)-15s:%(levelname)s:%(message)s", level=logging.INFO
601 )
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100602 deploy()