Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
141 lines
5.2 KiB
Python
141 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise Gaia Photo/Gallery on an Android emulator with its virtual sensor."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
spec = importlib.util.spec_from_file_location("probe", Path(__file__).with_name("probe-android.py"))
|
|
probe = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(probe)
|
|
ADB = str(ROOT / "android/toolchain/sdk/platform-tools/adb")
|
|
|
|
|
|
def adb(*args):
|
|
return subprocess.check_output([ADB, "-s", "emulator-5554", *args], text=True).strip()
|
|
|
|
|
|
assert (
|
|
adb("shell", "getprop", "ro.kernel.qemu") == "1"
|
|
), "Use a virtual sensor, not a personal camera."
|
|
|
|
|
|
def ev(app, code):
|
|
reply = probe.run(app, code)
|
|
if "error" in reply:
|
|
raise AssertionError(reply)
|
|
return reply.get("value")
|
|
|
|
|
|
def wait(app, code):
|
|
for _ in range(60):
|
|
value = ev(app, code)
|
|
if value:
|
|
return value
|
|
time.sleep(0.3)
|
|
raise TimeoutError(app + ": " + code)
|
|
|
|
|
|
def launch(app):
|
|
ev(
|
|
"system",
|
|
'VulpesCompat.call("apps.launch",{manifestURL:"http://'
|
|
+ app
|
|
+ '.localhost:18765/manifest.webapp"})',
|
|
)
|
|
wait(app, 'document.readyState==="complete" && !document.hidden')
|
|
|
|
|
|
result = {"scope": "Android 16 x86_64 emulator, virtual camera; physical phones untested."}
|
|
wait("system", 'document.body?.getAttribute("ready-state")==="fullyLoaded"')
|
|
ev("system", "(ScreenManager.turnScreenOn(true),true)")
|
|
time.sleep(.5)
|
|
ev("system", "(lockScreen.unlock(true),true)")
|
|
wait("system", "!lockScreen.locked")
|
|
time.sleep(.5)
|
|
launch("camera")
|
|
wait(
|
|
"camera",
|
|
'!!app.loaded && !!app.camera.mozCamera && !app.camera.isBusy && !!document.querySelector(".viewfinder video")?.videoWidth',
|
|
)
|
|
result["preview"] = ev(
|
|
"camera",
|
|
'(()=>{const v=document.querySelector(".viewfinder video");return {width:v.videoWidth,height:v.videoHeight,cameras:navigator.mozCameras.getListOfCameras()}})()',
|
|
)
|
|
result["original_gaia_ui"] = ev(
|
|
"camera",
|
|
'!!document.querySelector(".js-capture") && !document.querySelector(".vulpes-capability")',
|
|
)
|
|
assert result["original_gaia_ui"]
|
|
ev(
|
|
"camera",
|
|
'(async()=>{window.__beforeCapture=(await VulpesCompat.call("media.request",{type:"pictures",operation:"enumerate"})).map(f=>f.name);window.__streamBefore=app.camera.mozCamera;return true})()',
|
|
)
|
|
point = ev(
|
|
"camera",
|
|
'(()=>{const r=document.querySelector(".js-capture").getBoundingClientRect();return {x:(mozInnerScreenX+r.x+r.width/2)*devicePixelRatio,y:(mozInnerScreenY+r.y+r.height/2)*devicePixelRatio}})()',
|
|
)
|
|
adb("shell", "input", "tap", str(round(point["x"])), str(round(point["y"])))
|
|
photo = wait(
|
|
"camera",
|
|
'(async()=>(await VulpesCompat.call("media.request",{type:"pictures",operation:"enumerate"})).find(f=>!__beforeCapture.includes(f.name)))()',
|
|
)
|
|
assert photo["size"] > 0 and photo["type"] == "image/jpeg"
|
|
result["photo"] = photo
|
|
launch("gallery")
|
|
result["camera_stops_when_hidden"] = wait(
|
|
"camera", '__streamBefore.getTracks().every(t=>t.readyState==="ended")'
|
|
)
|
|
name = json.dumps(photo["name"])
|
|
result["gallery_indexes_photo"] = wait(
|
|
"gallery",
|
|
'typeof files!=="undefined" && files.some(f=>f.name==='
|
|
+ name
|
|
+ " && f.metadata.width>0 && f.metadata.thumbnail)",
|
|
)
|
|
ev(
|
|
"gallery",
|
|
'(()=>{const e=[...document.querySelectorAll(".thumbnailImage")].find(e=>e.dataset.filename==='
|
|
+ name
|
|
+ ");e.click();return true})()",
|
|
)
|
|
result["gallery_opens_photo"] = wait(
|
|
"gallery", 'document.body.classList.contains("fullscreenView")'
|
|
)
|
|
result["gallery_import_button"] = ev(
|
|
"gallery", '!!document.querySelector("gaia-header [data-vulpes-import]") && !document.querySelector(".vulpes-import")'
|
|
)
|
|
assert result["gallery_import_button"]
|
|
ev("gallery", "(launchCameraApp(),true)")
|
|
wait("camera", "!document.hidden")
|
|
result["camera_resumes"] = wait(
|
|
"camera",
|
|
'!!app.camera.mozCamera && app.camera.mozCamera!==__streamBefore && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live") && !app.camera.isBusy',
|
|
)
|
|
ev("camera", "(window.__streamBefore=app.camera.mozCamera,true)")
|
|
adb("shell", "input", "keyevent", "KEYCODE_HOME")
|
|
result["camera_stops_in_android_background"] = wait(
|
|
"camera", '__streamBefore.getTracks().every(t=>t.readyState==="ended")'
|
|
)
|
|
adb("shell", "am", "start", "-n", "org.vulpes_os.preview/.MainActivity")
|
|
result["camera_resumes_from_android_background"] = wait(
|
|
"camera",
|
|
'!document.hidden && !!app.camera.mozCamera && app.camera.mozCamera!==__streamBefore && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live") && !app.camera.isBusy',
|
|
)
|
|
result["capability_controls"] = ev(
|
|
"camera",
|
|
'document.querySelector(".js-flash").getAttribute("aria-disabled")==="true" && (document.querySelector(".mode-switch").getAttribute("aria-disabled")!=="true") === !!Object.keys(app.camera.mozCamera.capabilities.recorderProfiles).length',
|
|
)
|
|
assert result["capability_controls"]
|
|
result["errors"] = ev(
|
|
"camera", '__vulpesDiagnostics.filter(d=>d.kind==="error" || d.kind==="rejection")'
|
|
)
|
|
assert not result["errors"], result
|
|
launch("gallery")
|
|
(ROOT / "android/logs/camera-results.json").write_text(
|
|
json.dumps(result, indent=2, ensure_ascii=False) + "\n"
|
|
)
|
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|