"""Run registered BLE tests; WiFi reapplication requires its explicit option.""" import argparse import json from pathlib import Path import subprocess import sys import time sys.path.insert(0, str(Path(__file__).resolve().parent / '测试设备登记')) from prepare_test_device import load_registration, check_connected from remote_support import connect, execute def main(): parser = argparse.ArgumentParser() parser.add_argument('--adb', required=True) parser.add_argument('--build', type=Path, required=True) parser.add_argument('--poll-cycles', type=int, default=60) parser.add_argument('--test', choices=('ble', 'activity', 'layout'), default='ble') parser.add_argument('--root-input', action='store_true', help='Compatibility flag; activity tests always use authorized root input') parser.add_argument('--reapply-current-wifi', action='store_true', help='Explicitly reapply the already active saved WiFi profile over BLE; may briefly reconnect WiFi') parser.add_argument('--verify-current-controls', action='store_true', help='Reapply current brightness; briefly pause active animation and restore without selecting new content') parser.add_argument('--toggle-bluetooth', action='store_true', help='Requires explicit root Bluetooth-switch authorization plus activity root input') parser.add_argument('--grant-root', action='store_true', help='Compatibility flag; runner always verifies and grants authorized App permissions') args = parser.parse_args() args.grant_root = True if args.test in ("activity", "layout"): args.root_input = True if not 0 <= args.poll_cycles <= 60: parser.error('poll-cycles must be between 0 and 60') if args.root_input and args.test == 'ble': parser.error('root-input only applies to activity tests') if args.reapply_current_wifi and args.test != 'ble': parser.error('reapply-current-wifi only applies to BLE tests') if args.verify_current_controls and args.test != 'ble': parser.error('verify-current-controls only applies to BLE tests') if args.toggle_bluetooth and not (args.test == 'activity' and args.root_input): parser.error('toggle-bluetooth requires activity and root-input') device = next(d for d in load_registration() if d['alias'] == 'android-test-a') check_connected(args.adb, device) root_check = subprocess.run([args.adb, '-s', device['serial'], 'shell', 'su', '-c', 'id -u'], capture_output=True, text=True, timeout=15) if root_check.returncode or root_check.stdout.strip() != '0': raise RuntimeError('测试手机 root 权限不可用;停止测试,等待用户处理,不使用备用注入方式') client, fields = connect() try: code, output, error = execute(client, "python3 -c \"import json; p=json.load(open('/var/lib/matrix-screen-controller/mobile/identity.json')); print(json.dumps({'device_id':p['device_id']}))\"", password=fields['密码']) if code: raise RuntimeError('Cannot read authorized board identity') board_id = json.loads(output)['device_id'] from uuid import UUID UUID(board_id) board_name = 'QMS-' + board_id.replace('-', '')[:8] code, output, _ = execute(client, "python3 -c \"import urllib.request,json,hashlib; d=json.load(urllib.request.urlopen('http://127.0.0.1:8080/api/library/order',timeout=5)); print(hashlib.sha256('\\n'.join(i['type']+'|'+i['id'] for i in d['items']).encode()).hexdigest())\"", password=fields['密码']) library_hash = output.strip() if code or len(library_hash) != 64 or any(c not in '0123456789abcdef' for c in library_hash): raise RuntimeError('Cannot read device library order digest') if not board_name.startswith('QMS-') or len(board_name) != 12: raise RuntimeError('Invalid board identity') finally: client.close() def adb(*command, timeout=120, quiet=False): try: result = subprocess.run([args.adb, '-s', device['serial'], *command], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout) except subprocess.TimeoutExpired: # ADB's host timeout does not stop Android instrumentation itself. if command[:3] == ('shell', 'am', 'instrument'): subprocess.run([args.adb, '-s', device['serial'], 'shell', 'am', 'force-stop', 'org.qimiaoscreen.controller'], capture_output=True, timeout=15) raise RuntimeError('ADB operation timed out; device identifiers omitted') from None output = (result.stdout + result.stderr).replace(device['serial'], '[test-phone]').replace(board_name, '[test-board]').replace(board_id, '[test-board-id]') if not quiet: print(output, flush=True) if result.returncode: raise RuntimeError('ADB command failed') return output for relative in ('androidApp/build/outputs/apk/debug/androidApp-debug.apk', 'androidApp/build/outputs/apk/androidTest/debug/androidApp-debug-androidTest.apk'): apk = args.build / relative if not apk.is_file(): raise RuntimeError('APK missing') adb('install', '-r', str(apk)) adb('shell', 'am', 'force-stop', 'org.qimiaoscreen.controller') if args.grant_root: # Some vendor package managers asynchronously reset permissions after # install succeeds. Verify after a settling window, not just at grant. for attempt in range(3): for permission in ('BLUETOOTH_SCAN', 'BLUETOOTH_CONNECT'): adb('shell', 'su', '-c', 'pm grant org.qimiaoscreen.controller android.permission.' + permission) time.sleep(3) verified = adb('shell', 'dumpsys', 'package', 'org.qimiaoscreen.controller', quiet=True) if all('android.permission.' + p + ': granted=true' in verified for p in ('BLUETOOTH_SCAN', 'BLUETOOTH_CONNECT')): break else: raise RuntimeError('Bluetooth permission did not remain granted after installation') if args.root_input: adb('push', str(Path(__file__).with_name('authorized_app_input.sh')), '/data/local/tmp/qms-authorized-input.sh', quiet=True) layout_original = None if args.test == "layout": adb("push", str(Path(__file__).with_name("authorized_root_command.sh")), "/data/local/tmp/qms-authorized-root.sh", quiet=True) def root(command): return adb("shell", "su", "-c", command, quiet=True).strip() import re layout_original = {key: root("settings get system " + key) for key in ("font_scale", "accelerometer_rotation", "user_rotation")} layout_density = re.search(r"Override density: (\d+)", root("wm density")) layout_density = layout_density[1] if layout_density else "reset" dimensions = re.findall(r"(\d+)x(\d+)", root("wm size"))[-1] narrow = min(map(int, dimensions)) * 160 // 340 try: for font, rotation in (("1.0", "0"), ("1.3", "0"), ("1.3", "1")) if args.test == "layout" else ((None, None),): if layout_original: root("am force-stop org.qimiaoscreen.controller") root("wm density " + str(narrow)) root("settings put system font_scale " + font) root("settings put system accelerometer_rotation 0") root("settings put system user_rotation " + rotation) root("input keyevent KEYCODE_WAKEUP") root("wm dismiss-keyguard") root("am start -W -n org.qimiaoscreen.controller/.MainActivity") output = adb('shell', 'am', 'instrument', '-w', '-r', '-e', 'device_name', board_name, '-e', 'poll_cycles', str(args.poll_cycles), '-e', 'library_order_hash', library_hash, '-e', 'root_input', str(args.root_input).lower(), '-e', 'reapply_current_wifi', str(args.reapply_current_wifi).lower(), '-e', 'verify_current_controls', str(args.verify_current_controls).lower(), '-e', 'toggle_bluetooth', str(args.toggle_bluetooth).lower(), '-e', 'class', 'org.qimiaoscreen.controller.' + {'ble': 'BleIntegrationTest', 'activity': 'AppLifecycleTest', 'layout': 'LayoutTest'}[args.test], 'org.qimiaoscreen.controller.test/androidx.test.runner.AndroidJUnitRunner', timeout=360) if "OK (1 test)" not in output: raise RuntimeError("Instrumentation did not pass") if layout_original: print("Layout passed: font=" + font + ", rotation=" + rotation) finally: if layout_original: root("am force-stop org.qimiaoscreen.controller") root("wm density " + layout_density) for key, value in layout_original.items(): root("settings delete system " + key if value == "null" else "settings put system " + key + " " + value) if root("settings get system " + key) != value: raise RuntimeError("Layout settings restore failed") if args.test == "layout": adb("shell", "rm", "-f", "/data/local/tmp/qms-authorized-root.sh", quiet=True) if args.toggle_bluetooth: adb('shell', 'su', '-c', 'cmd bluetooth_manager enable', quiet=True) if args.root_input: adb('shell', 'rm', '-f', '/data/local/tmp/qms-authorized-input.sh', quiet=True) if 'OK (1 test)' not in output: raise RuntimeError('BLE instrumentation did not pass') if __name__ == '__main__': main()