feat(android-agent): Bildschirm-Aufnahme nach Neustart automatisch scharf schalten

Die BERECHTIGUNGEN bleiben dauerhaft (pm grant / appops in appops.xml), die
MediaProjection-SITZUNG aber nicht — Android laesst sie nicht speichern. Nach
Reboot/App-Neustart war der Agent also verbunden, konnte aber keinen Screenshot
machen, bis jemand am Fernseher 'Bildschirm-Zugriff erlauben' drueckt.

- CaptureActivity: unsichtbarer Helfer (Theme.Translucent, noHistory,
  excludeFromRecents), fordert die Projektion an und reicht das Ergebnis an den
  Dienst. Mit gesetztem AppOp PROJECT_MEDIA laeuft das OHNE Dialog durch.
- AgentService startet sie beim Dienststart automatisch nach (2,5s Verzoegerung),
  aber nur wenn captureEnabled gesetzt ist — wer die Aufnahme nie wollte, bekommt
  auch keinen Dialog.
- AgentConfig.captureEnabled: wird gesetzt, sobald die Aufnahme einmal lief, und
  kann beim Fern-Einrichten mitgegeben werden (nur wenn der Satellit PROJECT_MEDIA
  wirklich erteilen konnte).
Android-Version 0.0.1.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 02:41:46 +02:00
co-authored by Claude Opus 4.8
parent 586a2bf134
commit e43b44725b
6 changed files with 107 additions and 1 deletions
@@ -49,6 +49,17 @@
</intent-filter>
</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
android:name=".AgentService"
android:exported="false"
@@ -14,6 +14,10 @@ data class AgentConfig(
/** Wie antwortet ARIA auf diesem Geraet standardmaessig: "overlay" oder "speech".
* Pro Anfrage ueberschreibbar ("...bitte als Sprache"). */
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 {
private const val PREFS = "aria_agent"
@@ -28,6 +32,7 @@ data class AgentConfig(
name = p.getString("name", "") ?: "",
controlEnabled = p.getBoolean("control", false),
replyMode = p.getString("replyMode", "overlay") ?: "overlay",
captureEnabled = p.getBoolean("capture", false),
)
}
}
@@ -41,6 +46,7 @@ data class AgentConfig(
putString("name", name)
putBoolean("control", controlEnabled)
putString("replyMode", replyMode)
putBoolean("capture", captureEnabled)
apply()
}
}
@@ -93,6 +93,11 @@ class AgentService : Service() {
}
ScreenCapturer.start(applicationContext, code, data)
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")
} else {
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
@@ -123,6 +128,19 @@ class AgentService : Service() {
sendBroadcast(Intent(ACTION_STATUS).setPackage(packageName))
}
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
}
@@ -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),
replyMode = intent.getStringExtra("reply_mode")?.trim()
.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)
AgentService.start(this)