blob: b49967b347f9b694e8d99a9672361a91583f17f8 [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 Tchakaloff9c59fb82020-02-07 11:49:41 +010017VWS_CREDENTIALS = {"user": "fairphonetesting@gmail.com", "password": "aish3echi:uwaiSh"}
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010018
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010019ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M)
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020020
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010021
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020022class HostCommandError(BaseException):
23 """An error happened while issuing a command on the host."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010024
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020025 def __init__(self, command, error_message):
26 self.command = command
27 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010028 message = "Command `{}` failed: {}".format(command, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020029 super(HostCommandError, self).__init__(message)
30
31
32class DeviceCommandError(BaseException):
33 """An error happened while sending a command to a device."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010034
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020035 def __init__(self, serial, command, error_message):
36 self.serial = serial
37 self.command = command
38 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010039 message = "Command `{}` failed on {}: {}".format(command, serial, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020040 super(DeviceCommandError, self).__init__(message)
41
42
Borjan Tchakaloff7283b552020-02-07 16:40:47 +010043class InvalidIntentError(DeviceCommandError):
44 """An intent was rejected by a device."""
45
46
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010047def adb(*args, serial=None, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010048 """Run ADB command attached to serial.
49
50 Example:
51 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
52 >>> process.returncode
53 0
54 >>> process.stdout.strip()
55 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
56
57 :param *args:
58 List of options to ADB (including command).
59 :param str serial:
60 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020061 :param raise_on_error bool:
62 Whether to raise a DeviceCommandError exception if the return code is
63 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010064 :returns subprocess.CompletedProcess:
65 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020066 :raises DeviceCommandError:
67 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010068 """
69
70 # Make sure the adb server is started to avoid the infamous "out of date"
71 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020072 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010073 ["adb", "start-server"],
74 stdout=subprocess.PIPE,
75 stderr=subprocess.PIPE,
76 universal_newlines=True,
77 )
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020078 if ret.returncode < 0:
79 if raise_on_error:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010080 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020081 else:
82 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010083
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010084 command = ["adb"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010085 if serial:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010086 command += ["-s", serial]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010087 if args:
88 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020089 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010090 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
91 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010092
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020093 if raise_on_error and ret.returncode < 0:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010094 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010095
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020096 return ret
97
98
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020099def list_devices():
100 """List serial numbers of devices attached to adb.
101
102 Raises:
103 DeviceCommandError: If the underlying adb command failed.
104 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100105 process = adb("devices")
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200106 return ADB_DEVICES_PATTERN.findall(process.stdout)
107
108
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100109def aapt(*args, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100110 """Run an AAPT command.
111
112 :param *args:
113 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200114 :param raise_on_error bool:
115 Whether to raise a DeviceCommandError exception if the return code is
116 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100117 :returns subprocess.CompletedProcess:
118 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200119 :raises HostCommandError:
120 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100121 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100122 command = ["aapt"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100123 if args:
124 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200125 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100126 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
127 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100128
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200129 if raise_on_error and ret.returncode < 0:
130 raise HostCommandError(str(args), ret.stderr)
131
132 return ret
133
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100134
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100135def disable_privacy_impact_popup(device):
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100136 """Disable Privacy Impact popup on Android 5.
137
138 This simplifies UI automation. Disabling the feature globally is more robust
139 than clicking through the the Privacy Impact screen per app.
140 """
Borjan Tchakaloff7283b552020-02-07 16:40:47 +0100141 _LOG.info("Disable the Privacy Impact screen")
142 with device.launch("com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"):
143 disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
144 if not disable_privacy_impact_checkbox.checked:
145 disable_privacy_impact_checkbox.click()
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100146
147
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100148# Grant the permissions through the UI
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100149def configure_perms(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100150 # Input the credentials
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100151 device.ui(resourceId="android:id/content").child(text="Username").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100152 className="android.widget.EditText"
153 ).set_text(VWS_CREDENTIALS["user"])
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100154 device.ui(resourceId="android:id/content").child(text="Password").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100155 className="android.widget.EditText"
156 ).set_text(VWS_CREDENTIALS["password"])
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100157
158 # Sign in
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100159 signin_label = "SIGN IN" if device.sdk >= 24 else "Sign in"
160 device.ui(resourceId="android:id/content").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100161 text=signin_label, className="android.widget.Button"
162 ).click()
163
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100164
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100165def configure_settings(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100166 # Set the e-mail account
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100167 device.ui(text="Settings", className="android.widget.TextView").click()
168 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100169 "User settings", className="android.widget.LinearLayout"
170 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100171 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100172 "Email account", className="android.widget.LinearLayout"
173 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100174 prompt = device.ui(resourceId="android:id/content").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100175 "Email account", className="android.widget.LinearLayout"
176 )
177 prompt.child(resourceId="android:id/edit").set_text("fairphone.viser@gmail.com")
178 prompt.child(text="OK", className="android.widget.Button").click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100179 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100180 "Email password", className="android.widget.LinearLayout"
181 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100182 device.ui(text="Password :").child(className="android.widget.EditText").set_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100183 "fairphoneviser2017"
184 )
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100185 device.ui(description="OK", className="android.widget.TextView").click()
186 device.ui.press.back()
187 device.ui.press.back()
188
189
190class DeviceUnderTest:
191 """An Android device under test."""
192
193 serial: str
194 """The device serial number (adb/fastboot)."""
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100195 os_flavour: str
196 """The Fairphone-specific OS flavour."""
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100197 sdk: int
198 """The Android SDK version number."""
199 ui: uiautomator.Device
200 """The UI Automator handle piloting the device."""
201
202 def __init__(self, serial: str):
203 self.serial = serial
204 self.ui = uiautomator.Device(serial)
205
206 # Cache the Android SDK version
207 self.sdk = self.ui.info["sdkInt"]
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100208 # Cache the OS flavour
209 self.os_flavour = "gms" if self.is_gms_device() else "sibon"
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100210
211 def adb(self, *args) -> subprocess.CompletedProcess:
212 """Execute an adb command on this device.
213
214 :returns: The completed process.
215 :raise DeviceCommandError: If the underlying adb command failed.
216 """
217 return adb(*args, serial=self.serial)
218
219 def force_awake(self, always=True) -> None:
220 """Force the device to stay awake.
221
222 :raise DeviceCommandError: If the underlying adb command failed.
223 """
224 self.adb("shell", "svc power stayon {}".format("true" if always else "false"))
225
226 def unlock(self) -> None:
227 """Wake-up the device and unlock it.
228
229 :raise DeviceCommandError: If the underlying adb commands failed.
230 """
231 if not self.ui.info["screenOn"]:
232 self.adb("shell", "input keyevent KEYCODE_POWER")
233 time.sleep(1)
234 # The KEYCODE_MENU input is enough to unlock a "swipe up to unlock"
235 # lockscreen on Android 6, but unfortunately not Android 7. So we use a
236 # swipe up (that depends on the screen resolution) instead.
237 self.adb("shell", "input touchscreen swipe 930 880 930 380")
238 time.sleep(1)
239 self.adb("shell", "input keyevent KEYCODE_HOME")
240
241 def getprop(self, key: str) -> str:
242 """Get a system property.
243
244 Example:
245 >>> self.getprop('ro.build.id')
246 'FP2-gms-18.02.0'
247
248 :param key: Key of property to get.
249 :returns: Value of system property.
250 :raise DeviceCommandError: If the underlying adb command failed.
251 """
252 process = self.adb("shell", "getprop", key)
253 return process.stdout.strip()
254
255 def is_gms_device(self) -> bool:
256 """Whether the device runs GMS or sibon.
257
258 Example:
259 >>> self.is_gms_device()
260 True
261
262 :returns: True if device runs GMS, false otherwise.
263 :raise DeviceCommandError: If the underlying adb command failed.
264 """
265 return self.getprop("ro.build.id").startswith("FP2-gms-") or self.getprop(
266 "ro.build.version.incremental"
267 ).startswith("gms-")
268
269 def uninstall(self, apk: pathlib.Path) -> None:
270 """Uninstall an app from an APK file.
271
272 :raise ValueError: If the package name could not be read from the apk.
273 :raise DeviceCommandError: If the uninstall command failed.
274 """
275 ret = aapt("dump", "badging", str(apk))
276 package = None
277 for line in ret.stdout.splitlines():
278 if line.startswith("package"):
279 for token in line.split(" "):
280 if token.startswith("name="):
281 # Extract the package name out of the token
282 # (name='some.package.name')
283 package = token[6:-1]
284 break
285 if not package:
286 raise ValueError("Could not find package of app `{}`".format(apk.name))
287
288 self.adb("uninstall", package)
289
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100290 def uninstall_package(self, package_name: str) -> None:
291 """Uninstall an app from its package name.
292
293 :raise DeviceCommandError: If the uninstall command failed.
294 """
295 self.adb("uninstall", package_name)
296
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100297 def install(self, apk: pathlib.Path) -> None:
298 """Install an app from an APK file.
299
300 :raise DeviceCommandError: If the install command failed.
301 """
302 command = ["install", "-r"]
303 if self.sdk >= 23:
304 # From Marshmallow onwards, adb has a flag to grant default permissions
305 command.append("-g")
306
307 self.adb(*command, str(apk))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100308
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100309 def push(self, source: pathlib.Path, target: pathlib.Path) -> None:
310 """Push a file or directory to the device.
311
312 The target directory (if the source is a directory), or its
313 parent (if the source if a file) will be created on the device,
314 always.
315
316 :raise DeviceCommandError: If the push command failed.
317 """
318 if source.is_dir():
319 # `adb push` skips empty directories so we have to manually create the
320 # target directory itself.
321 self.adb("shell", "mkdir", "-p", str(target))
322 # `adb push` expects the target to be the host directory not the target
323 # directory itself. So we'll just copy the source directory content instead.
324 sources = [str(child) for child in source.iterdir()]
325 self.adb("push", *sources, str(target))
326 else:
327 self.adb("shell", "mkdir", "-p", str(target.parent))
328 self.adb("push", str(source), str(target))
329
330 def remove(self, target: pathlib.Path, *, recurse: bool = False) -> None:
331 """Remove a file or directory from the device.
332
333 :raise DeviceCommandError: If the remove command failed.
334 """
335 command = ["shell", "rm", "-f"]
336 if recurse:
337 command.append("-r")
338 command.append(str(target))
339 self.adb(*command)
340
Borjan Tchakaloff7283b552020-02-07 16:40:47 +0100341 @contextlib.contextmanager
342 def launch(self, package_or_activity: str):
343 """Launch an app and eventually goes back to the home screen.
344
345 There are two ways to start an application:
346
347 1. Start the main activity (advertised in the launcher), only
348 the package name is needed (e.g. `com.android.settings`);
349 2. Start a specific activity, the qualified activity name is
350 needed (e.g. `com.android.settings/.Settings`).
351
352 Example:
353
354 >>> device = DeviceUnderTest(...)
355 >>> with device.launch("com.android.settings"):
356 ... device.ui(text="Bluetooth").exists
357 True
358 >>> device.ui(text="Bluetooth").exists
359 False
360
361 :param package_or_activity: The package name or the qualified
362 activity name.
363 :raise DeviceCommandError: If the app manager command failed.
364 :raise InvalidIntentError: If the package or activity could not
365 be resolved.
366 """
367 if "/" in package_or_activity:
368 [package, activity] = package_or_activity.split("/", maxsplit=1)
369 else:
370 package, activity = package_or_activity, ""
371
372 if activity:
373 command = [
374 "shell",
375 "am",
376 "start",
377 "-a",
378 "android.intent.action.MAIN",
379 package_or_activity,
380 ]
381 else:
382 # Use the monkey to avoid specifying the exact activity name.
383 command = [
384 "shell",
385 "monkey",
386 "-p",
387 package,
388 "-c",
389 "android.intent.category.LAUNCHER",
390 "1",
391 ]
392
393 try:
394 process = self.adb(*command)
395 if process.stderr:
396 raise InvalidIntentError(self.serial, " ".join(command), process.stderr)
397 yield
398 finally:
399 self.ui.press.home()
400
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100401
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100402@dataclasses.dataclass(frozen=True)
403class AndroidApp:
404 """An installable Android app."""
405
406 package: str
407 """The app package name."""
408 apk: str
409 """The app package (APK) filename."""
410
411 def __str__(self) -> str:
412 return self.package
413
414
415@dataclasses.dataclass(frozen=True)
416class ViserProxyApp:
417 """A template for the viSer Proxy app."""
418
419 package: str
420 """The app package name."""
421 apk_filename_template: str
422 """The string template of the APK filename, in the `str.format()` style (`{}`).
423
424 The following place-holders can be used and will be replaced at runtime:
425 - `{sdk}`: The Android SDK number, e.g. `25` for Android 7.1.
426 - `{flavour}`: The Fairphone-specific Android build flavour, e.g. `gms`
427 (Fairphone OS) or `sibon` (Fairphone Open).
428 """
429
430 def resolve(self, *, sdk: int, flavour: str) -> AndroidApp:
431 """Resolve the app template into a Viser App."""
432 if sdk >= 24:
433 sdk = 24
434 else:
435 sdk = 19
436 return AndroidApp(
437 self.package, self.apk_filename_template.format(sdk=sdk, flavour=flavour)
438 )
439
440
441@dataclasses.dataclass(frozen=True)
442class ViserSuite:
443 """A SmartViser viSer app suite.
444
445 Example:
446
447 >>> device = DeviceUnderTest(...)
448 >>> suite = ViserSuite(...)
449 >>> suite.deploy(device)
450
451 """
452
453 VISER_PROXY_APP_TEMPLATE = ViserProxyApp(
454 "com.lunarlabs.panda.proxy",
455 "com.lunarlabs.panda.proxy-latest-sdk{sdk}-{flavour}.apk",
456 )
457 ANDROID_APPS = [
458 AndroidApp("com.smartviser.demogame", "com.smartviser.demogame-latest.apk"),
459 AndroidApp("com.lunarlabs.panda", "com.lunarlabs.panda-latest.apk"),
460 ]
461
462 prebuilts_path: pathlib.Path
463 scenarios_path: pathlib.Path
464 scenarios_data_path: pathlib.Path
465 target_path: pathlib.Path = pathlib.Path("/sdcard/Viser")
466
467 @property
468 def target_scenarios_path(self) -> pathlib.Path:
469 return self.target_path
470
471 @property
472 def target_scenarios_data_path(self) -> pathlib.Path:
473 return self.target_path / "Data"
474
475 def resolve_apps(self, device: DeviceUnderTest) -> Sequence[AndroidApp]:
476 """Resolve the apps based on the target device properties.
477
478 :param device: The device to target.
479 :returns: The sequence of apps suitable for the target device.
480 The sequence is ordered to satisfy the dependency graph
481 (i.e. required apps come first in the sequence).
482 """
483 return [
484 self.VISER_PROXY_APP_TEMPLATE.resolve(
485 sdk=device.sdk, flavour=device.os_flavour
486 )
487 ] + self.ANDROID_APPS
488
489 def deploy(self, device: DeviceUnderTest) -> None:
490 """Deploy the suite on a device.
491
492 Copy the test scenarios and their data, and install the
493 different apps composing the suite.
494
495 The previous configuration and data (i.e. reports) tied to the
496 app suite, if any, is deleted before hand.
497
498 :param device: The device to deploy the suite to.
499 """
500 self.cleanup_previous_deployment(device)
501 self.copy_scenarios(device)
502 self.install_suite(device)
503
504 def cleanup_previous_deployment(self, device: DeviceUnderTest) -> None:
505 """Clean-up a previous deployment of the suite on a device.
506
507 :param device: The device to clean-up.
508 """
509 # Uninstall the apps in the reverse order to cater for dependencies.
510 for app in reversed(self.resolve_apps(device)):
511 _LOG.info("Uninstall %s", app)
512 device.uninstall_package(app.package)
513
514 _LOG.info("Delete data from previous deployment")
515 device.remove(self.target_path, recurse=True)
516
517 def copy_scenarios(self, device: DeviceUnderTest) -> None:
518 """Copy the suite scenarios and their data on a device.
519
520 :param device: The device to copy the scenarios to.
521 """
522 _LOG.info(
523 "Copy scenarios: %s → %s", self.scenarios_path, self.target_scenarios_path
524 )
525 device.push(self.scenarios_path, self.target_scenarios_path)
526
527 _LOG.info(
528 "Copy scenarios data: %s → %s",
529 self.scenarios_data_path,
530 self.target_scenarios_data_path,
531 )
532 device.push(self.scenarios_path, self.target_scenarios_data_path)
533
534 def install_suite(self, device: DeviceUnderTest) -> None:
535 """Install the suite apps on a device.
536
537 :param device: The device to install the suite apps on.
538 """
539 for app in self.resolve_apps(device):
540 _LOG.info("Install %s", app)
541 device.install(self.prebuilts_path / app.apk)
542
543
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100544def deploy():
545 serials = []
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100546 suite = ViserSuite(
547 pathlib.Path("../../vendor/smartviser/viser/prebuilts/apk"),
548 pathlib.Path("../scenarios"),
549 pathlib.Path("../scenarios-data"),
550 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100551
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100552 if len(sys.argv) > 1:
553 serials.append(sys.argv[1])
554 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200555 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100556
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100557 for serial in serials:
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100558 _LOG.info("Deploy to device %s", serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100559
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100560 device = DeviceUnderTest(serial)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200561 try:
562 # Make sure the screen stays on - we're going to use UI automation
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100563 device.force_awake()
564 device.unlock()
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800565
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100566 # Disable Privacy Impact popup on Android 5.
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100567 if device.sdk <= 22:
568 disable_privacy_impact_popup(device)
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100569
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200570 # Push the scenarios, their data, and install the apps
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100571 suite.deploy(device)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100572
Borjan Tchakaloff7283b552020-02-07 16:40:47 +0100573 with device.launch("com.lunarlabs.panda"):
574 configure_perms(device)
575 configure_settings(device)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100576 except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
577 _LOG.error("Failed to execute deployment", exc_info=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200578 finally:
579 try:
580 # Leave the device alone now
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100581 device.force_awake(always=False)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100582 except DeviceCommandError:
583 _LOG.warning("Failed to tear down device", exc_info=True)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800584
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100585
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100586if __name__ == "__main__":
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100587 logging.basicConfig(
588 format="%(asctime)-15s:%(levelname)s:%(message)s", level=logging.INFO
589 )
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100590 deploy()