blob: d3dc1b0d572498640b3b39c69f784d4ef4598692 [file] [log] [blame]
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01001#!/usr/bin/env python3
2
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +01003import logging
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +01004import pathlib
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +01005import re
6import subprocess
7import sys
8import time
9
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +010010import uiautomator
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010011
12VWS_CREDENTIALS = {"user": "fairphonetesting@gmail.com", "password": "aish3echi:uwaiSh"}
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +010013
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010014
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010015PREBUILTS_PATH = "../../vendor/smartviser/viser/prebuilts/apk"
Borjan Tchakaloffd8445d32018-04-18 18:01:20 +020016
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010017PREBUILT_PROXY_APK_PATTERN = "com.lunarlabs.panda.proxy-latest-sdk{sdk}-{flavour}.apk"
Borjan Tchakaloffd8445d32018-04-18 18:01:20 +020018
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +010019
20_LOG = logging.getLogger(__name__)
21
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010022PREBUILT_APKS = ["com.smartviser.demogame-latest.apk", "com.lunarlabs.panda-latest.apk"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010023
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +020024FAIRPHONE_WIFI_NETWORKS = {
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010025 "Fairphone Guest": {"security": "WPA/WPA2 PSK", "password": "fairwifi"},
26 "Fairphone DEV (2.4 GHz)": {"security": "WPA/WPA2 PSK", "password": "fdev@adm"},
27 "Fairphone DEV (5 GHz)": {"security": "WPA/WPA2 PSK", "password": "fdev@adm"},
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +020028}
29
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010030ADB_DEVICES_PATTERN = re.compile(r"^([a-z0-9-]+)\s+device$", flags=re.M)
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +020031
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010032
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020033class HostCommandError(BaseException):
34 """An error happened while issuing a command on the host."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010035
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020036 def __init__(self, command, error_message):
37 self.command = command
38 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010039 message = "Command `{}` failed: {}".format(command, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020040 super(HostCommandError, self).__init__(message)
41
42
43class DeviceCommandError(BaseException):
44 """An error happened while sending a command to a device."""
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010045
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020046 def __init__(self, serial, command, error_message):
47 self.serial = serial
48 self.command = command
49 self.error_message = error_message
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010050 message = "Command `{}` failed on {}: {}".format(command, serial, error_message)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020051 super(DeviceCommandError, self).__init__(message)
52
53
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010054def adb(*args, serial=None, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010055 """Run ADB command attached to serial.
56
57 Example:
58 >>> process = adb('shell', 'getprop', 'ro.build.fingerprint', serial='cc60c021')
59 >>> process.returncode
60 0
61 >>> process.stdout.strip()
62 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.02.0/FP2-gms-18.02.0:user/release-keys'
63
64 :param *args:
65 List of options to ADB (including command).
66 :param str serial:
67 Identifier for ADB connection to device.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020068 :param raise_on_error bool:
69 Whether to raise a DeviceCommandError exception if the return code is
70 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010071 :returns subprocess.CompletedProcess:
72 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020073 :raises DeviceCommandError:
74 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010075 """
76
77 # Make sure the adb server is started to avoid the infamous "out of date"
78 # message that pollutes stdout.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020079 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010080 ["adb", "start-server"],
81 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE,
83 universal_newlines=True,
84 )
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020085 if ret.returncode < 0:
86 if raise_on_error:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010087 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020088 else:
89 return None
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010090
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010091 command = ["adb"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010092 if serial:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010093 command += ["-s", serial]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010094 if args:
95 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +020096 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +010097 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
98 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +010099
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200100 if raise_on_error and ret.returncode < 0:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100101 raise DeviceCommandError(serial if serial else "??", str(args), ret.stderr)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100102
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200103 return ret
104
105
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200106def list_devices():
107 """List serial numbers of devices attached to adb.
108
109 Raises:
110 DeviceCommandError: If the underlying adb command failed.
111 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100112 process = adb("devices")
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200113 return ADB_DEVICES_PATTERN.findall(process.stdout)
114
115
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100116def aapt(*args, raise_on_error=True):
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100117 """Run an AAPT command.
118
119 :param *args:
120 The AAPT command with its options.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200121 :param raise_on_error bool:
122 Whether to raise a DeviceCommandError exception if the return code is
123 less than 0.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100124 :returns subprocess.CompletedProcess:
125 Completed process.
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200126 :raises HostCommandError:
127 If the command failed.
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100128 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100129 command = ["aapt"]
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100130 if args:
131 command += list(args)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200132 ret = subprocess.run(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100133 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
134 )
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100135
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200136 if raise_on_error and ret.returncode < 0:
137 raise HostCommandError(str(args), ret.stderr)
138
139 return ret
140
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100141
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100142def configure_wifi_networks(device, networks):
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200143 """Configure Wi-Fi networks.
144
145 The `networks` parameters is a list of networks to configure hashed by
146 their SSID. Each network value should have the following format:
147 - security (str): The security value as can be found in the Wi-Fi
148 settings dialog. Common values are 'None' and 'WPA/WPA2 PSK'.
149 - password (str, optional): The network password if the security is
150 'WPA/WPA2 PSK'.
151
152 Parameters:
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100153 device: The device object.
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200154 networks (dict(dict(str))): The list of networks to configure.
155 Raises:
156 DeviceCommandError: If the UI automation fails.
157 """
158 # Open the Wi-Fi settings
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100159 device.adb(
160 "shell", "am start -a android.settings.WIFI_SETTINGS --activity-clear-task"
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100161 )
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200162
163 # Make sure Wi-Fi is enabled
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100164 wifi_enabler = device.ui(
165 text="OFF", resourceId="com.android.settings:id/switch_widget"
166 )
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200167 if wifi_enabler.exists:
168 wifi_enabler.click()
169
170 # Check for registered networks
171 registered_networks = set()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100172 device.ui(description="More options").click.wait()
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200173 time.sleep(1)
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100174 saved_networks = device.ui(text="Saved networks")
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200175 if saved_networks.exists:
Borjan Tchakaloff1d83e562018-05-16 18:47:04 +0200176 saved_networks.click.wait()
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200177 for ssid in networks.keys():
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100178 if device.ui(text=ssid).exists:
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200179 registered_networks.add(ssid)
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100180 device.ui.press.back()
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200181
182 missing_networks = networks.keys() - registered_networks
183
184 for ssid in registered_networks:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100185 print("Ignoring `{}` Wi-Fi network, already configured.".format(ssid))
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200186
187 for ssid in missing_networks:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100188 print("Configuring `{}` Wi-Fi network…".format(ssid))
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200189
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100190 device.ui(description="More options").click.wait()
191 device.ui(text="Add network").click.wait()
192 device.ui(resourceId="com.android.settings:id/ssid").set_text(ssid)
193 device.ui(resourceId="com.android.settings:id/security").click()
194 device.ui(text=networks[ssid]["security"]).click()
195 password_field = device.ui(resourceId="com.android.settings:id/password")
Karsten Tauschec20f4f32019-03-04 14:32:04 +0100196 time.sleep(1)
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100197 if "password" in networks[ssid] and networks[ssid]["password"]:
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200198 if not password_field.exists:
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100199 device.ui(text="Cancel").click()
200 raise DeviceCommandError(
201 device.ui, "UI: add Wi-Fi", "missing password field"
202 )
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100203 password_field.set_text(networks[ssid]["password"])
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200204 elif password_field.exists:
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100205 device.ui(text="Cancel").click()
206 raise DeviceCommandError(
207 device.ui, "UI: add Wi-Fi", "missing password data"
208 )
209 save_button = device.ui(text="Save")
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200210 if not save_button.click():
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100211 device.ui(text="Cancel").click()
212 raise DeviceCommandError(
213 device.ui, "UI: add Wi-Fi", "could not save network"
214 )
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200215
216 # Force the Wi-Fi on and off to pick the best network available
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100217 device.ui(text="ON", resourceId="com.android.settings:id/switch_widget").click()
218 device.ui(text="OFF", resourceId="com.android.settings:id/switch_widget").click()
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200219
220 # Leave the settings
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100221 device.ui.press.back()
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200222
223
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100224def disable_privacy_impact_popup(device):
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100225 """Disable Privacy Impact popup on Android 5.
226
227 This simplifies UI automation. Disabling the feature globally is more robust
228 than clicking through the the Privacy Impact screen per app.
229 """
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100230 print("Disabling the Privacy Impact screen…")
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100231 device.adb(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100232 "shell",
233 (
234 "am start -a android.intent.action.MAIN "
235 "com.fairphone.privacyimpact/.PrivacyImpactPreferenceActivity"
236 ),
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100237 )
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100238 disable_privacy_impact_checkbox = device.ui(className="android.widget.CheckBox")
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100239 if not disable_privacy_impact_checkbox.checked:
240 disable_privacy_impact_checkbox.click()
241
242
Borjan Tchakaloffd4ce8072018-05-16 18:20:13 +0200243def get_proxy_apk(android_sdk, flavour):
244 if android_sdk >= 24:
245 return PREBUILT_PROXY_APK_PATTERN.format(sdk=24, flavour=flavour)
246 else:
247 return PREBUILT_PROXY_APK_PATTERN.format(sdk=19, flavour=flavour)
248
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100249
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100250# Prepare the DUT
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100251def prepare_dut(device, scenarios_dir, data_dir, prebuilts_dir):
252 flavour = "gms" if device.is_gms_device() else "sibon"
253 proxy_apk = get_proxy_apk(device.sdk, flavour)
254 prebuilts_dir = pathlib.Path(prebuilts_dir)
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200255
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100256 # Uninstall the smartviser apps
Borjan Tchakaloffd4ce8072018-05-16 18:20:13 +0200257 for app in PREBUILT_APKS + [proxy_apk]:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100258 print("Uninstalling `{}`…".format(app))
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100259 device.uninstall(prebuilts_dir / app)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100260
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100261 # Copy the scenarios
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100262 print("Pushing scenarios from `{}`…".format(scenarios_dir))
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100263 device.push(scenarios_dir, pathlib.Path("/sdcard/Viser"))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100264
265 # Copy the scenarios data
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100266 print("Pushing scenarios data from `{}`…".format(data_dir))
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100267 device.push(data_dir, pathlib.Path("/sdcard/Viser/Data"))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100268
Franz-Xaver Geigerb3f7c982018-04-12 09:29:32 +0200269 # Install the smartviser apps (starting with the proxy app)
Borjan Tchakaloffd4ce8072018-05-16 18:20:13 +0200270 for app in [proxy_apk] + PREBUILT_APKS:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100271 print("Installing `{}`…".format(app))
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100272 device.install(prebuilts_dir / app)
Franz-Xaver Geigerb28348e2018-02-19 14:41:40 +0100273
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100274
275# Grant the permissions through the UI
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100276def configure_perms(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100277 # Input the credentials
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100278 device.ui(resourceId="android:id/content").child(text="Username").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100279 className="android.widget.EditText"
280 ).set_text(VWS_CREDENTIALS["user"])
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100281 device.ui(resourceId="android:id/content").child(text="Password").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100282 className="android.widget.EditText"
283 ).set_text(VWS_CREDENTIALS["password"])
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100284
285 # Sign in
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100286 signin_label = "SIGN IN" if device.sdk >= 24 else "Sign in"
287 device.ui(resourceId="android:id/content").child(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100288 text=signin_label, className="android.widget.Button"
289 ).click()
290
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100291
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100292def configure_settings(device):
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100293 # Set the e-mail account
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100294 device.ui(text="Settings", className="android.widget.TextView").click()
295 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100296 "User settings", className="android.widget.LinearLayout"
297 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100298 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100299 "Email account", className="android.widget.LinearLayout"
300 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100301 prompt = device.ui(resourceId="android:id/content").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100302 "Email account", className="android.widget.LinearLayout"
303 )
304 prompt.child(resourceId="android:id/edit").set_text("fairphone.viser@gmail.com")
305 prompt.child(text="OK", className="android.widget.Button").click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100306 device.ui(resourceId="android:id/list").child_by_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100307 "Email password", className="android.widget.LinearLayout"
308 ).click()
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100309 device.ui(text="Password :").child(className="android.widget.EditText").set_text(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100310 "fairphoneviser2017"
311 )
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100312 device.ui(description="OK", className="android.widget.TextView").click()
313 device.ui.press.back()
314 device.ui.press.back()
315
316
317class DeviceUnderTest:
318 """An Android device under test."""
319
320 serial: str
321 """The device serial number (adb/fastboot)."""
322 sdk: int
323 """The Android SDK version number."""
324 ui: uiautomator.Device
325 """The UI Automator handle piloting the device."""
326
327 def __init__(self, serial: str):
328 self.serial = serial
329 self.ui = uiautomator.Device(serial)
330
331 # Cache the Android SDK version
332 self.sdk = self.ui.info["sdkInt"]
333
334 def adb(self, *args) -> subprocess.CompletedProcess:
335 """Execute an adb command on this device.
336
337 :returns: The completed process.
338 :raise DeviceCommandError: If the underlying adb command failed.
339 """
340 return adb(*args, serial=self.serial)
341
342 def force_awake(self, always=True) -> None:
343 """Force the device to stay awake.
344
345 :raise DeviceCommandError: If the underlying adb command failed.
346 """
347 self.adb("shell", "svc power stayon {}".format("true" if always else "false"))
348
349 def unlock(self) -> None:
350 """Wake-up the device and unlock it.
351
352 :raise DeviceCommandError: If the underlying adb commands failed.
353 """
354 if not self.ui.info["screenOn"]:
355 self.adb("shell", "input keyevent KEYCODE_POWER")
356 time.sleep(1)
357 # The KEYCODE_MENU input is enough to unlock a "swipe up to unlock"
358 # lockscreen on Android 6, but unfortunately not Android 7. So we use a
359 # swipe up (that depends on the screen resolution) instead.
360 self.adb("shell", "input touchscreen swipe 930 880 930 380")
361 time.sleep(1)
362 self.adb("shell", "input keyevent KEYCODE_HOME")
363
364 def getprop(self, key: str) -> str:
365 """Get a system property.
366
367 Example:
368 >>> self.getprop('ro.build.id')
369 'FP2-gms-18.02.0'
370
371 :param key: Key of property to get.
372 :returns: Value of system property.
373 :raise DeviceCommandError: If the underlying adb command failed.
374 """
375 process = self.adb("shell", "getprop", key)
376 return process.stdout.strip()
377
378 def is_gms_device(self) -> bool:
379 """Whether the device runs GMS or sibon.
380
381 Example:
382 >>> self.is_gms_device()
383 True
384
385 :returns: True if device runs GMS, false otherwise.
386 :raise DeviceCommandError: If the underlying adb command failed.
387 """
388 return self.getprop("ro.build.id").startswith("FP2-gms-") or self.getprop(
389 "ro.build.version.incremental"
390 ).startswith("gms-")
391
392 def uninstall(self, apk: pathlib.Path) -> None:
393 """Uninstall an app from an APK file.
394
395 :raise ValueError: If the package name could not be read from the apk.
396 :raise DeviceCommandError: If the uninstall command failed.
397 """
398 ret = aapt("dump", "badging", str(apk))
399 package = None
400 for line in ret.stdout.splitlines():
401 if line.startswith("package"):
402 for token in line.split(" "):
403 if token.startswith("name="):
404 # Extract the package name out of the token
405 # (name='some.package.name')
406 package = token[6:-1]
407 break
408 if not package:
409 raise ValueError("Could not find package of app `{}`".format(apk.name))
410
411 self.adb("uninstall", package)
412
413 def install(self, apk: pathlib.Path) -> None:
414 """Install an app from an APK file.
415
416 :raise DeviceCommandError: If the install command failed.
417 """
418 command = ["install", "-r"]
419 if self.sdk >= 23:
420 # From Marshmallow onwards, adb has a flag to grant default permissions
421 command.append("-g")
422
423 self.adb(*command, str(apk))
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100424
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100425 def push(self, source: pathlib.Path, target: pathlib.Path) -> None:
426 """Push a file or directory to the device.
427
428 The target directory (if the source is a directory), or its
429 parent (if the source if a file) will be created on the device,
430 always.
431
432 :raise DeviceCommandError: If the push command failed.
433 """
434 if source.is_dir():
435 # `adb push` skips empty directories so we have to manually create the
436 # target directory itself.
437 self.adb("shell", "mkdir", "-p", str(target))
438 # `adb push` expects the target to be the host directory not the target
439 # directory itself. So we'll just copy the source directory content instead.
440 sources = [str(child) for child in source.iterdir()]
441 self.adb("push", *sources, str(target))
442 else:
443 self.adb("shell", "mkdir", "-p", str(target.parent))
444 self.adb("push", str(source), str(target))
445
446 def remove(self, target: pathlib.Path, *, recurse: bool = False) -> None:
447 """Remove a file or directory from the device.
448
449 :raise DeviceCommandError: If the remove command failed.
450 """
451 command = ["shell", "rm", "-f"]
452 if recurse:
453 command.append("-r")
454 command.append(str(target))
455 self.adb(*command)
456
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100457
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100458def deploy():
459 serials = []
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100460
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100461 if len(sys.argv) > 1:
462 serials.append(sys.argv[1])
463 else:
Franz-Xaver Geigerb0f55422018-04-17 10:33:37 +0200464 serials = list_devices()
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100465
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100466 for serial in serials:
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100467 _LOG.info("Deploy to device %s", serial)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100468
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100469 device = DeviceUnderTest(serial)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200470 try:
471 # Make sure the screen stays on - we're going to use UI automation
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100472 device.force_awake()
473 device.unlock()
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800474
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200475 # Configure common Fairphone Wi-Fi networks
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100476 if device.sdk < 24:
477 configure_wifi_networks(device, FAIRPHONE_WIFI_NETWORKS)
Borjan Tchakaloffd4ce8072018-05-16 18:20:13 +0200478 else:
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100479 print(
480 "Uh oh, the device is running Android SDK {} on which we "
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100481 "do not deploy Wi-Fi networks yet.".format(device.sdk)
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100482 )
Borjan Tchakaloff74d962a2018-04-11 17:47:14 +0200483
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100484 # Disable Privacy Impact popup on Android 5.
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100485 if device.sdk <= 22:
486 disable_privacy_impact_popup(device)
Karsten Tauscheb36529f2019-03-02 14:13:29 +0100487
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200488 # Push the scenarios, their data, and install the apps
Borjan Tchakaloffaafa2262020-02-07 11:53:07 +0100489 prepare_dut(
490 device,
491 pathlib.Path("../scenarios"),
492 pathlib.Path("../scenarios-data"),
493 PREBUILTS_PATH,
494 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100495
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200496 # Start the viser app
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100497 device.adb(
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100498 "shell",
499 "monkey",
500 "-p",
501 "com.lunarlabs.panda",
502 "-c",
503 "android.intent.category.LAUNCHER",
504 "1",
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100505 )
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100506
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100507 configure_perms(device)
Borjan Tchakaloffa4bdae12017-11-21 14:58:12 +0100508
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100509 configure_settings(device)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100510 except (HostCommandError, DeviceCommandError, uiautomator.JsonRPCError):
511 _LOG.error("Failed to execute deployment", exc_info=True)
Borjan Tchakaloffde9fa0f2018-04-12 12:05:48 +0200512 finally:
513 try:
514 # Leave the device alone now
Borjan Tchakaloff1e841d82020-01-24 16:25:50 +0100515 device.force_awake(always=False)
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100516 except DeviceCommandError:
517 _LOG.warning("Failed to tear down device", exc_info=True)
Borjan Tchakaloff64ec42b2018-02-07 11:33:15 -0800518
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100519
Borjan Tchakaloff7d4f6152020-01-24 15:41:00 +0100520if __name__ == "__main__":
Borjan Tchakaloff31c37e02020-02-07 14:03:27 +0100521 logging.basicConfig(
522 format="%(asctime)-15s:%(levelname)s:%(message)s", level=logging.INFO
523 )
Franz-Xaver Geigerd1079e22018-02-19 14:34:15 +0100524 deploy()