Files
StefanandClaude Opus 5 ad2c98443b Phase 0: Repo-Gerüst, Dokumentation und Machbarkeits-Spike
Vier Beweise, die das Projekt tragen, als eigenständig lauffähige Skripte:

  S1  Plasma Mobile in proot über genestetes kwin_wayland im X11-Backend.
      Fällt selbsttätig auf startplasmamobile und dann auf XFCE zurück, damit
      ein Fehlschlag eindeutig Plasma zuzuordnen ist und nicht der Umgebung.
  S2  Mikrofon über PulseAudio mit OpenSL-Quelle. Misst den Pegel statt nur die
      Dateigröße — der typische Fehlschlag ist eine formal korrekte WAV-Datei
      voller Stille.
  S3  Virtuelle PipeWire-Kamera per GStreamer-pipewiresink, ohne Kernel-Modul.
      Setzt die Firefox-Einstellung vorab, damit der Test nicht an einem
      vergessenen Häkchen scheitert.
  S4  Intent-Handoff mit lauffähiger Mini-Bridge: xdg-open-Handler für geo:,
      tel: und sms:, Token-Authentifizierung, feste Liste erlaubter Schemata.

provision/provision.sh setzt die Hintergrundbeschränkungen per ADB und benennt,
was ADB nicht kann und von Hand erledigt werden muss.

Dokumentiert sind auch die sieben verworfenen Wege — Waydroid, AVF, QEMU, SIP,
APK-Daten-Redirect, Fenster-Durchreichung und der Android-Server — mit Belegen,
damit sie nicht erneut durchdacht werden müssen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 17:25:36 +02:00

