IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
200
iqpilot/system/android/androidd.py
Normal file
200
iqpilot/system/android/androidd.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from iqpilot.cereal import messaging
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.realtime import Ratekeeper
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
|
||||
ANDROID_ROOT = "/data/android"
|
||||
HEADLESS = f"{ANDROID_ROOT}/waydroid_headless.sh"
|
||||
TRIM = f"{ANDROID_ROOT}/android_trim.sh"
|
||||
GUARD = f"{ANDROID_ROOT}/android_guard.sh"
|
||||
LXC_ATTACH = f"{ANDROID_ROOT}/root/usr/bin/lxc-attach"
|
||||
LD = f"{ANDROID_ROOT}/root/usr/lib/aarch64-linux-gnu"
|
||||
|
||||
# Waze's launcher is FreeMapAppActivity; there is no com.waze.MainActivity, and asking for
|
||||
# one fails with "Activity class does not exist" rather than anything that names the problem.
|
||||
NAV_APPS = {
|
||||
"waze": ("com.waze", "com.waze/.FreeMapAppActivity"),
|
||||
"maps": ("com.google.android.apps.maps", "com.google.android.apps.maps/com.google.android.maps.MapsActivity"),
|
||||
}
|
||||
BRIDGE_HOST = "192.168.240.112"
|
||||
BRIDGE_PORT = 8099
|
||||
GPS_SOURCES = ["iqLiveLocation", "liveLocationKalman", "gpsLocationExternal", "gpsLocation"]
|
||||
PROVIDERS = ("gps", "fused", "network")
|
||||
GPS_INTERVAL_S = 0.5
|
||||
|
||||
|
||||
def attach(*args: str, timeout: float = 10.0) -> subprocess.CompletedProcess:
|
||||
cmd = ["sudo", "-n", "env", f"LD_LIBRARY_PATH={LD}", LXC_ATTACH,
|
||||
"-P", "/var/lib/waydroid/lxc", "-n", "waydroid", "--", *args]
|
||||
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False)
|
||||
|
||||
|
||||
def container_booted() -> bool:
|
||||
try:
|
||||
return attach("/system/bin/getprop", "sys.boot_completed", timeout=8).stdout.strip() == "1"
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def bring_up() -> None:
|
||||
# AGNOS mounts / read-only, so no unit file can be installed; a transient unit is the only
|
||||
# way the compositor and lxc-start survive this process exiting.
|
||||
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android", "--service-type=oneshot",
|
||||
"--remain-after-exit", HEADLESS, "up"],
|
||||
capture_output=True, text=True, timeout=120, check=False)
|
||||
|
||||
|
||||
def start_guard() -> None:
|
||||
subprocess.run(["sudo", "-n", "systemd-run", "--unit=iq-android-guard", GUARD],
|
||||
capture_output=True, text=True, timeout=30, check=False)
|
||||
|
||||
|
||||
def trim(nav_app: str) -> None:
|
||||
subprocess.run(["sudo", "-n", "env", f"ANDROID_NAV_APP={nav_app}", TRIM],
|
||||
capture_output=True, timeout=240, check=False)
|
||||
|
||||
|
||||
def enable_mock_location() -> None:
|
||||
# Wiped by every container restart, and the package form does not cover uid 0: without the
|
||||
# --uid form every injection fails with SecurityException and the apps just say "no GPS".
|
||||
attach("/system/bin/cmd", "appops", "set", "--uid", "0", "android:mock_location", "allow")
|
||||
for p in PROVIDERS:
|
||||
attach("/system/bin/cmd", "location", "providers", "add-test-provider", p)
|
||||
attach("/system/bin/cmd", "location", "providers", "set-test-provider-enabled", p, "true")
|
||||
# The a11y tree is only updated while the display is awake; asleep it emits nothing at all.
|
||||
attach("/system/bin/svc", "power", "stayon", "true")
|
||||
|
||||
|
||||
def inject(lat: float, lon: float) -> None:
|
||||
for p in PROVIDERS:
|
||||
attach("/system/bin/cmd", "location", "providers", "set-test-provider-location", p,
|
||||
"--location", f"{lat:.6f},{lon:.6f}", "--accuracy", "4", timeout=6)
|
||||
|
||||
|
||||
def read_position(sm: messaging.SubMaster) -> tuple[float, float] | None:
|
||||
for src in GPS_SOURCES:
|
||||
if src not in sm.data or not sm.valid.get(src, False):
|
||||
continue
|
||||
msg = sm[src]
|
||||
for lat_a, lon_a in (("latitude", "longitude"), ("lat", "lon")):
|
||||
lat = getattr(msg, lat_a, None)
|
||||
lon = getattr(msg, lon_a, None)
|
||||
if lat is not None and lon is not None and (lat or lon):
|
||||
return float(lat), float(lon)
|
||||
return None
|
||||
|
||||
|
||||
class AndroidNavDaemon:
|
||||
def __init__(self) -> None:
|
||||
self.params = Params()
|
||||
self.sources = [s for s in GPS_SOURCES if s in messaging.SERVICE_LIST]
|
||||
self.sm = messaging.SubMaster(self.sources) if self.sources else None
|
||||
self.last_inject = 0.0
|
||||
self.up = False
|
||||
|
||||
def status(self, text: str) -> None:
|
||||
self.params.put("IQAndroidNavStatus", text)
|
||||
|
||||
def sign_in(self) -> None:
|
||||
email = self.params.get("IQAndroidNavEmail", encoding="utf8")
|
||||
password = self.params.get("IQAndroidNavPassword", encoding="utf8")
|
||||
if not email or not password:
|
||||
return
|
||||
try:
|
||||
with socket.create_connection((BRIDGE_HOST, BRIDGE_PORT), timeout=10) as s:
|
||||
s.sendall(json.dumps({"cmd": "signin", "email": email, "password": password}).encode() + b"\n")
|
||||
self.status("sign-in requested")
|
||||
except OSError:
|
||||
cloudlog.exception("androidd: sign-in request failed")
|
||||
self.status("sign-in failed")
|
||||
finally:
|
||||
# Never let the credential outlive the one use it was handed over for.
|
||||
self.params.remove("IQAndroidNavEmail")
|
||||
self.params.remove("IQAndroidNavPassword")
|
||||
|
||||
def nav_app(self) -> str:
|
||||
value = self.params.get("IQAndroidNavApp", encoding="utf8") or "waze"
|
||||
return value if value in NAV_APPS else "waze"
|
||||
|
||||
def enforce_single_app(self, nav_app: str) -> None:
|
||||
for name, (pkg, _) in NAV_APPS.items():
|
||||
if name != nav_app:
|
||||
attach("/system/bin/am", "force-stop", pkg)
|
||||
|
||||
def ensure_app_running(self, nav_app: str) -> None:
|
||||
pkg, component = NAV_APPS[nav_app]
|
||||
if attach("/system/bin/pidof", pkg, timeout=8).stdout.strip():
|
||||
return
|
||||
attach("/system/bin/am", "start", "-n", component, timeout=20)
|
||||
|
||||
def ensure_up(self) -> None:
|
||||
if container_booted():
|
||||
if not self.up:
|
||||
nav_app = self.nav_app()
|
||||
enable_mock_location()
|
||||
trim(nav_app)
|
||||
self.enforce_single_app(nav_app)
|
||||
start_guard()
|
||||
self.status(f"running:{nav_app}")
|
||||
self.up = True
|
||||
return
|
||||
self.up = False
|
||||
self.status("starting")
|
||||
bring_up()
|
||||
|
||||
def step(self) -> None:
|
||||
if not self.params.get_bool("IQAndroidNav"):
|
||||
if self.up:
|
||||
subprocess.run(["sudo", "-n", "systemctl", "stop", "iq-android", "iq-android-guard"],
|
||||
capture_output=True, timeout=60, check=False)
|
||||
self.up = False
|
||||
self.status("disabled")
|
||||
return
|
||||
|
||||
self.ensure_up()
|
||||
if not self.up:
|
||||
return
|
||||
|
||||
self.sign_in()
|
||||
self.ensure_app_running(self.nav_app())
|
||||
|
||||
if self.sm is None:
|
||||
return
|
||||
self.sm.update(0)
|
||||
now = time.monotonic()
|
||||
if now - self.last_inject < GPS_INTERVAL_S:
|
||||
return
|
||||
position = read_position(self.sm)
|
||||
if position is not None:
|
||||
self.last_inject = now
|
||||
try:
|
||||
inject(*position)
|
||||
except subprocess.SubprocessError:
|
||||
cloudlog.exception("androidd: location inject failed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.exists(HEADLESS):
|
||||
cloudlog.warning("androidd: android stack not staged, exiting")
|
||||
return
|
||||
daemon = AndroidNavDaemon()
|
||||
rk = Ratekeeper(2.0, print_delay_threshold=None)
|
||||
while True:
|
||||
try:
|
||||
daemon.step()
|
||||
except Exception:
|
||||
cloudlog.exception("androidd: step failed")
|
||||
rk.keep_time()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user