#!/usr/bin/env python3 """Measure display-cutout behaviour of an installed PWA across viewport-fit modes. Reads the WebappActivity window block *specifically* (a neighbouring window such as com.android.systemui.wallpapers.ImageWallpaper_system also carries a layoutInDisplayCutoutMode attribute and is easy to misattribute). Usage: ANDROID_SERIAL= ./cutout_matrix.py [--shortcut 'Fit Switch'] [--portrait] Reports, per viewport-fit value: cutoutMode - layoutInDisplayCutoutMode on the webapp window (absent == DEFAULT) frame - window frame; a left/top offset means the OS inset the window for the cutout inner/screen- if inner < screen the page is fitted; if equal the page is edge to edge safe - the page's safe-area-inset-* values """ import argparse import json import os import re import subprocess import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import cdp_adb # noqa: E402 FITS = ("cover", "auto", "contain") FLAG = "_ --disable-fre --no-first-run --enable-features=WebAppShortEdgesCutoutMode" PROBE = """(()=>{const p=document.querySelector("#probe"),s=getComputedStyle(p); return{fit:document.querySelector("button.active")?.dataset.fit, inner:innerWidth+"x"+innerHeight,screen:screen.width+"x"+screen.height, safe:[s.paddingLeft,s.paddingTop,s.paddingRight,s.paddingBottom].join("/")}})()""" def adb(*args, **kw): return subprocess.run(("adb",) + args, capture_output=True, text=True, **kw).stdout def in_page(expression): """Evaluate in the sampler page over CDP, reconnecting each time.""" cdp = cdp_adb.CDP(cdp_adb.find_target("e2e-mode-switch")) try: return cdp.evaluate(expression) finally: cdp.close() def webapp_window(): """Return (cutout_mode, frame) for the WebappActivity window only.""" txt = adb("shell", "dumpsys", "window", "windows") for block in re.split(r"(?= Window #\d+ Window\{)", txt): if "webapps.WebappActivity" not in block or "mAttrs=" not in block: continue attrs = next(l for l in block.split("\n") if "mAttrs=" in l) mode = re.search(r"layoutInDisplayCutoutMode=(\w+)", attrs) frame = re.search(r"frame=\[([0-9,]+)\]\[([0-9,]+)\]", block) return ( mode.group(1) if mode else "DEFAULT(absent)", f"[{frame.group(1)}][{frame.group(2)}]" if frame else "?", ) return None, None def main(): ap = argparse.ArgumentParser() ap.add_argument("--shortcut", default="Fit Switch") ap.add_argument("--portrait", action="store_true", help="measure portrait instead") ap.add_argument("--json", help="write raw results here") args = ap.parse_args() serial = os.environ.get("ANDROID_SERIAL", "") rotation = 0 if args.portrait else 1 model = adb("shell", "getprop", "ro.product.model").strip() release = adb("shell", "getprop", "ro.build.version.release").strip() sdk = adb("shell", "getprop", "ro.build.version.sdk").strip() version = "" for line in adb("shell", "dumpsys", "package", "org.chromium.chrome").splitlines(): if "versionName" in line: version = line.strip() break # The command line file is only honoured when debug_app is set to the package. adb("shell", f"printf '%s\\n' '{FLAG}' > /data/local/tmp/chrome-command-line") adb("shell", "settings", "put", "global", "debug_app", "org.chromium.chrome") adb("shell", "am", "force-stop", "org.chromium.chrome") # Launch in portrait: the launcher hides/reflows shortcuts when rotation is locked. adb("shell", "cmd", "window", "fixed-to-user-rotation", "enabled") adb("shell", "cmd", "window", "user-rotation", "lock", "0") adb("shell", "input", "keyevent", "HOME") time.sleep(3) launcher = os.path.join(os.path.dirname(os.path.abspath(__file__)), "launch_shortcut.py") subprocess.run([launcher, args.shortcut], check=False) time.sleep(8) top = adb("shell", "dumpsys", "activity", "activities") if "webapps.WebappActivity" not in top: print("ERROR: WebappActivity is not on top, is the PWA installed?", file=sys.stderr) return 1 adb("shell", "cmd", "window", "user-rotation", "lock", str(rotation)) time.sleep(4) print(f"device : {model} / Android {release} (SDK {sdk}) / {serial}") print(f"chromium : {version}") print(f"mode : {'portrait' if args.portrait else 'landscape'}, flag on\n") print(f"{'fit':8} {'cutoutMode':18} {'frame':24} page") results = [] for fit in FITS: in_page(f"document.querySelector('[data-fit=\"{fit}\"]').click(); true") time.sleep(3) mode, frame = webapp_window() page = in_page(PROBE) results.append({"fit": fit, "cutoutMode": mode, "frame": frame, "page": page}) print(f"{fit:8} {mode:18} {frame:24} {json.dumps(page, separators=(',', ':'))}") # Restore. adb("shell", "cmd", "window", "user-rotation", "free") adb("shell", "cmd", "window", "fixed-to-user-rotation", "default") adb("shell", "am", "force-stop", "org.chromium.chrome") adb("shell", "printf '%s\\n' '_ --disable-fre --no-first-run' > /data/local/tmp/chrome-command-line") if args.json: with open(args.json, "w") as fh: json.dump( {"model": model, "release": release, "sdk": sdk, "chromium": version, "results": results}, fh, indent=2) print(f"\nwrote {args.json}") return 0 if __name__ == "__main__": sys.exit(main())