169 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""HPOS Mini-Bridge — Prototyp des Intent-Handoffs für Beweis S4.
Läuft in Termux, lauscht auf 127.0.0.1 und übersetzt URIs vom Linux in
Android-Intents. Das ist die kleinste ehrliche Fassung dessen, was später die
Bridge-APK macht: sie speichert nichts, sie reicht durch.
Bewusst eng gehalten:
* nur 127.0.0.1, niemals extern erreichbar
* gemeinsames Token, damit nicht jede App auf dem Telefon Intents auslösen kann
* feste Liste erlaubter Schemata statt beliebiger Intents
Start: python spike/bridge-mini.py
"""
from __future__ import annotations
import os
import shlex
import socketserver
import subprocess
import sys
import time
import urllib.parse
HOST = "127.0.0.1"
PORT = int(os.environ.get("HPOS_BRIDGE_PORT", "8474"))
BASIS = os.path.dirname(os.path.abspath(__file__))
TOKEN_DATEI = os.path.join(BASIS, "out", "bridge-token")
# Paketname einer Begleit-App, die vor der Hauptanwendung starten soll — typisch
# eine App mit Overlay, das über der Navigation liegen muss. Leer = keine.
BEGLEIT_APP = os.environ.get("HPOS_BEGLEIT_APP", "").strip()
BEGLEIT_VORLAUF = float(os.environ.get("HPOS_BEGLEIT_VORLAUF", "2.5"))
# Was aus dem Linux kommen darf. Alles andere wird abgelehnt — eine offene
# Intent-Schnittstelle wäre ein Einfallstor für jede App auf dem Gerät.
ERLAUBTE_SCHEMATA = {"geo", "tel", "sms", "smsto", "mailto", "http", "https"}
def token_lesen() -> str:
"""Token holen, beim ersten Start erzeugen."""
os.makedirs(os.path.dirname(TOKEN_DATEI), exist_ok=True)
if not os.path.exists(TOKEN_DATEI):
with open(os.open(TOKEN_DATEI, os.O_CREAT | os.O_WRONLY, 0o600), "w") as f:
f.write(os.urandom(16).hex())
with open(TOKEN_DATEI) as f:
return f.read().strip()
TOKEN = token_lesen()
def am(*args: str) -> tuple[int, str]:
"""`am` aufrufen und Ergebnis zurückgeben."""
befehl = ["am", *args]
print(f" → {' '.join(shlex.quote(a) for a in befehl)}", flush=True)
try:
p = subprocess.run(befehl, capture_output=True, text=True, timeout=20)
return p.returncode, (p.stdout + p.stderr).strip()
except FileNotFoundError:
return 127, "am nicht gefunden — Paket termux-tools fehlt"
except subprocess.TimeoutExpired:
return 124, "am hat nicht geantwortet"
def app_starten(paket: str) -> None:
am("start", "--user", "0", "-a", "android.intent.action.MAIN",
"-c", "android.intent.category.LAUNCHER", "-p", paket)
def navigation(ziel: str) -> tuple[int, str]:
"""Turn-by-turn direkt starten statt nur die Karte zu zeigen.
Vorher optional die Begleit-App, damit deren Overlay schon steht, wenn die
Navigation in den Vordergrund kommt — andersherum legt sich die Navigation
darüber und das Overlay ist verdeckt.
"""
if BEGLEIT_APP:
print(f" Begleit-App vorab: {BEGLEIT_APP}", flush=True)
app_starten(BEGLEIT_APP)
time.sleep(BEGLEIT_VORLAUF)
uri = "google.navigation:q=" + urllib.parse.quote(ziel) + "&mode=d"
return am("start", "--user", "0", "-a", "android.intent.action.VIEW", "-d", uri)
def behandeln(zeile: str) -> str:
"""Eine Anfrage verarbeiten. Format: <token>\\t<uri>"""
teile = zeile.split("\t", 1)
if len(teile) != 2:
return "FEHLER ungueltiges-format"
token, uri = teile[0].strip(), teile[1].strip()
if token != TOKEN:
print(" ! Anfrage mit falschem Token abgelehnt", flush=True)
return "FEHLER token"
if not uri:
return "FEHLER leere-uri"
schema = uri.split(":", 1)[0].lower()
# geo:-URIs mit Suchbegriff bedeuten in der Praxis fast immer "dahin navigieren",
# nicht "Karte anschauen". Deshalb wird daraus Turn-by-turn.
if schema == "geo":
query = urllib.parse.parse_qs(urllib.parse.urlparse(uri).query).get("q", [""])[0]
if query:
rc, aus = navigation(query)
return f"OK navigation {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
if schema == "tel":
# DIAL statt CALL: die Nummer erscheint im Wähler, gewählt wird bewusst.
rc, aus = am("start", "--user", "0", "-a", "android.intent.action.DIAL", "-d", uri)
return f"OK dial {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
if schema in ERLAUBTE_SCHEMATA:
rc, aus = am("start", "--user", "0", "-a", "android.intent.action.VIEW", "-d", uri)
return f"OK view {rc} {aus}" if rc == 0 else f"FEHLER am {rc} {aus}"
print(f" ! Schema abgelehnt: {schema}", flush=True)
return f"FEHLER schema-nicht-erlaubt {schema}"
class Handler(socketserver.StreamRequestHandler):
timeout = 10
def handle(self) -> None:
try:
zeile = self.rfile.readline(4096).decode("utf-8", "replace")
except Exception as e:
print(f" ! Lesefehler: {e}", flush=True)
return
if not zeile:
return
antwort = behandeln(zeile)
print(f" ← {antwort}", flush=True)
try:
self.wfile.write((antwort + "\n").encode())
except Exception:
pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
def main() -> int:
print("HPOS Mini-Bridge (Prototyp für Beweis S4)")
print(f" Lauscht auf {HOST}:{PORT}")
print(f" Token-Datei {TOKEN_DATEI}")
print(f" Begleit-App {BEGLEIT_APP or '(keine)'}")
print(" Beenden mit Strg+C")
print()
try:
with Server((HOST, PORT), Handler) as srv:
srv.serve_forever()
except KeyboardInterrupt:
print("\nBeendet.")
except OSError as e:
print(f"Start fehlgeschlagen: {e}", file=sys.stderr)
print(f"Läuft schon etwas auf Port {PORT}?", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())