feat: partage d'écran Android 5+ (MediaProjection) et blocage des installations

- Partage d'écran par MediaProjection (ScreenCaptureService + ScreenConsentActivity)
  quand la capture par accessibilité n'est pas possible (Android < 11 ou service coupé).
  En mode propriétaire, le service d'accessibilité valide seul le consentement système.
- Blocage de l'installation d'applications (Play Store compris) piloté par le parent
  (policy block_installs → OwnerPolicy DISALLOW_INSTALL_APPS en mode propriétaire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
This commit is contained in:
tuxgyverandClaude Opus 4.8 committed 2026-10-25 22:28:16 +01:00
1 parent 665d6e2cb3
commit d4aa0e1962
14 files changed
+328 -17

No files matched your search

@@ -3,8 +3,10 @@ package com.jttools.datacontrol.remote
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.media.AudioManager
import android.os.Build
import android.os.SystemClock
import com.jttools.datacontrol.admin.AdminController
import com.jttools.datacontrol.guard.AppGuardService
import com.jttools.datacontrol.data.SettingsRepository
import com.jttools.datacontrol.guard.ParentalLockActivity
import com.jttools.datacontrol.log.AppLog
@@ -43,6 +45,9 @@ class RemoteControl @Inject constructor(
val screenSharing: Boolean get() = _screenShareUntil.value > SystemClock.elapsedRealtime()
/** Vrai pendant qu'on attend le consentement MediaProjection (le dialogue système est à l'écran). */
@Volatile var awaitingScreenConsent = false
private var started = false
fun start(scope: CoroutineScope) {
@@ -88,14 +93,11 @@ class RemoteControl @Inject constructor(
"punlock" -> parentalLock(false)
"mute" -> setMuted(true)
"unmute" -> setMuted(false)
"screen_start" -> {
val seconds = cmd.optInt("seconds", 120).coerceIn(10, 300)
_screenShareUntil.value = SystemClock.elapsedRealtime() + seconds * 1000L
AppLog.i("Commande", "Partage d'écran démarré ($seconds s)")
"done" to "partage démarré"
}
"screen_start" -> startScreenShare(cmd.optInt("seconds", 120).coerceIn(10, 300))
"screen_stop" -> {
_screenShareUntil.value = 0L
awaitingScreenConsent = false
ScreenCaptureService.stop(context)
AppLog.i("Commande", "Partage d'écran arrêté")
"done" to ""
}
@@ -123,6 +125,22 @@ class RemoteControl @Inject constructor(
}
}
/** Démarre le partage d'écran : capture silencieuse par l'accessibilité (Android 11+) ou MediaProjection. */
private fun startScreenShare(seconds: Int): Pair<String, String> {
_screenShareUntil.value = SystemClock.elapsedRealtime() + seconds * 1000L
val silent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && AppGuardService.isEnabled(context)
if (silent) {
awaitingScreenConsent = false
AppLog.i("Commande", "Partage d'écran démarré ($seconds s, accessibilité)")
} else if (!ScreenCaptureService.running) {
awaitingScreenConsent = true
runCatching { context.startActivity(ScreenConsentActivity.intent(context)) }
.onFailure { AppLog.w("Commande", "Lancement du partage MediaProjection impossible : ${it.message}") }
AppLog.i("Commande", "Partage d'écran démarré ($seconds s, MediaProjection)")
}
return "done" to "partage démarré"
}
private fun lock(): Pair<String, String> {
val dpm = context.getSystemService(DevicePolicyManager::class.java)
if (!AdminController.isActive(context)) {