Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ae18b658 | ||
|
|
27f9399eda | ||
|
|
c131820be0 | ||
|
|
e43b44725b | ||
|
|
586a2bf134 |
+68
-1
@@ -1501,6 +1501,69 @@ META_TOOLS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "host_notify",
|
||||||
|
"description": (
|
||||||
|
"Zeigt eine Nachricht AUF einem Host-Agenten an — bei einem Fire TV / "
|
||||||
|
"Android TV als EINBLENDUNG ueber dem laufenden Bild (Netflix & Co. "
|
||||||
|
"laufen weiter), wahlweise als Sprachausgabe. NIMM DIESES TOOL, wenn "
|
||||||
|
"Stefan etwas 'auf dem Fernseher einblenden/anzeigen' will — versuche "
|
||||||
|
"das NICHT ueber host_exec/adb (dort gibt es kein passendes Kommando). "
|
||||||
|
"mode: 'overlay' (einblenden), 'speech' (vorlesen) oder 'both'; ohne "
|
||||||
|
"Angabe gilt die Einstellung am Geraet. Braucht dort einmalig die "
|
||||||
|
"Berechtigung 'Ueber anderen Apps anzeigen'."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {"type": "string", "description": "Host-ID/Name (z.B. 'Fire-TV Schlafzimmer')."},
|
||||||
|
"text": {"type": "string", "description": "Der anzuzeigende Text."},
|
||||||
|
"title": {"type": "string", "description": "Ueberschrift (Default 'ARIA')."},
|
||||||
|
"seconds": {"type": "integer", "description": "Anzeigedauer (Default 12)."},
|
||||||
|
"mode": {"type": "string", "description": "overlay | speech | both"},
|
||||||
|
"focus": {"type": "string", "description": "Bei Sprachausgabe: 'duck' (laufende Medien werden leiser, Standard) | 'pause' (Film/Musik anhalten) | 'none'."},
|
||||||
|
},
|
||||||
|
"required": ["host", "text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "host_speak",
|
||||||
|
"description": (
|
||||||
|
"Laesst einen Host-Agenten Text laut vorlesen (Fire TV: ueber die "
|
||||||
|
"Fernseher-Lautsprecher). Fuer reine Sprachansagen; soll es auch/nur "
|
||||||
|
"sichtbar sein, nimm host_notify."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {"type": "string", "description": "Host-ID/Name."},
|
||||||
|
"text": {"type": "string", "description": "Was gesprochen werden soll."},
|
||||||
|
"focus": {"type": "string", "description": "'duck' (Medien leiser, Standard) | 'pause' (Film/Musik anhalten) | 'none'."},
|
||||||
|
},
|
||||||
|
"required": ["host", "text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "host_volume",
|
||||||
|
"description": "Setzt die Medien-Lautstaerke eines Host-Agenten (Fire TV / Android).",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {"type": "string", "description": "Host-ID/Name."},
|
||||||
|
"percent": {"type": "integer", "description": "0-100."},
|
||||||
|
},
|
||||||
|
"required": ["host", "percent"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -2864,6 +2927,9 @@ class Agent:
|
|||||||
"host_ui_swipe": ("ui_swipe", ["x1", "y1", "x2", "y2", "duration_ms"]),
|
"host_ui_swipe": ("ui_swipe", ["x1", "y1", "x2", "y2", "duration_ms"]),
|
||||||
"host_ui_key": ("ui_key", ["key"]),
|
"host_ui_key": ("ui_key", ["key"]),
|
||||||
"host_app_launch": ("app_launch", ["package", "query", "command"]),
|
"host_app_launch": ("app_launch", ["package", "query", "command"]),
|
||||||
|
"host_notify": ("notify", ["text", "title", "seconds", "mode", "focus"]),
|
||||||
|
"host_speak": ("speak", ["text", "focus"]),
|
||||||
|
"host_volume": ("volume", ["percent"]),
|
||||||
}
|
}
|
||||||
if name in _UI_ACTIONS:
|
if name in _UI_ACTIONS:
|
||||||
action, keys = _UI_ACTIONS[name]
|
action, keys = _UI_ACTIONS[name]
|
||||||
@@ -3674,7 +3740,8 @@ class Agent:
|
|||||||
if name in ("host_list", "host_exec", "host_read", "host_write",
|
if name in ("host_list", "host_exec", "host_read", "host_write",
|
||||||
"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"):
|
||||||
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()
|
||||||
|
|||||||
+24
-5
@@ -4753,11 +4753,20 @@
|
|||||||
document.getElementById('apk-status').textContent = '';
|
document.getElementById('apk-status').textContent = '';
|
||||||
const log = document.getElementById('apk-log');
|
const log = document.getElementById('apk-log');
|
||||||
log.style.display = 'none'; log.textContent = '';
|
log.style.display = 'none'; log.textContent = '';
|
||||||
// Namensfeld vorbelegen — aber nicht mit Seriennummern/IP-Platzhaltern,
|
// Namensfeld vorbelegen. Reihenfolge:
|
||||||
// die die Netz-Erkennung bei Fire TV oft liefert (z.B. 0d94400fa5b63301).
|
// 1. Name, den der Agent auf DIESEM Gerät schon trägt (per IP zugeordnet)
|
||||||
|
// 2. zuletzt hier eingegebener Name (im Browser gemerkt)
|
||||||
|
// 3. der Name aus der Netz-Erkennung — aber nur, wenn er menschenlesbar ist
|
||||||
|
// (Fire TV meldet oft Seriennummern wie F8FCE10B4E6C@AS-AFTKA[AirPlay])
|
||||||
|
const known = (hosts || []).find(h => h.ip && h.ip === ip);
|
||||||
|
const remembered = (function () {
|
||||||
|
try { return localStorage.getItem('apkName:' + satId + ':' + ip) || ''; }
|
||||||
|
catch (e) { return ''; }
|
||||||
|
})();
|
||||||
const looksTechnical = !name || /^[0-9a-f]{8,}$/i.test(name) ||
|
const looksTechnical = !name || /^[0-9a-f]{8,}$/i.test(name) ||
|
||||||
/^\d{1,3}(\.\d{1,3}){3}$/.test(name);
|
/^\d{1,3}(\.\d{1,3}){3}$/.test(name) || /[@\[\]:]/.test(name);
|
||||||
document.getElementById('apk-name').value = looksTechnical ? '' : name;
|
document.getElementById('apk-name').value =
|
||||||
|
(known && known.name) || remembered || (looksTechnical ? '' : name);
|
||||||
document.getElementById('apk-modal').classList.add('open');
|
document.getElementById('apk-modal').classList.add('open');
|
||||||
}
|
}
|
||||||
function closeApkDeploy() {
|
function closeApkDeploy() {
|
||||||
@@ -4785,6 +4794,7 @@
|
|||||||
// Am PC getippter Name gewinnt; sonst der aus der Netz-Erkennung (der
|
// Am PC getippter Name gewinnt; sonst der aus der Netz-Erkennung (der
|
||||||
// ist bei Fire TV oft nur die Seriennummer).
|
// ist bei Fire TV oft nur die Seriennummer).
|
||||||
params.name = document.getElementById('apk-name').value.trim() || apkTarget.name;
|
params.name = document.getElementById('apk-name').value.trim() || apkTarget.name;
|
||||||
|
rememberApkName(params.name);
|
||||||
}
|
}
|
||||||
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install',
|
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install',
|
||||||
requestId: apkRequestId, params });
|
requestId: apkRequestId, params });
|
||||||
@@ -4803,7 +4813,16 @@
|
|||||||
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.provision',
|
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.provision',
|
||||||
requestId: apkRequestId,
|
requestId: apkRequestId,
|
||||||
params: { ip: apkTarget.ip, provision: true,
|
params: { ip: apkTarget.ip, provision: true,
|
||||||
name: document.getElementById('apk-name').value.trim() || apkTarget.name } });
|
name: rememberApkName(document.getElementById('apk-name').value.trim()
|
||||||
|
|| apkTarget.name) } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merkt sich den vergebenen Namen pro Gerät (Vorschlag beim naechsten Mal). */
|
||||||
|
function rememberApkName(n) {
|
||||||
|
try {
|
||||||
|
if (apkTarget && n) localStorage.setItem('apkName:' + apkTarget.satId + ':' + apkTarget.ip, n);
|
||||||
|
} catch (e) { /* privater Modus o.ae. */ }
|
||||||
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prüft, ob der Agent auf dem Gerät installiert ist und läuft. */
|
/** Prüft, ob der Agent auf dem Gerät installiert ist und läuft. */
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ function hostList() {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return Array.from(hosts.values()).map(h => ({
|
return Array.from(hosts.values()).map(h => ({
|
||||||
hostId: h.hostId, name: h.name, os: h.os, caps: h.caps, control: h.control,
|
hostId: h.hostId, name: h.name, os: h.os, caps: h.caps, control: h.control,
|
||||||
online: (now - (h.last_seen || 0)) < 300000,
|
ip: h.ip || "", online: (now - (h.last_seen || 0)) < 300000,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,7 +1087,8 @@ function connectRVS(forcePlain) {
|
|||||||
const wasKnown = hosts.has(p.hostId);
|
const wasKnown = hosts.has(p.hostId);
|
||||||
hosts.set(p.hostId, {
|
hosts.set(p.hostId, {
|
||||||
hostId: p.hostId, name: p.name || p.hostId, os: p.os || "",
|
hostId: p.hostId, name: p.name || p.hostId, os: p.os || "",
|
||||||
caps: p.caps || [], control: !!p.control, last_seen: Date.now(),
|
caps: p.caps || [], control: !!p.control, ip: p.ip || "",
|
||||||
|
last_seen: Date.now(),
|
||||||
});
|
});
|
||||||
if (!wasKnown) broadcastHosts();
|
if (!wasKnown) broadcastHosts();
|
||||||
else hosts.get(p.hostId).last_seen = Date.now();
|
else hosts.get(p.hostId).last_seen = Date.now();
|
||||||
|
|||||||
@@ -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 101
|
versionCode 102
|
||||||
versionName '0.0.1.1'
|
versionName '0.0.1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
||||||
|
|||||||
@@ -49,6 +49,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Unsichtbarer Helfer: schaltet die Bildschirm-Aufnahme nach einem
|
||||||
|
Neustart automatisch wieder scharf (die Sitzung ist nicht speicherbar,
|
||||||
|
nur die Berechtigung). Durchsichtig, ohne Animation -> man sieht nichts. -->
|
||||||
|
<activity
|
||||||
|
android:name=".CaptureActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".AgentService"
|
android:name=".AgentService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ data class AgentConfig(
|
|||||||
/** Wie antwortet ARIA auf diesem Geraet standardmaessig: "overlay" oder "speech".
|
/** Wie antwortet ARIA auf diesem Geraet standardmaessig: "overlay" oder "speech".
|
||||||
* Pro Anfrage ueberschreibbar ("...bitte als Sprache"). */
|
* Pro Anfrage ueberschreibbar ("...bitte als Sprache"). */
|
||||||
var replyMode: String = "overlay",
|
var replyMode: String = "overlay",
|
||||||
|
/** Soll die Bildschirm-Aufnahme nach einem Neustart automatisch wieder scharf
|
||||||
|
* geschaltet werden? Wird gesetzt, sobald sie einmal erlaubt wurde — die
|
||||||
|
* SITZUNG ist naemlich nicht speicherbar, nur die Berechtigung. */
|
||||||
|
var captureEnabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS = "aria_agent"
|
private const val PREFS = "aria_agent"
|
||||||
@@ -28,6 +32,7 @@ data class AgentConfig(
|
|||||||
name = p.getString("name", "") ?: "",
|
name = p.getString("name", "") ?: "",
|
||||||
controlEnabled = p.getBoolean("control", false),
|
controlEnabled = p.getBoolean("control", false),
|
||||||
replyMode = p.getString("replyMode", "overlay") ?: "overlay",
|
replyMode = p.getString("replyMode", "overlay") ?: "overlay",
|
||||||
|
captureEnabled = p.getBoolean("capture", false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,7 @@ data class AgentConfig(
|
|||||||
putString("name", name)
|
putString("name", name)
|
||||||
putBoolean("control", controlEnabled)
|
putBoolean("control", controlEnabled)
|
||||||
putString("replyMode", replyMode)
|
putString("replyMode", replyMode)
|
||||||
|
putBoolean("capture", captureEnabled)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ class AgentService : Service() {
|
|||||||
}
|
}
|
||||||
ScreenCapturer.start(applicationContext, code, data)
|
ScreenCapturer.start(applicationContext, code, data)
|
||||||
if (ScreenCapturer.active) {
|
if (ScreenCapturer.active) {
|
||||||
|
// Merken: nach einem Neustart automatisch wieder scharf
|
||||||
|
// schalten (die Sitzung selbst ist nicht speicherbar).
|
||||||
|
AgentConfig.load(this).takeIf { !it.captureEnabled }?.let {
|
||||||
|
it.captureEnabled = true; it.save(this)
|
||||||
|
}
|
||||||
updateNotification("$status · Bildschirm-Zugriff aktiv")
|
updateNotification("$status · Bildschirm-Zugriff aktiv")
|
||||||
} else {
|
} else {
|
||||||
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
|
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
|
||||||
@@ -123,6 +128,19 @@ class AgentService : Service() {
|
|||||||
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
|
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
|
||||||
}
|
}
|
||||||
rvs?.start()
|
rvs?.start()
|
||||||
|
|
||||||
|
// Bildschirm-Aufnahme nach Geraete-/App-Neustart automatisch wieder scharf
|
||||||
|
// schalten. Die Berechtigung (AppOp PROJECT_MEDIA) bleibt erhalten, die
|
||||||
|
// SITZUNG nicht — ohne das muesste man am Fernseher jedes Mal von Hand
|
||||||
|
// "Bildschirm-Zugriff erlauben" druecken. Ist der AppOp gesetzt, laeuft
|
||||||
|
// die Anfrage ohne Dialog durch; sonst fragt Android einmal nach.
|
||||||
|
if (cfg.captureEnabled && !ScreenCapturer.active) {
|
||||||
|
android.os.Handler(mainLooper).postDelayed({
|
||||||
|
if (!ScreenCapturer.active) {
|
||||||
|
try { CaptureActivity.request(applicationContext) } catch (_: Throwable) {}
|
||||||
|
}
|
||||||
|
}, 2500)
|
||||||
|
}
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.hackersoft.ariaagent
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioFocusRequest
|
||||||
|
import android.media.AudioManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio-Fokus — damit ARIA nicht einfach ueber den laufenden Film redet.
|
||||||
|
*
|
||||||
|
* Ohne Fokus mischt Android beide Tonspuren: Netflix/Jellyfin/Spotify laufen in
|
||||||
|
* voller Lautstaerke weiter und ARIA ist kaum zu verstehen. Mit Fokus verhaelt
|
||||||
|
* sie sich wie ein normaler Sprachassistent:
|
||||||
|
*
|
||||||
|
* "duck" (Standard) laufende Medien werden LEISER, ARIA spricht darueber,
|
||||||
|
* danach geht die Lautstaerke von selbst wieder hoch.
|
||||||
|
* "pause" laufende Medien werden PAUSIERT (Player reagieren auf
|
||||||
|
* den Fokusverlust) — sinnvoll beim Zuhoeren, damit das
|
||||||
|
* Mikrofon nicht den Filmton aufnimmt.
|
||||||
|
* "none" kein Fokus (alte Mischung).
|
||||||
|
*/
|
||||||
|
object AudioFocusHelper {
|
||||||
|
|
||||||
|
private var request: AudioFocusRequest? = null
|
||||||
|
private var holder: String = ""
|
||||||
|
|
||||||
|
/** Fordert den Fokus an. [mode] = duck | pause | none. */
|
||||||
|
@Synchronized
|
||||||
|
fun request(ctx: Context, mode: String, who: String): Boolean {
|
||||||
|
if (mode == "none") return true
|
||||||
|
abandon(ctx)
|
||||||
|
val gain = if (mode == "pause") AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
|
||||||
|
else AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
||||||
|
val usage = if (who == "listen") AudioAttributes.USAGE_ASSISTANCE_SONIFICATION
|
||||||
|
else AudioAttributes.USAGE_ASSISTANT
|
||||||
|
return try {
|
||||||
|
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
val req = AudioFocusRequest.Builder(gain)
|
||||||
|
.setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(usage)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
// Kurze Unterbrechungen nicht als "verloren" behandeln.
|
||||||
|
.setWillPauseWhenDucked(false)
|
||||||
|
.setOnAudioFocusChangeListener { }
|
||||||
|
.build()
|
||||||
|
request = req
|
||||||
|
holder = who
|
||||||
|
am.requestAudioFocus(req) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den Fokus zurueck — die Medien werden wieder lauter / laufen weiter. */
|
||||||
|
@Synchronized
|
||||||
|
fun abandon(ctx: Context) {
|
||||||
|
val req = request ?: return
|
||||||
|
request = null
|
||||||
|
holder = ""
|
||||||
|
try {
|
||||||
|
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
am.abandonAudioFocusRequest(req)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package de.hackersoft.ariaagent
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.projection.MediaProjectionManager
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsichtbarer Helfer, der die Bildschirm-Aufnahme (neu) scharf schaltet.
|
||||||
|
*
|
||||||
|
* Hintergrund: Die Berechtigung (AppOp PROJECT_MEDIA) bleibt dauerhaft erhalten,
|
||||||
|
* die MediaProjection-SITZUNG aber nicht — Android laesst sie grundsaetzlich
|
||||||
|
* nicht speichern, sie stirbt mit dem Prozess. Nach einem Neustart des Geraets
|
||||||
|
* oder der App muesste man sonst jedes Mal von Hand "Bildschirm-Zugriff erlauben"
|
||||||
|
* druecken (auf einem Fire TV mit der Fernbedienung besonders laestig).
|
||||||
|
*
|
||||||
|
* Diese Activity loest die Anfrage automatisch aus und reicht das Ergebnis an den
|
||||||
|
* Dienst weiter. Ist PROJECT_MEDIA per ADB erlaubt, laeuft das OHNE Dialog durch —
|
||||||
|
* der Nutzer sieht nichts (durchsichtiges Theme, kein Fenster-Inhalt).
|
||||||
|
*/
|
||||||
|
class CaptureActivity : Activity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQ = 4711
|
||||||
|
|
||||||
|
/** Startet die (Neu-)Scharfschaltung aus dem Dienst heraus. */
|
||||||
|
fun request(ctx: Context) {
|
||||||
|
val i = Intent(ctx, CaptureActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or
|
||||||
|
Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||||
|
ctx.startActivity(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
try {
|
||||||
|
val mpm = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
startActivityForResult(mpm.createScreenCaptureIntent(), REQ)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
ScreenCapturer.lastError = "Anfrage fehlgeschlagen: ${e.message}"
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == REQ && resultCode == RESULT_OK && data != null) {
|
||||||
|
val i = Intent(this, AgentService::class.java)
|
||||||
|
.setAction(AgentService.ACTION_PROJECTION)
|
||||||
|
.putExtra(AgentService.EXTRA_RESULT_CODE, resultCode)
|
||||||
|
.putExtra(AgentService.EXTRA_RESULT_DATA, data)
|
||||||
|
androidx.core.content.ContextCompat.startForegroundService(this, i)
|
||||||
|
} else {
|
||||||
|
// Abgelehnt oder (ohne AppOp) Dialog weggeklickt — kein Drama, der
|
||||||
|
// Nutzer kann es in der App jederzeit nachholen.
|
||||||
|
ScreenCapturer.lastError =
|
||||||
|
"Bildschirm-Zugriff nicht erteilt (in der Agent-App nachholen)"
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,6 +246,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
controlEnabled = intent.getBooleanExtra("control", true),
|
controlEnabled = intent.getBooleanExtra("control", true),
|
||||||
replyMode = intent.getStringExtra("reply_mode")?.trim()
|
replyMode = intent.getStringExtra("reply_mode")?.trim()
|
||||||
.takeUnless { it.isNullOrEmpty() } ?: old.replyMode,
|
.takeUnless { it.isNullOrEmpty() } ?: old.replyMode,
|
||||||
|
// Der Satellit erteilt beim Einrichten PROJECT_MEDIA — dann darf sich
|
||||||
|
// die Bildschirm-Aufnahme nach jedem Neustart selbst scharf schalten.
|
||||||
|
captureEnabled = intent.getBooleanExtra("capture", old.captureEnabled),
|
||||||
)
|
)
|
||||||
cfg.save(this)
|
cfg.save(this)
|
||||||
AgentService.start(this)
|
AgentService.start(this)
|
||||||
|
|||||||
@@ -126,11 +126,24 @@ class RvsClient(
|
|||||||
p.put("name", config.displayName())
|
p.put("name", config.displayName())
|
||||||
val kind = if (isTv) "Fire TV / Android TV" else "Android"
|
val kind = if (isTv) "Fire TV / Android TV" else "Android"
|
||||||
p.put("os", "$kind ${Build.VERSION.RELEASE} (${Build.MODEL})")
|
p.put("os", "$kind ${Build.VERSION.RELEASE} (${Build.MODEL})")
|
||||||
|
// Eigene LAN-IP mitmelden: so kann die Diagnostic diesen Agenten dem im
|
||||||
|
// Netz entdeckten Geraet zuordnen (z.B. um beim Ausrollen den bereits
|
||||||
|
// vergebenen Namen vorzuschlagen statt der technischen Kennung).
|
||||||
|
localIp()?.let { p.put("ip", it) }
|
||||||
p.put("caps", JSONArray(caps))
|
p.put("caps", JSONArray(caps))
|
||||||
p.put("control", config.controlEnabled)
|
p.put("control", config.controlEnabled)
|
||||||
send(webSocket, "host_hello", p)
|
send(webSocket, "host_hello", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Erste echte IPv4 dieses Geraets im LAN (ohne Loopback). */
|
||||||
|
private fun localIp(): String? = try {
|
||||||
|
java.net.NetworkInterface.getNetworkInterfaces().toList()
|
||||||
|
.filter { it.isUp && !it.isLoopback }
|
||||||
|
.flatMap { it.inetAddresses.toList() }
|
||||||
|
.firstOrNull { !it.isLoopbackAddress && it is java.net.Inet4Address }
|
||||||
|
?.hostAddress
|
||||||
|
} catch (_: Throwable) { null }
|
||||||
|
|
||||||
private fun startPing(webSocket: WebSocket) {
|
private fun startPing(webSocket: WebSocket) {
|
||||||
pingThread?.interrupt()
|
pingThread?.interrupt()
|
||||||
pingThread = Thread {
|
pingThread = Thread {
|
||||||
@@ -336,12 +349,14 @@ class RvsClient(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Audio bevorzugt (ARIAs echte Stimme), sonst Geraete-TTS. */
|
/** Audio bevorzugt (ARIAs echte Stimme), sonst Geraete-TTS.
|
||||||
|
* focus: duck (Medien leiser, Standard) | pause (Medien anhalten) | none. */
|
||||||
private fun speakOut(p: JSONObject, text: String): Result<Unit> {
|
private fun speakOut(p: JSONObject, text: String): Result<Unit> {
|
||||||
val audio = p.optString("audio_base64")
|
val audio = p.optString("audio_base64")
|
||||||
|
val focus = p.optString("focus").ifBlank { "duck" }
|
||||||
return if (audio.isNotBlank())
|
return if (audio.isNotBlank())
|
||||||
Speaker.play(appCtx, audio, p.optString("audio_format", "wav"))
|
Speaker.play(appCtx, audio, p.optString("audio_format", "wav"), focus)
|
||||||
else Speaker.speak(appCtx, text)
|
else Speaker.speak(appCtx, text, focus)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doVolume(p: JSONObject): JSONObject {
|
private fun doVolume(p: JSONObject): JSONObject {
|
||||||
|
|||||||
@@ -24,28 +24,38 @@ object Speaker {
|
|||||||
private var ttsReady = false
|
private var ttsReady = false
|
||||||
private var player: MediaPlayer? = null
|
private var player: MediaPlayer? = null
|
||||||
|
|
||||||
/** Spielt fertiges Audio (Base64, z.B. WAV aus dem ARIA-TTS) ueber die Lautsprecher. */
|
/** Spielt fertiges Audio (Base64, z.B. WAV aus dem ARIA-TTS) ueber die Lautsprecher.
|
||||||
fun play(ctx: Context, audioB64: String, format: String = "wav"): Result<Unit> = runCatching {
|
* [focus] = duck (Medien leiser, Standard) | pause (Medien anhalten) | none. */
|
||||||
|
fun play(ctx: Context, audioB64: String, format: String = "wav",
|
||||||
|
focus: String = "duck"): Result<Unit> = runCatching {
|
||||||
stop()
|
stop()
|
||||||
val bytes = Base64.decode(audioB64, Base64.DEFAULT)
|
val bytes = Base64.decode(audioB64, Base64.DEFAULT)
|
||||||
val f = File(ctx.cacheDir, "aria_speak.${format.ifBlank { "wav" }}")
|
val f = File(ctx.cacheDir, "aria_speak.${format.ifBlank { "wav" }}")
|
||||||
f.writeBytes(bytes)
|
f.writeBytes(bytes)
|
||||||
|
// Erst Fokus holen, damit Netflix & Co. leiser werden (sonst redet ARIA
|
||||||
|
// einfach mittendrin mit und man versteht beides nicht).
|
||||||
|
AudioFocusHelper.request(ctx, focus, "speak")
|
||||||
player = MediaPlayer().apply {
|
player = MediaPlayer().apply {
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
.setUsage(AudioAttributes.USAGE_ASSISTANT)
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
setDataSource(f.absolutePath)
|
setDataSource(f.absolutePath)
|
||||||
setOnCompletionListener { mp -> try { mp.release() } catch (_: Exception) {}; player = null }
|
setOnCompletionListener { mp ->
|
||||||
|
try { mp.release() } catch (_: Exception) {}
|
||||||
|
player = null
|
||||||
|
AudioFocusHelper.abandon(ctx) // Medien wieder laut / weiterlaufen
|
||||||
|
}
|
||||||
|
setOnErrorListener { _, _, _ -> AudioFocusHelper.abandon(ctx); false }
|
||||||
prepare()
|
prepare()
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rueckfallebene: geraeteeigene Sprachausgabe. */
|
/** Rueckfallebene: geraeteeigene Sprachausgabe. */
|
||||||
fun speak(ctx: Context, text: String): Result<Unit> = runCatching {
|
fun speak(ctx: Context, text: String, focus: String = "duck"): Result<Unit> = runCatching {
|
||||||
if (tts == null) {
|
if (tts == null) {
|
||||||
tts = TextToSpeech(ctx.applicationContext) { status ->
|
tts = TextToSpeech(ctx.applicationContext) { status ->
|
||||||
ttsReady = status == TextToSpeech.SUCCESS
|
ttsReady = status == TextToSpeech.SUCCESS
|
||||||
@@ -56,6 +66,13 @@ object Speaker {
|
|||||||
while (!ttsReady && waited < 3000) { Thread.sleep(100); waited += 100 }
|
while (!ttsReady && waited < 3000) { Thread.sleep(100); waited += 100 }
|
||||||
}
|
}
|
||||||
if (!ttsReady) throw IllegalStateException("Keine Sprachausgabe verfuegbar (TTS-Engine fehlt)")
|
if (!ttsReady) throw IllegalStateException("Keine Sprachausgabe verfuegbar (TTS-Engine fehlt)")
|
||||||
|
AudioFocusHelper.request(ctx, focus, "speak")
|
||||||
|
tts?.setOnUtteranceProgressListener(object : android.speech.tts.UtteranceProgressListener() {
|
||||||
|
override fun onStart(id: String?) {}
|
||||||
|
override fun onDone(id: String?) { AudioFocusHelper.abandon(ctx) }
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun onError(id: String?) { AudioFocusHelper.abandon(ctx) }
|
||||||
|
})
|
||||||
tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, "aria")
|
tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, "aria")
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
@@ -68,9 +85,10 @@ object Speaker {
|
|||||||
am.setStreamVolume(AudioManager.STREAM_MUSIC, v, AudioManager.FLAG_SHOW_UI)
|
am.setStreamVolume(AudioManager.STREAM_MUSIC, v, AudioManager.FLAG_SHOW_UI)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop(ctx: Context? = null) {
|
||||||
try { player?.stop(); player?.release() } catch (_: Exception) {}
|
try { player?.stop(); player?.release() } catch (_: Exception) {}
|
||||||
player = null
|
player = null
|
||||||
try { tts?.stop() } catch (_: Exception) {}
|
try { tts?.stop() } catch (_: Exception) {}
|
||||||
|
ctx?.let { AudioFocusHelper.abandon(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ class VoiceSession(
|
|||||||
|
|
||||||
rec = r
|
rec = r
|
||||||
recording = true
|
recording = true
|
||||||
|
// Beim Zuhoeren laufende Medien ANHALTEN — sonst nimmt das Mikrofon den
|
||||||
|
// Filmton mit auf und die Spracherkennung versteht nur Kauderwelsch.
|
||||||
|
AudioFocusHelper.request(appCtx, "pause", "listen")
|
||||||
|
|
||||||
send("stt_stream_start", JSONObject().apply {
|
send("stt_stream_start", JSONObject().apply {
|
||||||
put("requestId", sid)
|
put("requestId", sid)
|
||||||
@@ -154,6 +157,9 @@ class VoiceSession(
|
|||||||
try { aec?.release() } catch (_: Exception) {}
|
try { aec?.release() } catch (_: Exception) {}
|
||||||
try { ns?.release() } catch (_: Exception) {}
|
try { ns?.release() } catch (_: Exception) {}
|
||||||
rec = null; aec = null; ns = null
|
rec = null; aec = null; ns = null
|
||||||
|
// Aufnahme vorbei -> Medien laufen weiter (die Antwort holt sich gleich
|
||||||
|
// selbst wieder Fokus, sobald ARIAs Audio kommt).
|
||||||
|
AudioFocusHelper.abandon(appCtx)
|
||||||
sessionId?.let {
|
sessionId?.let {
|
||||||
send("stt_stream_end", JSONObject().apply {
|
send("stt_stream_end", JSONObject().apply {
|
||||||
put("requestId", it); put("reason", reason); put("targetInstance", "")
|
put("requestId", it); put("reason", reason); put("targetInstance", "")
|
||||||
@@ -245,6 +251,8 @@ class VoiceSession(
|
|||||||
try { track?.stop() } catch (_: Exception) {}
|
try { track?.stop() } catch (_: Exception) {}
|
||||||
try { track?.release() } catch (_: Exception) {}
|
try { track?.release() } catch (_: Exception) {}
|
||||||
track = null; playingMessageId = null
|
track = null; playingMessageId = null
|
||||||
|
// Fertig gesprochen -> Medien wieder hochfahren/weiterlaufen lassen.
|
||||||
|
AudioFocusHelper.abandon(appCtx)
|
||||||
onEvent("bereit")
|
onEvent("bereit")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -253,6 +261,8 @@ class VoiceSession(
|
|||||||
val pcm = try { Base64.decode(b64, Base64.DEFAULT) } catch (_: Exception) { return }
|
val pcm = try { Base64.decode(b64, Base64.DEFAULT) } catch (_: Exception) { return }
|
||||||
if (track == null || playingMessageId != mid) {
|
if (track == null || playingMessageId != mid) {
|
||||||
try { track?.release() } catch (_: Exception) {}
|
try { track?.release() } catch (_: Exception) {}
|
||||||
|
// ARIA antwortet -> laufende Medien leiser (duck), bis sie fertig ist.
|
||||||
|
AudioFocusHelper.request(appCtx, "duck", "speak")
|
||||||
val min = AudioTrack.getMinBufferSize(rate, AudioFormat.CHANNEL_OUT_MONO,
|
val min = AudioTrack.getMinBufferSize(rate, AudioFormat.CHANNEL_OUT_MONO,
|
||||||
AudioFormat.ENCODING_PCM_16BIT)
|
AudioFormat.ENCODING_PCM_16BIT)
|
||||||
track = AudioTrack.Builder()
|
track = AudioTrack.Builder()
|
||||||
@@ -276,5 +286,6 @@ class VoiceSession(
|
|||||||
stopListening("shutdown")
|
stopListening("shutdown")
|
||||||
try { track?.stop(); track?.release() } catch (_: Exception) {}
|
try { track?.stop(); track?.release() } catch (_: Exception) {}
|
||||||
track = null
|
track = null
|
||||||
|
AudioFocusHelper.abandon(appCtx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.1"
|
AGENT_VERSION = "0.0.1.2"
|
||||||
|
|
||||||
HEARTBEAT_SEC = 25
|
HEARTBEAT_SEC = 25
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,11 @@ CONTROL_ENABLED=true
|
|||||||
# adb.provision Agent FERN einrichten: RVS-Zugang uebergeben, statt ihn mit der
|
# adb.provision Agent FERN einrichten: RVS-Zugang uebergeben, statt ihn mit der
|
||||||
# Fernbedienung abzutippen (params: ip, rvs_host, rvs_token, ...)
|
# Fernbedienung abzutippen (params: ip, rvs_host, rvs_token, ...)
|
||||||
# adb.status laeuft/installiert der Agent auf dem Geraet? (params: ip)
|
# adb.status laeuft/installiert der Agent auf dem Geraet? (params: ip)
|
||||||
|
# adb.permissions Sonderrechte per ADB erteilen (Bildschirmaufnahme/Overlay/
|
||||||
|
# Mikro) — spart das Zusammenklicken mit der Fernbedienung
|
||||||
# ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion
|
# ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion
|
||||||
# ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab.
|
# ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab.
|
||||||
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status
|
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status,adb.permissions
|
||||||
|
|
||||||
# ─── ADB / Android-Rollout (optional) ──────────────────────────────
|
# ─── ADB / Android-Rollout (optional) ──────────────────────────────
|
||||||
# Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren.
|
# Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren.
|
||||||
|
|||||||
+53
-3
@@ -637,6 +637,8 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict:
|
|||||||
return await loop.run_in_executor(None, _do_adb_provision, params)
|
return await loop.run_in_executor(None, _do_adb_provision, params)
|
||||||
if action == "adb.status":
|
if action == "adb.status":
|
||||||
return await loop.run_in_executor(None, _do_adb_status, params)
|
return await loop.run_in_executor(None, _do_adb_status, params)
|
||||||
|
if action == "adb.permissions":
|
||||||
|
return await loop.run_in_executor(None, _do_adb_permissions, params)
|
||||||
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
|
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
|
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
|
||||||
@@ -1267,6 +1269,42 @@ def _do_adb_status(params: dict) -> dict:
|
|||||||
"nicht installiert")}}
|
"nicht installiert")}}
|
||||||
|
|
||||||
|
|
||||||
|
def _adb_grant_permissions(target: str, package: str) -> dict:
|
||||||
|
"""Erteilt dem Agenten die Sonderrechte per ADB — das spart das Antippen mit
|
||||||
|
der Fernbedienung (auf einem Fire TV besonders muehsam):
|
||||||
|
PROJECT_MEDIA Bildschirmaufnahme ohne Nachfrage-Dialog (Screenshots)
|
||||||
|
SYSTEM_ALERT_WINDOW Einblendung ueber dem laufenden Bild (Overlay)
|
||||||
|
RECORD_AUDIO Mikrofon (Sprachdialog)
|
||||||
|
Best effort: was nicht geht, wird gemeldet, bricht aber nichts ab."""
|
||||||
|
granted, failed = [], []
|
||||||
|
ops = [("appops", ["appops", "set", package, "PROJECT_MEDIA", "allow"], "Bildschirmaufnahme"),
|
||||||
|
("appops", ["appops", "set", package, "SYSTEM_ALERT_WINDOW", "allow"], "Einblendung")]
|
||||||
|
perms = [("android.permission.RECORD_AUDIO", "Mikrofon")]
|
||||||
|
for _, cmd, label in ops:
|
||||||
|
rc, out, err = _adb_run(["-s", target, "shell"] + cmd, timeout=30)
|
||||||
|
(granted if rc == 0 and "error" not in f"{out}{err}".lower() else failed).append(label)
|
||||||
|
for perm, label in perms:
|
||||||
|
rc, out, err = _adb_run(["-s", target, "shell", "pm", "grant", package, perm], timeout=30)
|
||||||
|
(granted if rc == 0 and "error" not in f"{out}{err}".lower() else failed).append(label)
|
||||||
|
return {"granted": granted, "failed": failed}
|
||||||
|
|
||||||
|
|
||||||
|
def _do_adb_permissions(params: dict) -> dict:
|
||||||
|
"""Nur die Berechtigungen setzen (ohne Neuinstallation/Einrichtung)."""
|
||||||
|
target = _adb_target(params)
|
||||||
|
if not target:
|
||||||
|
return {"ok": False, "error": "ip/host erforderlich."}
|
||||||
|
package = (params.get("package") or AGENT_PACKAGE).strip()
|
||||||
|
ok, msg = _adb_connect(target)
|
||||||
|
if not ok:
|
||||||
|
return {"ok": False, "error": msg}
|
||||||
|
r = _adb_grant_permissions(target, package)
|
||||||
|
msg = "erteilt: " + (", ".join(r["granted"]) or "—")
|
||||||
|
if r["failed"]:
|
||||||
|
msg += " · nicht moeglich: " + ", ".join(r["failed"])
|
||||||
|
return {"ok": True, "result": {"target": target, "message": msg, **r}}
|
||||||
|
|
||||||
|
|
||||||
def _do_adb_provision(params: dict) -> dict:
|
def _do_adb_provision(params: dict) -> dict:
|
||||||
"""Richtet den ARIA-Agent auf dem Geraet FERN ein (RVS-Zugang uebergeben).
|
"""Richtet den ARIA-Agent auf dem Geraet FERN ein (RVS-Zugang uebergeben).
|
||||||
|
|
||||||
@@ -1301,6 +1339,11 @@ def _do_adb_provision(params: dict) -> dict:
|
|||||||
return {"ok": False, "error": f"'{package}' ist nicht installiert — "
|
return {"ok": False, "error": f"'{package}' ist nicht installiert — "
|
||||||
"erst adb.install ausfuehren."}
|
"erst adb.install ausfuehren."}
|
||||||
|
|
||||||
|
# Sonderrechte gleich mit erteilen (Bildschirm/Overlay/Mikro) — sonst muesste
|
||||||
|
# man sie am Fernseher mit der Fernbedienung zusammenklicken.
|
||||||
|
perms = ({"granted": [], "failed": []} if params.get("skip_permissions")
|
||||||
|
else _adb_grant_permissions(target, package))
|
||||||
|
|
||||||
# WICHTIG: 'adb shell' haengt die Argumente zu EINER Kommandozeile zusammen,
|
# WICHTIG: 'adb shell' haengt die Argumente zu EINER Kommandozeile zusammen,
|
||||||
# die auf dem Geraet nochmal von der Shell zerlegt wird. Werte mit Leerzeichen
|
# die auf dem Geraet nochmal von der Shell zerlegt wird. Werte mit Leerzeichen
|
||||||
# ("Wohnzimmer Fire TV") wuerden dabei abgeschnitten -> jeden Wert quoten.
|
# ("Wohnzimmer Fire TV") wuerden dabei abgeschnitten -> jeden Wert quoten.
|
||||||
@@ -1319,7 +1362,10 @@ def _do_adb_provision(params: dict) -> dict:
|
|||||||
"-e", "token", q(rvs_token),
|
"-e", "token", q(rvs_token),
|
||||||
"--ei", "port", str(int(params.get("rvs_port") or 443)),
|
"--ei", "port", str(int(params.get("rvs_port") or 443)),
|
||||||
"--ez", "tls", "true" if _as_bool(params.get("rvs_tls"), True) else "false",
|
"--ez", "tls", "true" if _as_bool(params.get("rvs_tls"), True) else "false",
|
||||||
"--ez", "control", "true" if _as_bool(params.get("control"), True) else "false"]
|
"--ez", "control", "true" if _as_bool(params.get("control"), True) else "false",
|
||||||
|
# Nur wenn die Bildschirm-Berechtigung wirklich sitzt: dann darf sich
|
||||||
|
# die Aufnahme nach jedem Neustart selbst scharf schalten (dialogfrei).
|
||||||
|
"--ez", "capture", "true" if "Bildschirmaufnahme" in perms.get("granted", []) else "false"]
|
||||||
if params.get("name"):
|
if params.get("name"):
|
||||||
args += ["-e", "name", q(str(params["name"]))]
|
args += ["-e", "name", q(str(params["name"]))]
|
||||||
if params.get("reply_mode"):
|
if params.get("reply_mode"):
|
||||||
@@ -1336,9 +1382,13 @@ def _do_adb_provision(params: dict) -> dict:
|
|||||||
"Android hat den Einrichtungs-Intent nicht ausgeliefert (Activity "
|
"Android hat den Einrichtungs-Intent nicht ausgeliefert (Activity "
|
||||||
"war schon offen). Auf dem Geraet die Agent-App einmal schliessen "
|
"war schon offen). Auf dem Geraet die Agent-App einmal schliessen "
|
||||||
"und erneut 'Nur einrichten' druecken. Details: " + txt[:200]}
|
"und erneut 'Nur einrichten' druecken. Details: " + txt[:200]}
|
||||||
|
msg = f"Agent eingerichtet (RVS {rvs_host}) und gestartet"
|
||||||
|
if perms.get("granted"):
|
||||||
|
msg += " · Rechte erteilt: " + ", ".join(perms["granted"])
|
||||||
|
if perms.get("failed"):
|
||||||
|
msg += " · manuell noetig: " + ", ".join(perms["failed"])
|
||||||
return {"ok": True, "result": {
|
return {"ok": True, "result": {
|
||||||
"target": target, "package": package,
|
"target": target, "package": package, "permissions": perms, "message": msg}}
|
||||||
"message": f"Agent eingerichtet (RVS {rvs_host}) und gestartet"}}
|
|
||||||
|
|
||||||
|
|
||||||
def _as_bool(v, default: bool) -> bool:
|
def _as_bool(v, default: bool) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user