Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
157d702f5b | ||
|
|
15fbc8ff9c | ||
|
|
5912e47e32 | ||
|
|
5cb2f71c41 |
@@ -97,7 +97,7 @@ ARIA hat zwei Rollen:
|
|||||||
| Compute-Node(s) (STT/TTS/LLM) | 1..n GPU-Rechner | `cd xtts && cp .env.example .env && docker compose up -d` |
|
| Compute-Node(s) (STT/TTS/LLM) | 1..n GPU-Rechner | `cd xtts && cp .env.example .env && docker compose up -d` |
|
||||||
| Satellit(en) 🛰️ (optional) | Fremdes Netz (Büro …) | `cd satellite && cp .env.example .env && docker compose up -d --build` |
|
| Satellit(en) 🛰️ (optional) | Fremdes Netz (Büro …) | `cd satellite && cp .env.example .env && docker compose up -d --build` |
|
||||||
| Host-Agent(en) 💻 (optional) | Direkt auf einem Rechner | `cd host-agent && ./build.sh` → Binary + `.env` auf den Rechner, starten (Details unten & in [`host-agent/README.md`](host-agent/README.md)) |
|
| Host-Agent(en) 💻 (optional) | Direkt auf einem Rechner | `cd host-agent && ./build.sh` → Binary + `.env` auf den Rechner, starten (Details unten & in [`host-agent/README.md`](host-agent/README.md)) |
|
||||||
| Android-Agent 📱📺 (optional) | Handy, Tablet, Fire TV | `cd host-agent/android && ./build.sh` → APK, oder per Satellit via ADB ausrollen (Details unten) |
|
| Android-Agent 📱📺 (optional) | Handy, Tablet, Fire TV | `cd host-agent/android && ./build.sh` → APK, oder per Satellit via ADB bzw. per MDM ausrollen (Details unten) |
|
||||||
| Android App | Stefans Handy | APK installieren (Auto-Update via RVS) |
|
| Android App | Stefans Handy | APK installieren (Auto-Update via RVS) |
|
||||||
|
|
||||||
> Compute-Nodes sind optional: ohne sie faellt STT auf lokales Whisper (CPU,
|
> Compute-Nodes sind optional: ohne sie faellt STT auf lokales Whisper (CPU,
|
||||||
@@ -1232,6 +1232,25 @@ streamt das Mikrofon mit demselben Protokoll wie die App
|
|||||||
> dieselbe Antwort gleichzeitig vorlesen. Jedes Geraet gibt darum nur aus, was
|
> dieselbe Antwort gleichzeitig vorlesen. Jedes Geraet gibt darum nur aus, was
|
||||||
> aus *seiner* eigenen Frage entstanden ist (`audioRequestId` → `messageId`).
|
> aus *seiner* eigenen Frage entstanden ist (`audioRequestId` → `messageId`).
|
||||||
|
|
||||||
|
**APK installieren** (`host_install_apk`) — ARIA kann eine APK, die unter
|
||||||
|
`/shared/` liegt, auf das Geraet schicken und den Installationsdialog oeffnen:
|
||||||
|
|
||||||
|
1. Der Agent holt die Datei **stueckweise** (4 MB) per `file_request` mit
|
||||||
|
`offset`/`length` bei der Bridge ab. Der RVS schickt jede Antwort gezielt nur
|
||||||
|
an den Agenten — nicht an alle Geraete im Raum —, und der Fire TV muss nie
|
||||||
|
die ganze Datei auf einmal im Speicher halten.
|
||||||
|
2. Die Pruefsumme (sha256, vom Brain mitgegeben) wird verglichen; eine
|
||||||
|
beschaedigte Datei wird verworfen.
|
||||||
|
3. Android zeigt den Installationsdialog — direkt, wenn der Agent „Ueber
|
||||||
|
anderen Apps anzeigen" darf, sonst ueber eine Benachrichtigung „Tippen zum
|
||||||
|
Installieren".
|
||||||
|
|
||||||
|
> **Still installieren geht nicht.** Eine normale App darf das unter Android
|
||||||
|
> nicht; am Geraet muss jemand „Installieren" bestaetigen. Beim ersten Mal muss
|
||||||
|
> dort ausserdem **„Unbekannte Apps installieren"** fuer den ARIA Host-Agent
|
||||||
|
> erlaubt werden — fehlt das, sagt der Agent es ARIA, bevor er etwas laedt.
|
||||||
|
> Voraussetzung ist ausserdem „Steuerung erlauben" in der Agent-App.
|
||||||
|
|
||||||
### Bauen + ausrollen
|
### Bauen + ausrollen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1267,6 +1286,65 @@ vorgeschlagen; nur wenn es keinen gibt, kommt der automatisch generierte.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Ausrollen per MDM 📲 — Cockpit-App und Android-Agent
|
||||||
|
|
||||||
|
Beide Android-Apps lassen sich ueber ein MDM verteilen. Die Zugangsdaten gibt
|
||||||
|
das MDM als **App-Konfiguration** (Android „Managed Configuration") mit — am
|
||||||
|
Geraet muss niemand einen QR-Code scannen oder etwas eintippen.
|
||||||
|
|
||||||
|
Im MDM pro App den **Paketnamen** eintragen und darunter die Werte als
|
||||||
|
Schluessel / Typ / Wert:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Der Typ wird pro Zeile aus *Text*, *Zahl* oder *Ja/Nein* gewaehlt:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Cockpit-App — Paketname `com.ariacockpit`
|
||||||
|
|
||||||
|
| Schluessel | Typ | Wert | Pflicht |
|
||||||
|
|------------|-----|------|---------|
|
||||||
|
| `host` | Text | RVS-Hostname, z.B. `aria-rvs.example.de` (= `RVS_HOST`) | ja |
|
||||||
|
| `token` | Text | Pairing-Token (= `RVS_TOKEN`, derselbe wie im QR-Code) | ja |
|
||||||
|
| `port` | Zahl | `443` (= `RVS_PORT`) | nein, Standard 443 |
|
||||||
|
| `tls` | Ja/Nein | Ja (= `RVS_TLS`) | nein, Standard Ja |
|
||||||
|
|
||||||
|
### Android-Agent — Paketname `de.hackersoft.ariaagent`
|
||||||
|
|
||||||
|
| Schluessel | Typ | Wert | Pflicht |
|
||||||
|
|------------|-----|------|---------|
|
||||||
|
| `host` | Text | RVS-Hostname (= `RVS_HOST`) | ja |
|
||||||
|
| `token` | Text | Pairing-Token (= `RVS_TOKEN`) | ja |
|
||||||
|
| `port` | Zahl | `443` | nein, Standard 443 |
|
||||||
|
| `tls` | Ja/Nein | Ja | nein, Standard Ja |
|
||||||
|
| `name` | Text | Geraetename, so erscheint er in der Diagnostic | nein, sonst der Modellname |
|
||||||
|
| `control` | Ja/Nein | Ja = ARIA darf das Geraet steuern (tippen, wischen, Apps starten) | nein, Standard Nein |
|
||||||
|
| `reply_mode` | Text | `overlay` (Einblendung) oder `speech` (Sprache) | nein, Standard `overlay` |
|
||||||
|
|
||||||
|
### Was dabei passiert
|
||||||
|
|
||||||
|
- **Nur mit `host` UND `token`** uebernimmt die App die Werte. Fehlt einer der
|
||||||
|
beiden, bleibt alles wie bisher (QR-Code bzw. Eingabe am Geraet).
|
||||||
|
- **Das MDM hat Vorrang.** Eine Aenderung im MDM (z.B. neuer Token) ueberschreibt
|
||||||
|
die Werte am Geraet. Beim Agenten bleiben nicht gesetzte Schluessel auf ihrem
|
||||||
|
bisherigen Wert.
|
||||||
|
- **Wann gelesen wird:** Cockpit beim App-Start, der Agent beim App-Start und
|
||||||
|
nach einem Neustart des Geraets. Eine Aenderung im MDM greift also erst danach.
|
||||||
|
- **Was das MDM nicht erledigen kann:** Beim Agenten muessen die
|
||||||
|
Bedienungshilfe (fuer `control`), die Einblendung ueber anderen Apps und die
|
||||||
|
Bildschirm-Aufnahme weiterhin einmal am Geraet erlaubt werden — Android laesst
|
||||||
|
diese Spezialberechtigungen nicht per App-Konfiguration zu. Cockpit fragt
|
||||||
|
Mikrofon, Benachrichtigungen usw. wie gewohnt beim ersten Start.
|
||||||
|
- Der **Token ist ein Zugangsschluessel**: Wer ihn kennt, landet im selben
|
||||||
|
RVS-Room. Im MDM nur fuer Admins sichtbar ablegen.
|
||||||
|
|
||||||
|
Welche Schluessel es gibt, steht in `res/xml/app_restrictions.xml` der
|
||||||
|
jeweiligen App (`android/android/app/...` bzw. `host-agent/android/app/...`);
|
||||||
|
ein MDM, das die Liste selbst ausliest, zeigt sie dort auch mit Beschreibung an.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Docker Volumes
|
## Docker Volumes
|
||||||
|
|
||||||
| Volume / Bind | Pfad im Container | Zweck |
|
| Volume / Bind | Pfad im Container | Zweck |
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
<!-- MDM-Ausrollen: welche Werte ein MDM mitgeben kann -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.APP_RESTRICTIONS"
|
||||||
|
android:resource="@xml/app_restrictions" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
add(BackgroundAudioPackage())
|
add(BackgroundAudioPackage())
|
||||||
add(ShareIntentPackage())
|
add(ShareIntentPackage())
|
||||||
add(TlsTrustPackage())
|
add(TlsTrustPackage())
|
||||||
|
add(MdmConfigPackage())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
override fun getJSMainModuleName(): String = "index"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.RestrictionsManager
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.bridge.Arguments
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbindungsdaten aus dem MDM ("Managed Configuration" / App-Restrictions).
|
||||||
|
*
|
||||||
|
* Das MDM schreibt Schluessel/Werte fuer com.ariacockpit; welche es gibt,
|
||||||
|
* steht in res/xml/app_restrictions.xml. Die App liest sie beim Start —
|
||||||
|
* so muss beim Ausrollen niemand einen QR-Code scannen.
|
||||||
|
*
|
||||||
|
* Gibt nur die Schluessel zurueck, die das MDM wirklich gesetzt hat.
|
||||||
|
*/
|
||||||
|
class MdmConfigModule(ctx: ReactApplicationContext) : ReactContextBaseJavaModule(ctx) {
|
||||||
|
override fun getName() = "MdmConfig"
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun getConfig(promise: Promise) {
|
||||||
|
try {
|
||||||
|
val rm = reactApplicationContext.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
|
||||||
|
val b = rm.applicationRestrictions
|
||||||
|
val out = Arguments.createMap()
|
||||||
|
for (key in b.keySet()) {
|
||||||
|
when (val v = b.get(key)) {
|
||||||
|
is String -> out.putString(key, v)
|
||||||
|
is Int -> out.putInt(key, v)
|
||||||
|
is Boolean -> out.putBoolean(key, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise.resolve(out)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.resolve(Arguments.createMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MdmConfigPackage : ReactPackage {
|
||||||
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
||||||
|
listOf(MdmConfigModule(reactContext))
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Texte fuer die MDM-Werte (res/xml/app_restrictions.xml) -->
|
||||||
|
<resources>
|
||||||
|
<string name="mdm_host_title" translatable="false">RVS-Host</string>
|
||||||
|
<string name="mdm_host_description" translatable="false">Hostname des RVS, z.B. aria-rvs.example.de</string>
|
||||||
|
<string name="mdm_port_title" translatable="false">RVS-Port</string>
|
||||||
|
<string name="mdm_tls_title" translatable="false">TLS verwenden</string>
|
||||||
|
<string name="mdm_token_title" translatable="false">Token</string>
|
||||||
|
<string name="mdm_token_description" translatable="false">Derselbe Token wie im QR-Code</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Werte, die ein MDM beim Ausrollen mitgeben kann (Managed Configuration).
|
||||||
|
Sind host UND token gesetzt, verbindet sich die App damit — ohne QR-Code. -->
|
||||||
|
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<restriction
|
||||||
|
android:key="host"
|
||||||
|
android:title="@string/mdm_host_title"
|
||||||
|
android:description="@string/mdm_host_description"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
<restriction
|
||||||
|
android:key="port"
|
||||||
|
android:title="@string/mdm_port_title"
|
||||||
|
android:defaultValue="443"
|
||||||
|
android:restrictionType="integer" />
|
||||||
|
<restriction
|
||||||
|
android:key="tls"
|
||||||
|
android:title="@string/mdm_tls_title"
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:restrictionType="bool" />
|
||||||
|
<restriction
|
||||||
|
android:key="token"
|
||||||
|
android:title="@string/mdm_token_title"
|
||||||
|
android:description="@string/mdm_token_description"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
</restrictions>
|
||||||
@@ -491,7 +491,40 @@ class RVSConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbindungsdaten aus dem MDM (Managed Configuration), falls beim Ausrollen
|
||||||
|
* gesetzt. Nur gueltig mit host UND token. Schluessel: res/xml/app_restrictions.xml
|
||||||
|
*/
|
||||||
|
async loadMdmConfig(): Promise<ConnectionConfig | null> {
|
||||||
|
try {
|
||||||
|
const Mdm: any = NativeModules.MdmConfig;
|
||||||
|
if (Platform.OS !== 'android' || !Mdm?.getConfig) return null;
|
||||||
|
const m = await Mdm.getConfig();
|
||||||
|
const host = String(m?.host ?? '').trim();
|
||||||
|
const token = String(m?.token ?? '').trim();
|
||||||
|
if (!host || !token) return null;
|
||||||
|
const port = Number(m.port);
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
token,
|
||||||
|
port: Number.isFinite(port) && port > 0 ? port : 443,
|
||||||
|
useTLS: m.tls !== false,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadConfig(): Promise<ConnectionConfig | null> {
|
async loadConfig(): Promise<ConnectionConfig | null> {
|
||||||
|
// Vom MDM gesetzte Werte haben Vorrang: so kommen geaenderte Zugangsdaten
|
||||||
|
// beim naechsten App-Start an, ohne dass jemand am Geraet etwas tun muss.
|
||||||
|
const mdm = await this.loadMdmConfig();
|
||||||
|
if (mdm) {
|
||||||
|
console.log('[RVS] Konfiguration aus dem MDM');
|
||||||
|
this.config = mdm;
|
||||||
|
await this.saveConfig(mdm);
|
||||||
|
return mdm;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY);
|
const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|||||||
+54
-1
@@ -1838,6 +1838,31 @@ META_TOOLS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "host_install_apk",
|
||||||
|
"description": (
|
||||||
|
"Installiert eine APK auf einem Android-Agenten (Handy, Tablet, "
|
||||||
|
"Fire TV). Die APK muss unter /shared/ liegen; der Agent holt sie "
|
||||||
|
"ueber den RVS und oeffnet den Installationsdialog von Android. "
|
||||||
|
"STILL installieren geht nicht: am Geraet muss jemand 'Installieren' "
|
||||||
|
"bestaetigen (Android-Regel). Beim ersten Mal muss dort ausserdem "
|
||||||
|
"'Unbekannte Apps installieren' fuer den ARIA Host-Agent erlaubt "
|
||||||
|
"werden. Ob die Installation durch ist, pruefst Du danach z.B. mit "
|
||||||
|
"host_app_launch oder einem Screenshot. Braucht 'Steuerung erlauben' "
|
||||||
|
"am Geraet."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {"type": "string", "description": "Host-ID/Name (siehe host_list)."},
|
||||||
|
"path": {"type": "string", "description": "Pfad der APK unter /shared/, z.B. /shared/uploads/app.apk."},
|
||||||
|
},
|
||||||
|
"required": ["host", "path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -3843,6 +3868,33 @@ class Agent:
|
|||||||
+ json.dumps(nodes, ensure_ascii=False, indent=2)
|
+ json.dumps(nodes, ensure_ascii=False, indent=2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if name == "host_install_apk":
|
||||||
|
apk = os.path.realpath((arguments.get("path") or "").strip())
|
||||||
|
if not apk.startswith("/shared/") or not apk.lower().endswith(".apk"):
|
||||||
|
return "FEHLER: path muss eine .apk unter /shared/ sein."
|
||||||
|
if not os.path.isfile(apk):
|
||||||
|
return f"FEHLER: APK nicht gefunden: {apk}"
|
||||||
|
import hashlib as _hl
|
||||||
|
size = os.path.getsize(apk)
|
||||||
|
h = _hl.sha256()
|
||||||
|
with open(apk, "rb") as _f:
|
||||||
|
for _blk in iter(lambda: _f.read(1024 * 1024), b""):
|
||||||
|
h.update(_blk)
|
||||||
|
# Der Agent holt die Datei stueckweise — Zeit nach Groesse
|
||||||
|
# bemessen (grob 3 s pro MB, mindestens 2 Minuten).
|
||||||
|
_timeout = min(1800, 120 + 3 * (size // (1024 * 1024)))
|
||||||
|
result = _post("/internal/host",
|
||||||
|
{"host": host, "action": "apk_install",
|
||||||
|
"params": {"path": apk, "size": size,
|
||||||
|
"sha256": h.hexdigest(),
|
||||||
|
"name": os.path.basename(apk)},
|
||||||
|
"timeout": _timeout},
|
||||||
|
_timeout + 10)
|
||||||
|
if not result.get("ok"):
|
||||||
|
return f"FEHLER: {result.get('error')}"
|
||||||
|
r = result.get("result") or {}
|
||||||
|
return f"OK ({host}): {r.get('message', 'Installation angestossen')}"
|
||||||
|
|
||||||
# ── Steuern (Meilenstein 3) ──────────────────────────────
|
# ── Steuern (Meilenstein 3) ──────────────────────────────
|
||||||
_UI_ACTIONS = {
|
_UI_ACTIONS = {
|
||||||
"host_ui_tap": ("ui_tap", ["x", "y", "button", "double"]),
|
"host_ui_tap": ("ui_tap", ["x", "y", "button", "double"]),
|
||||||
@@ -4860,7 +4912,8 @@ class Agent:
|
|||||||
"host_info", "host_screenshot", "host_ui_dump",
|
"host_info", "host_screenshot", "host_ui_dump",
|
||||||
"host_ui_tap", "host_ui_text", "host_ui_swipe",
|
"host_ui_tap", "host_ui_text", "host_ui_swipe",
|
||||||
"host_ui_key", "host_app_launch",
|
"host_ui_key", "host_app_launch",
|
||||||
"host_notify", "host_speak", "host_volume"):
|
"host_notify", "host_speak", "host_volume",
|
||||||
|
"host_install_apk"):
|
||||||
return self._dispatch_host(name, arguments)
|
return self._dispatch_host(name, arguments)
|
||||||
if name == "vm_register":
|
if name == "vm_register":
|
||||||
pid = (project_id or "").strip()
|
pid = (project_id or "").strip()
|
||||||
|
|||||||
@@ -3315,6 +3315,43 @@ class ARIABridge:
|
|||||||
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
# Stueckweiser Abruf (z.B. Android-Agent holt eine APK): mit
|
||||||
|
# "offset" kommt nur ein Teil zurueck. So muss niemand eine grosse
|
||||||
|
# Datei am Stueck in den Speicher nehmen (Fire TV hat wenig RAM).
|
||||||
|
if "offset" in payload:
|
||||||
|
CHUNK_MAX = 8 * 1024 * 1024
|
||||||
|
try:
|
||||||
|
offset = max(0, int(payload.get("offset") or 0))
|
||||||
|
length = int(payload.get("length") or CHUNK_MAX)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
offset, length = 0, CHUNK_MAX
|
||||||
|
length = max(1, min(length, CHUNK_MAX))
|
||||||
|
try:
|
||||||
|
total = os.path.getsize(server_path)
|
||||||
|
with open(server_path, "rb") as f:
|
||||||
|
f.seek(offset)
|
||||||
|
chunk = f.read(length)
|
||||||
|
except OSError as exc:
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_response",
|
||||||
|
"payload": {"requestId": req_id, "error": f"Lesen fehlgeschlagen: {exc}"},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
await self._send_to_rvs({
|
||||||
|
"type": "file_response",
|
||||||
|
"payload": {
|
||||||
|
"requestId": req_id,
|
||||||
|
"serverPath": server_path,
|
||||||
|
"name": os.path.basename(server_path),
|
||||||
|
"offset": offset,
|
||||||
|
"sizeBytes": total,
|
||||||
|
"eof": offset + len(chunk) >= total,
|
||||||
|
"base64": base64.b64encode(chunk).decode("ascii"),
|
||||||
|
},
|
||||||
|
"timestamp": int(asyncio.get_event_loop().time() * 1000),
|
||||||
|
})
|
||||||
|
return
|
||||||
# Groessen-Check VOR base64-Encode + Send. Sonst zerreisst's bei
|
# Groessen-Check VOR base64-Encode + Send. Sonst zerreisst's bei
|
||||||
# grossen Files (>~70 MB binaer) die WebSocket-Verbindung mit
|
# grossen Files (>~70 MB binaer) die WebSocket-Verbindung mit
|
||||||
# Code 1009 (message too big) — RVS-Server droppt, Bridge crasht
|
# Code 1009 (message too big) — RVS-Server droppt, Bridge crasht
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId 'de.hackersoft.ariaagent'
|
applicationId 'de.hackersoft.ariaagent'
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
||||||
versionCode 108
|
versionCode 200
|
||||||
versionName '0.0.1.8'
|
versionName '0.0.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AppCompat.DayNight">
|
android:theme="@style/Theme.AppCompat.DayNight">
|
||||||
|
|
||||||
|
<!-- MDM-Ausrollen: welche Werte ein MDM mitgeben kann -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.APP_RESTRICTIONS"
|
||||||
|
android:resource="@xml/app_restrictions" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package de.hackersoft.ariaagent
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK von ARIA empfangen und installieren (host_command "apk_install").
|
||||||
|
*
|
||||||
|
* 1. Die APK liegt bei ARIA unter /shared/. Wir holen sie STUECKWEISE per
|
||||||
|
* file_request (offset/length) — der RVS schickt die Antwort gezielt nur
|
||||||
|
* an uns, nicht an alle Geraete im Raum, und der Fire TV muss nie die
|
||||||
|
* ganze Datei auf einmal im Speicher halten.
|
||||||
|
* 2. Pruefsumme (sha256) vergleichen.
|
||||||
|
* 3. Installationsdialog von Android oeffnen. Still installieren darf eine
|
||||||
|
* normale App nicht — am Geraet muss jemand bestaetigen.
|
||||||
|
*/
|
||||||
|
object ApkReceiver {
|
||||||
|
private const val DIR = "incoming"
|
||||||
|
private const val CHUNK = 4 * 1024 * 1024
|
||||||
|
private const val CH = "aria_agent_install"
|
||||||
|
private const val NOTIF_ID = 4712
|
||||||
|
|
||||||
|
/** Holt ein Stueck: (path, offset, length) -> file_response-Payload oder null (Timeout). */
|
||||||
|
fun interface ChunkFetcher {
|
||||||
|
fun fetch(path: String, offset: Long, length: Int): JSONObject?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun run(ctx: Context, params: JSONObject, fetcher: ChunkFetcher): JSONObject {
|
||||||
|
val path = params.optString("path")
|
||||||
|
val size = params.optLong("size", -1)
|
||||||
|
val sha = params.optString("sha256").lowercase()
|
||||||
|
val name = params.optString("name").ifBlank { File(path).name }
|
||||||
|
if (!path.startsWith("/shared/") || !path.lowercase().endsWith(".apk"))
|
||||||
|
return err("path muss eine .apk unter /shared/ sein")
|
||||||
|
if (size <= 0) return err("Groesse fehlt")
|
||||||
|
|
||||||
|
// Ohne diese Erlaubnis oeffnet Android den Dialog gar nicht — lieber
|
||||||
|
// VOR dem Download sagen, statt Megabytes umsonst zu holen.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !ctx.packageManager.canRequestPackageInstalls()) {
|
||||||
|
openUnknownSourcesSettings(ctx)
|
||||||
|
return err("Auf dem Geraet ist 'Unbekannte Apps installieren' fuer den ARIA Host-Agent " +
|
||||||
|
"nicht erlaubt. Die Einstellung ist jetzt dort geoeffnet (falls die App im " +
|
||||||
|
"Vordergrund ist) — einmal erlauben, dann nochmal versuchen.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dir = File(ctx.cacheDir, DIR).apply { mkdirs() }
|
||||||
|
dir.listFiles()?.forEach { it.delete() } // Reste frueherer Uebertragungen
|
||||||
|
if (dir.usableSpace < size + 20L * 1024 * 1024)
|
||||||
|
return err("Zu wenig Speicher auf dem Geraet (${size / 1048576} MB noetig, " +
|
||||||
|
"${dir.usableSpace / 1048576} MB frei)")
|
||||||
|
|
||||||
|
val safe = name.replace(Regex("[^A-Za-z0-9._-]+"), "_")
|
||||||
|
val file = File(dir, safe)
|
||||||
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
|
file.outputStream().use { out ->
|
||||||
|
var offset = 0L
|
||||||
|
while (offset < size) {
|
||||||
|
var chunk: JSONObject? = null
|
||||||
|
for (attempt in 1..3) {
|
||||||
|
chunk = fetcher.fetch(path, offset, CHUNK)
|
||||||
|
if (chunk != null) break
|
||||||
|
}
|
||||||
|
if (chunk == null) return err("Keine Antwort beim Abholen (bei ${offset / 1048576} MB)")
|
||||||
|
if (chunk.has("error")) return err("Abholen fehlgeschlagen: ${chunk.optString("error")}")
|
||||||
|
if (!chunk.has("offset"))
|
||||||
|
return err("Die Bridge kennt den stueckweisen Abruf noch nicht — Bridge neu deployen.")
|
||||||
|
val bytes = android.util.Base64.decode(chunk.optString("base64"), android.util.Base64.DEFAULT)
|
||||||
|
if (bytes.isEmpty()) return err("Leeres Stueck bei ${offset / 1048576} MB")
|
||||||
|
out.write(bytes)
|
||||||
|
md.update(bytes)
|
||||||
|
offset += bytes.size
|
||||||
|
if (chunk.optBoolean("eof")) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file.length() != size) {
|
||||||
|
file.delete()
|
||||||
|
return err("Unvollstaendig: ${file.length()} von $size Bytes")
|
||||||
|
}
|
||||||
|
val got = md.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
if (sha.isNotEmpty() && got != sha) {
|
||||||
|
file.delete()
|
||||||
|
return err("Pruefsumme stimmt nicht — Datei beschaedigt, nicht installiert")
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = try { ctx.packageManager.getPackageArchiveInfo(file.path, 0) } catch (_: Exception) { null }
|
||||||
|
?: run { file.delete(); return err("Die Datei ist keine gueltige APK") }
|
||||||
|
val label = "${info.packageName} ${info.versionName ?: ""}".trim()
|
||||||
|
|
||||||
|
val shown = openInstaller(ctx, file, label)
|
||||||
|
val msg = if (shown)
|
||||||
|
"APK $name ($label) uebertragen — der Installationsdialog ist auf dem Geraet offen " +
|
||||||
|
"und muss dort bestaetigt werden."
|
||||||
|
else
|
||||||
|
"APK $name ($label) uebertragen — auf dem Geraet liegt eine Benachrichtigung " +
|
||||||
|
"'Tippen zum Installieren'. Jemand muss sie antippen und bestaetigen."
|
||||||
|
return JSONObject().put("ok", true).put("result", JSONObject()
|
||||||
|
.put("message", msg).put("package", info.packageName)
|
||||||
|
.put("version", info.versionName ?: "").put("bytes", size))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dialog direkt oeffnen, wenn Android das aus dem Hintergrund erlaubt; immer
|
||||||
|
* auch eine Benachrichtigung als Rueckfallweg. true = Dialog direkt geoeffnet. */
|
||||||
|
private fun openInstaller(ctx: Context, file: File, label: String): Boolean {
|
||||||
|
val uri = FileProvider.getUriForFile(ctx, "${ctx.packageName}.fileprovider", file)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
nm.createNotificationChannel(NotificationChannel(CH, "Installationen von ARIA",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH))
|
||||||
|
}
|
||||||
|
val pi = PendingIntent.getActivity(ctx, 2, intent,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
nm.notify(NOTIF_ID, NotificationCompat.Builder(ctx, CH)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setContentTitle("ARIA moechte eine App installieren")
|
||||||
|
.setContentText("$label — tippen zum Installieren")
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
// Aus dem Hintergrund duerfen Apps nur mit "Ueber anderen Apps anzeigen"
|
||||||
|
// eine Activity starten — die hat der Agent meist (Einblendungen).
|
||||||
|
if (!OverlayView.canDraw(ctx)) return false
|
||||||
|
return try { ctx.startActivity(intent); true } catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openUnknownSourcesSettings(ctx: Context) {
|
||||||
|
try {
|
||||||
|
ctx.startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
|
Uri.parse("package:${ctx.packageName}")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun err(m: String) = JSONObject().put("ok", false).put("error", m)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
|||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(ctx: Context, intent: Intent) {
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
|
MdmConfig.apply(ctx) // vom MDM geaenderte Werte uebernehmen
|
||||||
if (AgentConfig.load(ctx).isValid()) {
|
if (AgentConfig.load(ctx).isValid()) {
|
||||||
AgentService.start(ctx)
|
AgentService.start(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// Fern-Einrichtung (z.B. Fire TV, wo QR-Scan und Tippen wegfallen): der
|
// Fern-Einrichtung (z.B. Fire TV, wo QR-Scan und Tippen wegfallen): der
|
||||||
// Satellit startet die App per ADB mit den Zugangsdaten als Extras.
|
// Satellit startet die App per ADB mit den Zugangsdaten als Extras.
|
||||||
// Hat das geklappt, ist die UI schon befuellt — sonst normal laden.
|
// Hat das geklappt, ist die UI schon befuellt — sonst normal laden.
|
||||||
if (!applyProvisioning(intent)) loadIntoUi(AgentConfig.load(this))
|
if (!applyProvisioning(intent)) {
|
||||||
|
// Per MDM ausgerollt? Dann Werte uebernehmen und gleich verbinden.
|
||||||
|
val mdm = MdmConfig.apply(this)
|
||||||
|
if (mdm != null) {
|
||||||
|
AgentService.start(this)
|
||||||
|
toast("Vom MDM eingerichtet: ${mdm.host} — Agent gestartet")
|
||||||
|
}
|
||||||
|
loadIntoUi(mdm ?: AgentConfig.load(this))
|
||||||
|
}
|
||||||
|
|
||||||
// Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
|
// Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
|
||||||
// und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
|
// und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.hackersoft.ariaagent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.RestrictionsManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einrichtung per MDM ("Managed Configuration" / App-Restrictions).
|
||||||
|
*
|
||||||
|
* Das MDM schreibt Schluessel/Werte fuer de.hackersoft.ariaagent; welche es
|
||||||
|
* gibt, steht in res/xml/app_restrictions.xml. Sind host UND token gesetzt,
|
||||||
|
* uebernimmt der Agent sie — vom MDM gesetzte Werte haben Vorrang vor der
|
||||||
|
* Eingabe am Geraet, nicht gesetzte Schluessel lassen den alten Wert stehen.
|
||||||
|
*
|
||||||
|
* Gelesen wird beim App-Start und nach dem Booten.
|
||||||
|
*/
|
||||||
|
object MdmConfig {
|
||||||
|
|
||||||
|
/** Gibt die neue Konfiguration zurueck, wenn das MDM etwas geaendert hat, sonst null. */
|
||||||
|
fun apply(ctx: Context): AgentConfig? {
|
||||||
|
val b = try {
|
||||||
|
(ctx.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager)
|
||||||
|
.applicationRestrictions
|
||||||
|
} catch (_: Exception) { return null }
|
||||||
|
|
||||||
|
val host = b.getString("host")?.trim().orEmpty()
|
||||||
|
val token = b.getString("token")?.trim().orEmpty()
|
||||||
|
if (host.isEmpty() || token.isEmpty()) return null
|
||||||
|
|
||||||
|
val old = AgentConfig.load(ctx)
|
||||||
|
val cfg = old.copy(
|
||||||
|
host = host,
|
||||||
|
token = token,
|
||||||
|
port = if (b.containsKey("port")) b.getInt("port").takeIf { it > 0 } ?: 443 else old.port,
|
||||||
|
tls = if (b.containsKey("tls")) b.getBoolean("tls") else old.tls,
|
||||||
|
name = b.getString("name")?.trim().takeUnless { it.isNullOrEmpty() } ?: old.name,
|
||||||
|
controlEnabled = if (b.containsKey("control")) b.getBoolean("control") else old.controlEnabled,
|
||||||
|
replyMode = b.getString("reply_mode")?.trim()
|
||||||
|
?.takeIf { it == "overlay" || it == "speech" } ?: old.replyMode,
|
||||||
|
)
|
||||||
|
if (cfg == old) return null
|
||||||
|
cfg.save(ctx)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import okhttp3.WebSocket
|
|||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +59,9 @@ class RvsClient(
|
|||||||
// Ausgabe auf dem Geraet: Einblendung ueber dem Bild + Sprachausgabe.
|
// Ausgabe auf dem Geraet: Einblendung ueber dem Bild + Sprachausgabe.
|
||||||
"notify", "speak", "volume",
|
"notify", "speak", "volume",
|
||||||
// Sprachdialog: Mikro oeffnen/schliessen (ARIA kann es auch anstossen).
|
// Sprachdialog: Mikro oeffnen/schliessen (ARIA kann es auch anstossen).
|
||||||
"listen", "listen_stop")
|
"listen", "listen_stop",
|
||||||
|
// APK von ARIA empfangen + Installationsdialog oeffnen.
|
||||||
|
"apk_install")
|
||||||
if (isTv) base += "tv" // Kennzeichen: dieses Geraet ist ein Fernseher/Stick
|
if (isTv) base += "tv" // Kennzeichen: dieses Geraet ist ein Fernseher/Stick
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
@@ -189,6 +193,13 @@ class RvsClient(
|
|||||||
AgentUpdater.onAvailable(appCtx, config, p.optString("version"), p.optLong("size"))
|
AgentUpdater.onAvailable(appCtx, config, p.optString("version"), p.optLong("size"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Antwort auf einen eigenen file_request (APK-Abruf)? Dann an den
|
||||||
|
// wartenden Thread weiterreichen — sonst niemanden interessiert.
|
||||||
|
if (type == "file_response") {
|
||||||
|
val p = msg.optJSONObject("payload") ?: return
|
||||||
|
fileWaiters[p.optString("requestId")]?.offer(p)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (type != "host_command") {
|
if (type != "host_command") {
|
||||||
if (type in VOICE_TYPES) {
|
if (type in VOICE_TYPES) {
|
||||||
try { voice.handle(type, msg.optJSONObject("payload") ?: JSONObject()) }
|
try { voice.handle(type, msg.optJSONObject("payload") ?: JSONObject()) }
|
||||||
@@ -230,6 +241,26 @@ class RvsClient(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APK holen dauert (viele Stuecke): eigener Thread, denn die Antworten
|
||||||
|
// kommen ueber genau diesen Lese-Thread herein.
|
||||||
|
if (action == "apk_install") {
|
||||||
|
if (!config.controlEnabled) {
|
||||||
|
reply(err("Steuerung ist in der Agent-App deaktiviert (Schalter 'Steuerung erlauben')."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Thread {
|
||||||
|
val r = try {
|
||||||
|
ApkReceiver.run(appCtx, params) { path, offset, length ->
|
||||||
|
fetchChunk(webSocket, path, offset, length)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
err("Fehler bei '$action': ${t.javaClass.simpleName}: ${t.message}")
|
||||||
|
}
|
||||||
|
reply(r)
|
||||||
|
}.also { it.isDaemon = true }.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// WICHTIG: JEDE Aktion muss ein host_result liefern — auch bei Exception
|
// WICHTIG: JEDE Aktion muss ein host_result liefern — auch bei Exception
|
||||||
// ODER OutOfMemoryError (Throwable!). Sonst bekommt ARIA statt einer
|
// ODER OutOfMemoryError (Throwable!). Sonst bekommt ARIA statt einer
|
||||||
// Fehlermeldung nur einen Timeout (kein Result kommt zurueck).
|
// Fehlermeldung nur einen Timeout (kein Result kommt zurueck).
|
||||||
@@ -265,6 +296,25 @@ class RvsClient(
|
|||||||
reply(result)
|
reply(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wartende file_request-Abrufe: requestId -> Briefkasten fuer die Antwort. */
|
||||||
|
private val fileWaiters = ConcurrentHashMap<String, LinkedBlockingQueue<JSONObject>>()
|
||||||
|
|
||||||
|
/** Ein Stueck einer Datei unter /shared/ bei der Bridge abholen (null = Timeout). */
|
||||||
|
private fun fetchChunk(webSocket: WebSocket, path: String, offset: Long, length: Int): JSONObject? {
|
||||||
|
val rid = "apk-" + java.util.UUID.randomUUID().toString()
|
||||||
|
val box = LinkedBlockingQueue<JSONObject>(1)
|
||||||
|
fileWaiters[rid] = box
|
||||||
|
try {
|
||||||
|
val sent = send(webSocket, "file_request", JSONObject()
|
||||||
|
.put("requestId", rid).put("serverPath", path)
|
||||||
|
.put("offset", offset).put("length", length))
|
||||||
|
if (!sent) return null
|
||||||
|
return box.poll(60, TimeUnit.SECONDS)
|
||||||
|
} finally {
|
||||||
|
fileWaiters.remove(rid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Steuer-Aktionen, die ein entsperrtes Gerät brauchen. */
|
/** Steuer-Aktionen, die ein entsperrtes Gerät brauchen. */
|
||||||
private val CONTROL_ACTIONS = setOf("ui_tap", "ui_text", "ui_swipe", "ui_key", "app_launch")
|
private val CONTROL_ACTIONS = setOf("ui_tap", "ui_text", "ui_swipe", "ui_key", "app_launch")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Texte fuer die MDM-Werte (res/xml/app_restrictions.xml) -->
|
||||||
|
<resources>
|
||||||
|
<string name="mdm_host_title" translatable="false">RVS-Host</string>
|
||||||
|
<string name="mdm_host_description" translatable="false">Hostname des RVS, z.B. aria-rvs.example.de</string>
|
||||||
|
<string name="mdm_port_title" translatable="false">RVS-Port</string>
|
||||||
|
<string name="mdm_tls_title" translatable="false">TLS verwenden</string>
|
||||||
|
<string name="mdm_token_title" translatable="false">Token</string>
|
||||||
|
<string name="mdm_token_description" translatable="false">Derselbe Token wie im QR-Code</string>
|
||||||
|
<string name="mdm_name_title" translatable="false">Geraetename</string>
|
||||||
|
<string name="mdm_name_description" translatable="false">So erscheint das Geraet in der Diagnostic (leer = Modellname)</string>
|
||||||
|
<string name="mdm_control_title" translatable="false">Steuern erlauben</string>
|
||||||
|
<string name="mdm_reply_mode_title" translatable="false">Antwort als</string>
|
||||||
|
<string name="mdm_reply_mode_description" translatable="false">overlay (Einblendung) oder speech (Sprache)</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Werte, die ein MDM beim Ausrollen mitgeben kann (Managed Configuration).
|
||||||
|
Sind host UND token gesetzt, verbindet sich der Agent damit — ohne QR-Code. -->
|
||||||
|
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<restriction
|
||||||
|
android:key="host"
|
||||||
|
android:title="@string/mdm_host_title"
|
||||||
|
android:description="@string/mdm_host_description"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
<restriction
|
||||||
|
android:key="port"
|
||||||
|
android:title="@string/mdm_port_title"
|
||||||
|
android:defaultValue="443"
|
||||||
|
android:restrictionType="integer" />
|
||||||
|
<restriction
|
||||||
|
android:key="tls"
|
||||||
|
android:title="@string/mdm_tls_title"
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:restrictionType="bool" />
|
||||||
|
<restriction
|
||||||
|
android:key="token"
|
||||||
|
android:title="@string/mdm_token_title"
|
||||||
|
android:description="@string/mdm_token_description"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
<restriction
|
||||||
|
android:key="name"
|
||||||
|
android:title="@string/mdm_name_title"
|
||||||
|
android:description="@string/mdm_name_description"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
<restriction
|
||||||
|
android:key="control"
|
||||||
|
android:title="@string/mdm_control_title"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:restrictionType="bool" />
|
||||||
|
<restriction
|
||||||
|
android:key="reply_mode"
|
||||||
|
android:title="@string/mdm_reply_mode_title"
|
||||||
|
android:description="@string/mdm_reply_mode_description"
|
||||||
|
android:defaultValue="overlay"
|
||||||
|
android:restrictionType="string" />
|
||||||
|
</restrictions>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Auto-Update: nur der Update-Ordner im Cache wird an den Installer freigegeben -->
|
<!-- Auto-Update: nur Update- und Eingangsordner im Cache werden an den Installer freigegeben -->
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path name="updates" path="updates/" />
|
<cache-path name="updates" path="updates/" />
|
||||||
|
<!-- Von ARIA geschickte APKs (host_command apk_install) -->
|
||||||
|
<cache-path name="incoming" path="incoming/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ OUT_MAX_CHARS_HARD = int(os.environ.get("OUT_MAX_CHARS_HARD", "200000") or "2000
|
|||||||
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
|
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
|
||||||
|
|
||||||
# Version (wird von release_agent.sh beim Release gesetzt).
|
# Version (wird von release_agent.sh beim Release gesetzt).
|
||||||
AGENT_VERSION = "0.0.1.8"
|
AGENT_VERSION = "0.0.2.0"
|
||||||
|
|
||||||
HEARTBEAT_SEC = 25
|
HEARTBEAT_SEC = 25
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user