feat: faire sonner le téléphone à distance et accusé de réception des commandes

Nouvelle action « Faire sonner » (commandes ring / ring_stop) : sonnerie
d'alarme à plein volume même en silencieux, vibration et notification
« touchez pour arrêter », arrêt automatique au bout de la durée demandée.

Chaque commande reçue est désormais acquittée au serveur (exécutée ou en
échec, avec le détail), ce qui complète l'audit côté espace parents.

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 15:31:36 +01:00
1 parent 7af250cc1c
commit ed8be296ba
5 files changed
+217 -23

No files matched your search

@@ -33,7 +33,8 @@ import javax.inject.Singleton
class RemoteControl @Inject constructor(
@ApplicationContext private val context: Context,
private val remote: RemoteSync,
private val settings: SettingsRepository
private val settings: SettingsRepository,
private val ringer: Ringer
) {
/** Instant (temps monotone) jusqu'auquel l'écran est partagé ; 0 = pas de partage. */
private val _screenShareUntil = MutableStateFlow(0L)
@@ -58,8 +59,11 @@ class RemoteControl @Inject constructor(
continue
}
for (i in 0 until commands.length()) {
runCatching { handle(commands.getJSONObject(i)) }
.onFailure { AppLog.w("Commande", "Commande ignorée : ${it.message}") }
val cmd = commands.getJSONObject(i)
val outcome = runCatching { handle(cmd) }
val (status, detail) = outcome.getOrNull()
?: ("failed" to (outcome.exceptionOrNull()?.message ?: "erreur"))
remote.ackCommand(cmd.optString("id"), status, detail)
}
}
}
@@ -76,35 +80,43 @@ class RemoteControl @Inject constructor(
}
}
private suspend fun handle(cmd: JSONObject) {
when (cmd.optString("type")) {
"lock" -> lock()
"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)")
}
"screen_stop" -> {
_screenShareUntil.value = 0L
AppLog.i("Commande", "Partage d'écran arrêté")
}
/** Renvoie (statut, détail) pour l'audit : "done" ou "failed". */
private fun handle(cmd: JSONObject): Pair<String, String> = when (cmd.optString("type")) {
"lock" -> lock()
"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_stop" -> {
_screenShareUntil.value = 0L
AppLog.i("Commande", "Partage d'écran arrêté")
"done" to ""
}
"ring" -> ringer.start(cmd.optInt("seconds", 60))
"ring_stop" -> {
ringer.stop(silent = false)
"done" to ""
}
else -> "failed" to "commande inconnue"
}
private fun lock() {
private fun lock(): Pair<String, String> {
val dpm = context.getSystemService(DevicePolicyManager::class.java)
if (!AdminController.isActive(context)) {
AppLog.w("Commande", "Verrouillage impossible : DataControl n'est pas administrateur de l'appareil")
return
return "failed" to "DataControl n'est pas administrateur de l'appareil"
}
runCatching { dpm.lockNow() }
.onSuccess { AppLog.i("Commande", "Téléphone verrouillé / mis en veille") }
.onFailure { AppLog.w("Commande", "Verrouillage refusé : ${it.message}") }
return runCatching { dpm.lockNow() }.fold(
onSuccess = { AppLog.i("Commande", "Téléphone verrouillé / mis en veille"); "done" to "" },
onFailure = { AppLog.w("Commande", "Verrouillage refusé : ${it.message}"); "failed" to (it.message ?: "refusé") }
)
}
private fun setMuted(muted: Boolean) {
private fun setMuted(muted: Boolean): Pair<String, String> {
val am = context.getSystemService(AudioManager::class.java)
val dir = if (muted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE
val streams = intArrayOf(
@@ -117,6 +129,7 @@ class RemoteControl @Inject constructor(
if (runCatching { am.adjustStreamVolume(s, dir, 0) }.isSuccess) any = true
}
AppLog.i("Commande", if (muted) "Son coupé (${if (any) "ok" else "refusé"})" else "Son rétabli")
return if (any) "done" to "" else "failed" to "aucun flux audio modifiable (accès « Ne pas déranger » ?)"
}
private companion object {