blob: 2369aa7d2abe07e4d758cbb8dbc003ad27df8c9d [file] [log] [blame]
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +01001#!/usr/bin/env python3
2# Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
Christoffer Quist Adamsen21f7f352022-05-30 19:52:43 +02006import argparse
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +02007import os
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +01008import subprocess
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +02009import sys
10import threading
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010011import time
12
Christoffer Quist Adamsen21f7f352022-05-30 19:52:43 +020013from enum import Enum
14
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020015sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
17import utils
18
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010019DEVNULL=subprocess.DEVNULL
20
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020021class ProcessReader(threading.Thread):
22
23 def __init__(self, process):
24 threading.Thread.__init__(self)
25 self.lines = []
26 self.process = process
27
28 def run(self):
29 for line in self.process.stdout:
30 line = line.decode('utf-8').strip()
31 self.lines.append(line)
32
33 def stop(self):
34 self.process.kill()
35
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010036class ScreenState(Enum):
37 OFF_LOCKED = 1,
38 OFF_UNLOCKED = 2
39 ON_LOCKED = 3
40 ON_UNLOCKED = 4
41
42 def is_off(self):
43 return self == ScreenState.OFF_LOCKED or self == ScreenState.OFF_UNLOCKED
44
45 def is_on(self):
46 return self == ScreenState.ON_LOCKED or self == ScreenState.ON_UNLOCKED
47
48 def is_on_and_locked(self):
49 return self == ScreenState.ON_LOCKED
50
51 def is_on_and_unlocked(self):
52 return self == ScreenState.ON_UNLOCKED
53
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +020054def broadcast(action, component, device_id=None):
55 print('Sending broadcast %s' % action)
56 cmd = create_adb_cmd('shell am broadcast -a %s %s' % (action, component), device_id)
57 return subprocess.check_output(cmd).decode('utf-8').strip().splitlines()
58
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010059def create_adb_cmd(arguments, device_id=None):
60 assert isinstance(arguments, list) or isinstance(arguments, str)
61 cmd = ['adb']
62 if device_id is not None:
63 cmd.append('-s')
64 cmd.append(device_id)
65 cmd.extend(arguments if isinstance(arguments, list) else arguments.split(' '))
66 return cmd
67
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010068def capture_app_profile_data(app_id, device_id=None):
69 cmd = create_adb_cmd(
70 'shell killall -s SIGUSR1 %s' % app_id, device_id)
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020071 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010072 time.sleep(5)
73
74def check_app_has_profile_data(app_id, device_id=None):
75 profile_path = get_profile_path(app_id)
76 cmd = create_adb_cmd(
77 'shell du /data/misc/profiles/cur/0/%s/primary.prof' % app_id,
78 device_id)
79 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
80 size_str = stdout[:stdout.index('\t')]
81 assert size_str.isdigit()
82 size = int(size_str)
83 if size == 4:
84 raise ValueError('Expected size of profile at %s to be > 4K' % profile_path)
85
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020086def clear_logcat(device_id=None):
87 cmd = create_adb_cmd('logcat -c', device_id)
88 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
89
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010090def clear_profile_data(app_id, device_id=None):
91 cmd = create_adb_cmd(
92 'shell cmd package compile --reset %s' % app_id, device_id)
93 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
94
95def drop_caches(device_id=None):
96 cmd = create_adb_cmd(
97 ['shell', 'echo 3 > /proc/sys/vm/drop_caches'], device_id)
98 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
99
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200100def ensure_screen_on(device_id=None):
101 if get_screen_state(device_id).is_off():
102 toggle_screen(device_id)
Christoffer Quist Adamsen7a0014f2022-05-19 08:43:00 +0200103 assert get_screen_state(device_id).is_on()
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200104
105def ensure_screen_off(device_id=None):
106 if get_screen_state(device_id).is_on():
107 toggle_screen(device_id)
Christoffer Quist Adamsen7a0014f2022-05-19 08:43:00 +0200108 assert get_screen_state(device_id).is_off()
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200109
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100110def force_compilation(app_id, device_id=None):
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200111 print('Applying AOT (full)')
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100112 cmd = create_adb_cmd(
113 'shell cmd package compile -m speed -f %s' % app_id, device_id)
114 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
115
116def force_profile_compilation(app_id, device_id=None):
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200117 print('Applying AOT (profile)')
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100118 cmd = create_adb_cmd(
119 'shell cmd package compile -m speed-profile -f %s' % app_id, device_id)
120 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
121
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100122def get_apk_path(app_id, device_id=None):
123 cmd = create_adb_cmd('shell pm path %s' % app_id, device_id)
124 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
125 if not stdout.startswith('package:'):
126 raise ValueError(
127 'Expected stdout to start with "package:", was: %s' % stdout)
128 apk_path = stdout[len('package:'):]
129 if not apk_path.endswith('.apk'):
130 raise ValueError(
131 'Expected stdout to end with ".apk", was: %s' % stdout)
132 return apk_path
133
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200134def get_profile_data(app_id, device_id=None):
135 with utils.TempDir() as temp:
136 source = get_profile_path(app_id)
137 target = os.path.join(temp, 'primary.prof')
138 cmd = create_adb_cmd('pull %s %s' % (source, target), device_id)
139 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
140 with open(target, 'rb') as f:
141 return f.read()
142
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100143def get_profile_path(app_id):
144 return '/data/misc/profiles/cur/0/%s/primary.prof' % app_id
145
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100146def get_minor_major_page_faults(app_id, device_id=None):
147 pid = get_pid(app_id, device_id)
Christoffer Quist Adamsen2d5d9632022-04-06 14:48:30 +0200148 cmd = create_adb_cmd('shell ps -p %i -o MINFL,MAJFL' % pid, device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100149 stdout = subprocess.check_output(cmd).decode('utf-8')
150 lines_it = iter(stdout.splitlines())
151 first_line = next(lines_it)
152 assert first_line == ' MINFL MAJFL'
153 second_line = next(lines_it)
154 minfl, majfl = second_line.split()
155 assert minfl.isdigit()
156 assert majfl.isdigit()
157 return (int(minfl), int(majfl))
158
159def get_pid(app_id, device_id=None):
160 cmd = create_adb_cmd('shell pidof %s' % app_id, device_id)
161 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
162 assert stdout.isdigit()
163 pid = int(stdout)
164 return pid
165
166def get_screen_state(device_id=None):
167 cmd = create_adb_cmd('shell dumpsys nfc', device_id)
168 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
169 screen_state_value = None
170 for line in stdout.splitlines():
171 if line.startswith('mScreenState='):
172 value_start_index = len('mScreenState=')
173 screen_state_value=line[value_start_index:]
174 if screen_state_value is None:
175 raise ValueError('Expected to find mScreenState in: adb shell dumpsys nfc')
176 if not hasattr(ScreenState, screen_state_value):
177 raise ValueError(
178 'Expected mScreenState to be a value of ScreenState, was: %s'
179 % screen_state_value)
180 return ScreenState[screen_state_value]
181
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100182def get_classes_and_methods_from_app_profile(app_id, device_id=None):
183 apk_path = get_apk_path(app_id, device_id)
184 profile_path = get_profile_path(app_id)
185
186 # Generates a list of class and method descriptors, prefixed with one or more
187 # flags 'H' (hot), 'S' (startup), 'P' (post startup).
188 #
189 # Example:
190 #
191 # HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
192 # HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
193 # HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
194 # PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
195 # HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
196 # Landroidx/compose/runtime/ComposerImpl;
197 #
198 # See also https://developer.android.com/studio/profile/baselineprofiles.
199 cmd = create_adb_cmd(
200 'shell profman --dump-classes-and-methods'
201 ' --profile-file=%s --apk=%s --dex-location=%s'
Christoffer Quist Adamsen2d5d9632022-04-06 14:48:30 +0200202 % (profile_path, apk_path, apk_path), device_id)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100203 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
204 lines = stdout.splitlines()
205 classes_and_methods = []
206 flags_to_name = { 'H': 'hot', 'S': 'startup', 'P': 'post_startup' }
207 for line in lines:
208 flags = { 'hot': False, 'startup': False, 'post_startup': False }
209 while line[0] in flags_to_name:
210 flag_abbreviation = line[0]
211 flag_name = flags_to_name.get(flag_abbreviation)
212 flags[flag_name] = True
213 line = line[1:]
214 assert line.startswith('L')
215 classes_and_methods.append({ 'descriptor': line, 'flags': flags })
216 return classes_and_methods
217
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100218def get_screen_off_timeout(device_id=None):
219 cmd = create_adb_cmd(
220 'shell settings get system screen_off_timeout', device_id)
221 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
222 assert stdout.isdigit()
223 screen_off_timeout = int(stdout)
224 return screen_off_timeout
225
226def install(apk, device_id=None):
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200227 print('Installing %s' % apk)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100228 cmd = create_adb_cmd('install %s' % apk, device_id)
229 stdout = subprocess.check_output(cmd).decode('utf-8')
230 assert 'Success' in stdout
231
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200232def install_profile(app_id, device_id=None):
233 # This assumes that the profileinstaller library has been added to the app,
234 # https://developer.android.com/jetpack/androidx/releases/profileinstaller.
235 action = 'androidx.profileinstaller.action.INSTALL_PROFILE'
236 component = '%s/androidx.profileinstaller.ProfileInstallReceiver' % app_id
237 stdout = broadcast(action, component, device_id)
238 assert len(stdout) == 2
239 assert stdout[0] == ('Broadcasting: Intent { act=%s flg=0x400000 cmp=%s }' % (action, component))
240 assert stdout[1] == 'Broadcast completed: result=1', stdout[1]
241 stop_app(app_id, device_id)
242 force_profile_compilation(app_id, device_id)
243
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100244def issue_key_event(key_event, device_id=None, sleep_in_seconds=1):
245 cmd = create_adb_cmd('shell input keyevent %s' % key_event, device_id)
246 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
247 assert len(stdout) == 0
248 time.sleep(sleep_in_seconds)
249
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100250def launch_activity(
251 app_id, activity, device_id=None, wait_for_activity_to_launch=False):
252 args = ['shell', 'am', 'start', '-n', '%s/%s' % (app_id, activity)]
253 if wait_for_activity_to_launch:
254 args.append('-W')
255 cmd = create_adb_cmd(args, device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100256 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
Christoffer Quist Adamsen13e43132022-08-05 11:31:56 +0200257 if activity.startswith(app_id):
258 expected_stdout = (
259 'Starting: Intent { cmp=%s/.%s }' % (app_id, activity[len(app_id)+1:]))
260 else:
261 expected_stdout = 'Starting: Intent { cmp=%s/%s }' % (app_id, activity)
262 assert stdout.startswith(expected_stdout), 'was %s, expected %s' % (stdout, expected_stdout)
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100263 lines = stdout.splitlines()
264 result = {}
265 for line in lines:
266 if line.startswith('TotalTime: '):
267 total_time_str = line.removeprefix('TotalTime: ')
268 assert total_time_str.isdigit()
269 result['total_time'] = int(total_time_str)
270 assert not wait_for_activity_to_launch or 'total_time' in result
271 return result
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100272
Christoffer Quist Adamsen1786a592022-04-21 13:13:04 +0200273def navigate_to_home_screen(device_id=None):
274 cmd = create_adb_cmd('shell input keyevent KEYCODE_HOME', device_id)
275 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
276
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100277def prepare_for_interaction_with_device(device_id=None, device_pin=None):
278 # Increase screen off timeout to avoid device screen turns off.
279 twenty_four_hours_in_millis = 24 * 60 * 60 * 1000
280 previous_screen_off_timeout = get_screen_off_timeout(device_id)
281 set_screen_off_timeout(twenty_four_hours_in_millis, device_id)
282
283 # Unlock device.
284 unlock(device_id, device_pin)
285
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200286 teardown_options = {
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100287 'previous_screen_off_timeout': previous_screen_off_timeout
288 }
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200289 return teardown_options
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100290
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100291def root(device_id=None):
292 cmd = create_adb_cmd('root', device_id)
293 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
294
295def set_screen_off_timeout(screen_off_timeout_in_millis, device_id=None):
296 cmd = create_adb_cmd(
297 'shell settings put system screen_off_timeout %i'
298 % screen_off_timeout_in_millis,
299 device_id)
300 stdout = subprocess.check_output(cmd).decode('utf-8').strip()
301 assert len(stdout) == 0
302
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200303def start_logcat(device_id=None, format=None, filter=None):
304 args = ['logcat']
305 if format:
306 args.extend(['--format', format])
307 if filter:
308 args.append(filter)
309 cmd = create_adb_cmd(args, device_id)
310 logcat_process = subprocess.Popen(
311 cmd, bufsize=1024*1024, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
312 reader = ProcessReader(logcat_process)
313 reader.start()
314 return reader
315
316def stop_logcat(logcat_reader):
317 logcat_reader.stop()
318 logcat_reader.join()
319 return logcat_reader.lines
320
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100321def stop_app(app_id, device_id=None):
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200322 print('Shutting down %s' % app_id)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100323 cmd = create_adb_cmd('shell am force-stop %s' % app_id, device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100324 subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
325
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200326def teardown_after_interaction_with_device(teardown_options, device_id=None):
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100327 # Reset screen off timeout.
328 set_screen_off_timeout(
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200329 teardown_options['previous_screen_off_timeout'],
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100330 device_id)
331
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200332def toggle_screen(device_id=None):
333 issue_key_event('KEYCODE_POWER', device_id)
334
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100335def uninstall(app_id, device_id=None):
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200336 print('Uninstalling %s' % app_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100337 cmd = create_adb_cmd('uninstall %s' % app_id, device_id)
338 process_result = subprocess.run(cmd, capture_output=True)
339 stdout = process_result.stdout.decode('utf-8')
340 stderr = process_result.stderr.decode('utf-8')
341 if process_result.returncode == 0:
342 assert 'Success' in stdout
Christoffer Quist Adamsen2a87a6e2022-08-05 11:32:11 +0200343 elif stdout.startswith('cmd: Failure calling service package: Broken pipe'):
344 assert app_id == 'com.google.android.youtube'
345 print('Waiting after broken pipe')
346 time.sleep(15)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100347 else:
348 expected_error = (
349 'java.lang.IllegalArgumentException: Unknown package: %s' % app_id)
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +0200350 assert 'Failure [DELETE_FAILED_INTERNAL_ERROR]' in stdout \
Christoffer Quist Adamsen13e43132022-08-05 11:31:56 +0200351 or expected_error in stderr, \
352 'stdout: %s, stderr: %s' % (stdout, stderr)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100353
354def unlock(device_id=None, device_pin=None):
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200355 ensure_screen_on(device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100356 screen_state = get_screen_state(device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100357 assert screen_state.is_on(), 'was %s' % screen_state
358 if screen_state.is_on_and_locked():
359 if device_pin is not None:
360 raise NotImplementedError('Device unlocking with pin not implemented')
361 issue_key_event('KEYCODE_MENU', device_id)
362 screen_state = get_screen_state(device_id)
363 assert screen_state.is_on_and_unlocked(), 'was %s' % screen_state
Christoffer Quist Adamsen21f7f352022-05-30 19:52:43 +0200364
365def parse_options(argv):
366 result = argparse.ArgumentParser(description='Run adb utils.')
367 result.add_argument('--device-id',
368 help='Device id (e.g., emulator-5554).')
369 result.add_argument('--device-pin',
370 help='Device pin code (e.g., 1234)')
371 result.add_argument('--ensure-screen-off',
372 help='Ensure screen off',
373 action='store_true',
374 default=False)
375 result.add_argument('--get-screen-state',
376 help='Get screen state',
377 action='store_true',
378 default=False)
379 result.add_argument('--unlock',
380 help='Unlock device',
381 action='store_true',
382 default=False)
383 options, args = result.parse_known_args(argv)
384 return options, args
385
386def main(argv):
387 (options, args) = parse_options(argv)
388 if options.ensure_screen_off:
389 ensure_screen_off(options.device_id)
390 elif options.get_screen_state:
391 print(get_screen_state(options.device_id))
392 elif options.unlock:
393 unlock(options.device_id, options.device_pin)
394
395
396if __name__ == '__main__':
397 sys.exit(main(sys.argv[1:]))