blob: a24ca86b75b02f8d19068c8a109c2d3547a68b98 [file] [log] [blame]
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01001#!/usr/bin/env python3
2
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +01003import dataclasses
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +01004import logging
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01005import pathlib
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +01006import re
7import subprocess
8import sys
9import time
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +010010from typing import Sequence
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010011
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +010012import uiautomator
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010013
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +010014_LOG = logging.getLogger(__name__)
15
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +010016VWS_CREDENTIALS = {"user": "fairphonetesting@gmail.com", "password": "aish3echi:uwaiSh"}
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010017
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010018ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M)
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020019
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010020
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020021class HostCommandError(BaseException):
22 """An error happened while issuing a command on the host."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010023
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020024 def __init__(self, command, error_message):
25 self.command = command
26 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010027 message = "Command `{}` failed: {}".format(command, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020028 super(HostCommandError, self).__init__(message)
29
30
31class DeviceCommandError(BaseException):
32 """An error happened while sending a command to a device."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010033
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020034 def __init__(self, serial, command, error_message):
35 self.serial = serial
36 self.command = command
37 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010038 message = "Command `{}` failed on {}: {}".format(command, serial, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020039 super(DeviceCommandError, self).__init__(message)
40
41
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010042def adb(*args, serial=None, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010043 """Run ADB command attached to serial.
44
45 Example:
46 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
47 >>> process.returncode
48 0
49 >>> process.stdout.strip()
50 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
51
52 :param *args:
53 List of options to ADB (including command).
54 :param str serial:
55 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020056 :param raise_on_error bool:
57 Whether to raise a DeviceCommandError exception if the return code is
58 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010059 :returns subprocess.CompletedProcess:
60 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020061 :raises DeviceCommandError:
62 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010063 """
64
65 # Make sure the adb server is started to avoid the infamous "out of date"
66 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020067 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010068 ["adb", "start-server"],
69 stdout=subprocess.PIPE,
70 stderr=subprocess.PIPE,
71 universal_newlines=True,
72 )
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020073 if ret.returncode < 0:
74 if raise_on_error:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010075 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020076 else:
77 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010078
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010079 command = ["adb"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010080 if serial:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010081 command += ["-s", serial]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010082 if args:
83 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020084 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010085 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
86 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010087
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020088 if raise_on_error and ret.returncode < 0:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010089 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010090
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020091 return ret
92
93
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020094def list_devices():
95 """List serial numbers of devices attached to adb.
96
97 Raises:
98 DeviceCommandError: If the underlying adb command failed.
99 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100100 process = adb("devices")
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200101 return ADB_DEVICES_PATTERN.findall(process.stdout)
102
103
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100104def aapt(*args, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100105 """Run an AAPT command.
106
107 :param *args:
108 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200109 :param raise_on_error bool:
110 Whether to raise a DeviceCommandError exception if the return code is
111 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100112 :returns subprocess.CompletedProcess:
113 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200114 :raises HostCommandError:
115 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100116 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100117 command = ["aapt"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100118 if args:
119 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200120 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100121 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
122 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100123
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200124 if raise_on_error and ret.returncode < 0:
125 raise HostCommandError(str(args), ret.stderr)
126
127 return ret
128
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100129
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100130def disable_privacy_impact_popup(device):
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100131 """Disable Privacy Impact popup on Android 5.
132
133 This simplifies UI automation. Disabling the feature globally is more robust
134 than clicking through the the Privacy Impact screen per app.
135 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100136 print("Disabling the Privacy Impact screen…")
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100137 device.adb(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100138 "shell",
139 (
140 "am start -a android.intent.action.MAIN "
141 "com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"
142 ),
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100143 )
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100144 disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100145 if not disable_privacy_impact_checkbox.checked:
146 disable_privacy_impact_checkbox.click()
147
148
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100149# Grant the permissions through the UI
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100150def configure_perms(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100151 # Input the credentials
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100152 device.ui(resourceId="android:id/content").child(text="Username").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100153 className="android.widget.EditText"
154 ).set_text(VWS_CREDENTIALS["user"])
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100155 device.ui(resourceId="android:id/content").child(text="Password").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100156 className="android.widget.EditText"
157 ).set_text(VWS_CREDENTIALS["password"])
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100158
159 # Sign in
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100160 signin_label = "SIGN IN" if device.sdk >= 24 else "Sign in"
161 device.ui(resourceId="android:id/content").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100162 text=signin_label, className="android.widget.Button"
163 ).click()
164
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100165
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100166def configure_settings(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100167 # Set the e-mail account
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100168 device.ui(text="Settings", className="android.widget.TextView").click()
169 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100170 "User settings", className="android.widget.LinearLayout"
171 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100172 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100173 "Email account", className="android.widget.LinearLayout"
174 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100175 prompt = device.ui(resourceId="android:id/content").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100176 "Email account", className="android.widget.LinearLayout"
177 )
178 prompt.child(resourceId="android:id/edit").set_text("fairphone.viser@gmail.com")
179 prompt.child(text="OK", className="android.widget.Button").click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100180 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100181 "Email password", className="android.widget.LinearLayout"
182 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100183 device.ui(text="Password :").child(className="android.widget.EditText").set_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100184 "fairphoneviser2017"
185 )
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100186 device.ui(description="OK", className="android.widget.TextView").click()
187 device.ui.press.back()
188 device.ui.press.back()
189
190
191class DeviceUnderTest:
192 """An Android device under test."""
193
194 serial: str
195 """The device serial number (adb/fastboot)."""
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100196 os_flavour: str
197 """The Fairphone-specific OS flavour."""
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100198 sdk: int
199 """The Android SDK version number."""
200 ui: uiautomator.Device
201 """The UI Automator handle piloting the device."""
202
203 def __init__(self, serial: str):
204 self.serial = serial
205 self.ui = uiautomator.Device(serial)
206
207 # Cache the Android SDK version
208 self.sdk = self.ui.info["sdkInt"]
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100209 # Cache the OS flavour
210 self.os_flavour = "gms" if self.is_gms_device() else "sibon"
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100211
212 def adb(self, *args) -> subprocess.CompletedProcess:
213 """Execute an adb command on this device.
214
215 :returns: The completed process.
216 :raise DeviceCommandError: If the underlying adb command failed.
217 """
218 return adb(*args, serial=self.serial)
219
220 def force_awake(self, always=True) -> None:
221 """Force the device to stay awake.
222
223 :raise DeviceCommandError: If the underlying adb command failed.
224 """
225 self.adb("shell", "svc power stayon {}".format("true" if always else "false"))
226
227 def unlock(self) -> None:
228 """Wake-up the device and unlock it.
229
230 :raise DeviceCommandError: If the underlying adb commands failed.
231 """
232 if not self.ui.info["screenOn"]:
233 self.adb("shell", "input keyevent KEYCODE_POWER")
234 time.sleep(1)
235 # The KEYCODE_MENU input is enough to unlock a "swipe up to unlock"
236 # lockscreen on Android 6, but unfortunately not Android 7. So we use a
237 # swipe up (that depends on the screen resolution) instead.
238 self.adb("shell", "input touchscreen swipe 930 880 930 380")
239 time.sleep(1)
240 self.adb("shell", "input keyevent KEYCODE_HOME")
241
242 def getprop(self, key: str) -> str:
243 """Get a system property.
244
245 Example:
246 >>> self.getprop('ro.build.id')
247 'FP2-gms-18.02.0'
248
249 :param key: Key of property to get.
250 :returns: Value of system property.
251 :raise DeviceCommandError: If the underlying adb command failed.
252 """
253 process = self.adb("shell", "getprop", key)
254 return process.stdout.strip()
255
256 def is_gms_device(self) -> bool:
257 """Whether the device runs GMS or sibon.
258
259 Example:
260 >>> self.is_gms_device()
261 True
262
263 :returns: True if device runs GMS, false otherwise.
264 :raise DeviceCommandError: If the underlying adb command failed.
265 """
266 return self.getprop("ro.build.id").startswith("FP2-gms-") or self.getprop(
267 "ro.build.version.incremental"
268 ).startswith("gms-")
269
270 def uninstall(self, apk: pathlib.Path) -> None:
271 """Uninstall an app from an APK file.
272
273 :raise ValueError: If the package name could not be read from the apk.
274 :raise DeviceCommandError: If the uninstall command failed.
275 """
276 ret = aapt("dump", "badging", str(apk))
277 package = None
278 for line in ret.stdout.splitlines():
279 if line.startswith("package"):
280 for token in line.split(" "):
281 if token.startswith("name="):
282 # Extract the package name out of the token
283 # (name='some.package.name')
284 package = token[6:-1]
285 break
286 if not package:
287 raise ValueError("Could not find package of app `{}`".format(apk.name))
288
289 self.adb("uninstall", package)
290
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100291 def uninstall_package(self, package_name: str) -> None:
292 """Uninstall an app from its package name.
293
294 :raise DeviceCommandError: If the uninstall command failed.
295 """
296 self.adb("uninstall", package_name)
297
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100298 def install(self, apk: pathlib.Path) -> None:
299 """Install an app from an APK file.
300
301 :raise DeviceCommandError: If the install command failed.
302 """
303 command = ["install", "-r"]
304 if self.sdk >= 23:
305 # From Marshmallow onwards, adb has a flag to grant default permissions
306 command.append("-g")
307
308 self.adb(*command, str(apk))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100309
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100310 def push(self, source: pathlib.Path, target: pathlib.Path) -> None:
311 """Push a file or directory to the device.
312
313 The target directory (if the source is a directory), or its
314 parent (if the source if a file) will be created on the device,
315 always.
316
317 :raise DeviceCommandError: If the push command failed.
318 """
319 if source.is_dir():
320 # `adb push` skips empty directories so we have to manually create the
321 # target directory itself.
322 self.adb("shell", "mkdir", "-p", str(target))
323 # `adb push` expects the target to be the host directory not the target
324 # directory itself. So we'll just copy the source directory content instead.
325 sources = [str(child) for child in source.iterdir()]
326 self.adb("push", *sources, str(target))
327 else:
328 self.adb("shell", "mkdir", "-p", str(target.parent))
329 self.adb("push", str(source), str(target))
330
331 def remove(self, target: pathlib.Path, *, recurse: bool = False) -> None:
332 """Remove a file or directory from the device.
333
334 :raise DeviceCommandError: If the remove command failed.
335 """
336 command = ["shell", "rm", "-f"]
337 if recurse:
338 command.append("-r")
339 command.append(str(target))
340 self.adb(*command)
341
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100342
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100343@dataclasses.dataclass(frozen=True)
344class AndroidApp:
345 """An installable Android app."""
346
347 package: str
348 """The app package name."""
349 apk: str
350 """The app package (APK) filename."""
351
352 def __str__(self) -> str:
353 return self.package
354
355
356@dataclasses.dataclass(frozen=True)
357class ViserProxyApp:
358 """A template for the viSer Proxy app."""
359
360 package: str
361 """The app package name."""
362 apk_filename_template: str
363 """The string template of the APK filename, in the `str.format()` style (`{}`).
364
365 The following place-holders can be used and will be replaced at runtime:
366 - `{sdk}`: The Android SDK number, e.g. `25` for Android 7.1.
367 - `{flavour}`: The Fairphone-specific Android build flavour, e.g. `gms`
368 (Fairphone OS) or `sibon` (Fairphone Open).
369 """
370
371 def resolve(self, *, sdk: int, flavour: str) -> AndroidApp:
372 """Resolve the app template into a Viser App."""
373 if sdk >= 24:
374 sdk = 24
375 else:
376 sdk = 19
377 return AndroidApp(
378 self.package, self.apk_filename_template.format(sdk=sdk, flavour=flavour)
379 )
380
381
382@dataclasses.dataclass(frozen=True)
383class ViserSuite:
384 """A SmartViser viSer app suite.
385
386 Example:
387
388 >>> device = DeviceUnderTest(...)
389 >>> suite = ViserSuite(...)
390 >>> suite.deploy(device)
391
392 """
393
394 VISER_PROXY_APP_TEMPLATE = ViserProxyApp(
395 "com.lunarlabs.panda.proxy",
396 "com.lunarlabs.panda.proxy-latest-sdk{sdk}-{flavour}.apk",
397 )
398 ANDROID_APPS = [
399 AndroidApp("com.smartviser.demogame", "com.smartviser.demogame-latest.apk"),
400 AndroidApp("com.lunarlabs.panda", "com.lunarlabs.panda-latest.apk"),
401 ]
402
403 prebuilts_path: pathlib.Path
404 scenarios_path: pathlib.Path
405 scenarios_data_path: pathlib.Path
406 target_path: pathlib.Path = pathlib.Path("/sdcard/Viser")
407
408 @property
409 def target_scenarios_path(self) -> pathlib.Path:
410 return self.target_path
411
412 @property
413 def target_scenarios_data_path(self) -> pathlib.Path:
414 return self.target_path / "Data"
415
416 def resolve_apps(self, device: DeviceUnderTest) -> Sequence[AndroidApp]:
417 """Resolve the apps based on the target device properties.
418
419 :param device: The device to target.
420 :returns: The sequence of apps suitable for the target device.
421 The sequence is ordered to satisfy the dependency graph
422 (i.e. required apps come first in the sequence).
423 """
424 return [
425 self.VISER_PROXY_APP_TEMPLATE.resolve(
426 sdk=device.sdk, flavour=device.os_flavour
427 )
428 ] + self.ANDROID_APPS
429
430 def deploy(self, device: DeviceUnderTest) -> None:
431 """Deploy the suite on a device.
432
433 Copy the test scenarios and their data, and install the
434 different apps composing the suite.
435
436 The previous configuration and data (i.e. reports) tied to the
437 app suite, if any, is deleted before hand.
438
439 :param device: The device to deploy the suite to.
440 """
441 self.cleanup_previous_deployment(device)
442 self.copy_scenarios(device)
443 self.install_suite(device)
444
445 def cleanup_previous_deployment(self, device: DeviceUnderTest) -> None:
446 """Clean-up a previous deployment of the suite on a device.
447
448 :param device: The device to clean-up.
449 """
450 # Uninstall the apps in the reverse order to cater for dependencies.
451 for app in reversed(self.resolve_apps(device)):
452 _LOG.info("Uninstall %s", app)
453 device.uninstall_package(app.package)
454
455 _LOG.info("Delete data from previous deployment")
456 device.remove(self.target_path, recurse=True)
457
458 def copy_scenarios(self, device: DeviceUnderTest) -> None:
459 """Copy the suite scenarios and their data on a device.
460
461 :param device: The device to copy the scenarios to.
462 """
463 _LOG.info(
464 "Copy scenarios: %s → %s", self.scenarios_path, self.target_scenarios_path
465 )
466 device.push(self.scenarios_path, self.target_scenarios_path)
467
468 _LOG.info(
469 "Copy scenarios data: %s → %s",
470 self.scenarios_data_path,
471 self.target_scenarios_data_path,
472 )
473 device.push(self.scenarios_path, self.target_scenarios_data_path)
474
475 def install_suite(self, device: DeviceUnderTest) -> None:
476 """Install the suite apps on a device.
477
478 :param device: The device to install the suite apps on.
479 """
480 for app in self.resolve_apps(device):
481 _LOG.info("Install %s", app)
482 device.install(self.prebuilts_path / app.apk)
483
484
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100485def deploy():
486 serials = []
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100487 suite = ViserSuite(
488 pathlib.Path("../../vendor/smartviser/viser/prebuilts/apk"),
489 pathlib.Path("../scenarios"),
490 pathlib.Path("../scenarios-data"),
491 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100492
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100493 if len(sys.argv) > 1:
494 serials.append(sys.argv[1])
495 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200496 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100497
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100498 for serial in serials:
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100499 _LOG.info("Deploy to device %s", serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100500
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100501 device = DeviceUnderTest(serial)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200502 try:
503 # Make sure the screen stays on - we're going to use UI automation
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100504 device.force_awake()
505 device.unlock()
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800506
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100507 # Disable Privacy Impact popup on Android 5.
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100508 if device.sdk <= 22:
509 disable_privacy_impact_popup(device)
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100510
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200511 # Push the scenarios, their data, and install the apps
Borjan Tchakaloff9c59fb82020-02-07 11:49:41 +0100512 suite.deploy(device)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100513
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200514 # Start the viser app
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100515 device.adb(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100516 "shell",
517 "monkey",
518 "-p",
519 "com.lunarlabs.panda",
520 "-c",
521 "android.intent.category.LAUNCHER",
522 "1",
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100523 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100524
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100525 configure_perms(device)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100526
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100527 configure_settings(device)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100528 except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
529 _LOG.error("Failed to execute deployment", exc_info=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200530 finally:
531 try:
532 # Leave the device alone now
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100533 device.force_awake(always=False)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100534 except DeviceCommandError:
535 _LOG.warning("Failed to tear down device", exc_info=True)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800536
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100537
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100538if __name__ == "__main__":
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100539 logging.basicConfig(
540 format="%(asctime)-15s:%(levelname)s:%(message)s", level=logging.INFO
541 )
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100542 deploy()