feat: actions à distance du parent (voir l'écran, verrouiller/veille, couper le son)
- Partage d'écran par le service d'accessibilité (Android 11+), image réduite envoyée dans le tunnel - Bandeau « Écran partagé avec le parent » visible pendant tout le partage, arrêt auto après 5 min - Verrouillage/mise en veille (lockNow, force-lock) et coupure/rétablissement du son - Boucle de commandes en attente longue via /api/device/commands Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R9CBCVP5oDu2sfoYPisNCQ
This commit is contained in:
1 parent
e6215ab4a7
commit
7af250cc1c
12 files changed
+288
-10
No files matched your search
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.0 – 2026-10-25
|
||||
|
||||
- ✨ **Ajouté** — **Actions à distance depuis l'espace parents** (téléphones DataControl) : voir l'écran en direct, verrouiller / mettre en veille, couper ou rétablir le son.
|
||||
- ✨ **Ajouté** — Partage d'écran par le service d'accessibilité (Android 11+), image réduite envoyée par le tunnel ; un bandeau rouge « Écran partagé avec le parent » reste affiché sur le téléphone pendant toute la durée, et le partage s'arrête de lui-même au bout de 5 minutes.
|
||||
|
||||
## v0.4.0 – 2026-10-25
|
||||
|
||||
- ✨ **Ajouté** — **Mises à jour poussées depuis le serveur** : depuis l'espace parents (bouton « Déployer » ou « Mettre à jour DataControl partout »), la nouvelle version est téléchargée par le tunnel, son empreinte vérifiée, puis installée — sans aucune action sur le téléphone en mode propriétaire, sinon après un toucher sur la notification « Mise à jour de DataControl ».
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DataControl
|
||||
|
||||
🚀 v0.4.0 · 🤖 Android · 🟣 Kotlin · 🎨 Jetpack Compose · 🔒 WireGuard · 🛡️ mode propriétaire · 👨👩👧 contrôle parental · 🧪 tests · 📄 licence non déclarée
|
||||
🚀 v0.5.0 · 🤖 Android · 🟣 Kotlin · 🎨 Jetpack Compose · 🔒 WireGuard · 🛡️ mode propriétaire · 👨👩👧 contrôle parental · 🧪 tests · 📄 licence non déclarée
|
||||
|
||||
**DataControl** est l'application Android du téléphone de l'enfant. Elle relie le téléphone en permanence, par un tunnel **WireGuard**, au serveur de contrôle parental **familyd** du parent : tout le trafic (Wi-Fi comme données mobiles) passe par ce serveur, qui décide de ce qui passe. Sur le téléphone, elle bloque les applications interdites, compte le temps d'écran et suit la consommation de données mobiles.
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- **Journal de diagnostic** — État du tunnel, mode appliqué et, avec le service d'accessibilité, chaque application utilisée. Conservé 15 jours.
|
||||
- **Mise à jour intégrée** — Dernière Release Gitea depuis l'onglet *À propos* ; le serveur familyd la propose aussi aux appareils du tunnel sur `http://10.78.0.1/installer`.
|
||||
- **Déploiement depuis le serveur** — Le parent pousse la nouvelle version depuis l'espace parents : téléchargement par le tunnel, empreinte SHA-256 vérifiée, installation silencieuse en mode propriétaire (sinon un toucher sur la notification).
|
||||
- **Actions à distance** — Depuis l'espace parents : voir l'écran en direct (service d'accessibilité, Android 11+, avec bandeau visible « Écran partagé » sur le téléphone), verrouiller / mettre en veille, couper ou rétablir le son.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package com.jttools.datacontrol.guard
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -13,6 +20,8 @@ import android.telecom.TelecomManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.jttools.datacontrol.log.AppLog
|
||||
import com.jttools.datacontrol.remote.RemoteControl
|
||||
import com.jttools.datacontrol.remote.RemoteSync
|
||||
import com.jttools.datacontrol.service.MonitorStateHolder
|
||||
import com.jttools.datacontrol.service.NetworkTransport
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -22,8 +31,11 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Détecte l'application au premier plan (service d'accessibilité, sans lire le contenu des écrans)
|
||||
@@ -35,6 +47,10 @@ class AppGuardService : AccessibilityService() {
|
||||
|
||||
@Inject lateinit var engine: GuardEngine
|
||||
@Inject lateinit var stateHolder: MonitorStateHolder
|
||||
@Inject lateinit var remoteControl: RemoteControl
|
||||
@Inject lateinit var remoteSync: RemoteSync
|
||||
|
||||
private var shareBanner: TextView? = null
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var exempt: Set<String> = emptySet()
|
||||
@@ -65,6 +81,94 @@ class AppGuardService : AccessibilityService() {
|
||||
currentPkg?.let { enforce(it) }
|
||||
}
|
||||
}
|
||||
startScreenShareLoop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Partage d'écran demandé par le parent : capture l'écran (API 30+) et l'envoie au serveur, tant que
|
||||
* [RemoteControl.screenShareUntil] n'est pas atteint. Un bandeau « Écran partagé » reste affiché.
|
||||
*/
|
||||
private fun startScreenShareLoop() = scope.launch {
|
||||
while (isActive) {
|
||||
if (!remoteControl.screenSharing) {
|
||||
hideBanner()
|
||||
delay(1_000)
|
||||
continue
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
AppLog.w("Écran", "Partage d'écran indisponible (Android 11 minimum)")
|
||||
remoteSync.pushEvent("screen_unsupported", "Partage d'écran impossible : Android 11 minimum")
|
||||
delay(5_000)
|
||||
continue
|
||||
}
|
||||
showBanner()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val jpeg = captureJpeg()
|
||||
if (jpeg != null) remoteSync.pushScreen(jpeg)
|
||||
}
|
||||
delay(SCREEN_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.RequiresApi(Build.VERSION_CODES.R)
|
||||
private suspend fun captureJpeg(): ByteArray? {
|
||||
val bitmap = suspendCancellableCoroutine<Bitmap?> { cont ->
|
||||
runCatching {
|
||||
takeScreenshot(android.view.Display.DEFAULT_DISPLAY, mainExecutor,
|
||||
object : TakeScreenshotCallback {
|
||||
override fun onSuccess(result: ScreenshotResult) {
|
||||
val bmp = runCatching {
|
||||
Bitmap.wrapHardwareBuffer(result.hardwareBuffer, result.colorSpace)
|
||||
?.copy(Bitmap.Config.ARGB_8888, false)
|
||||
}.getOrNull()
|
||||
result.hardwareBuffer.close()
|
||||
if (cont.isActive) cont.resume(bmp)
|
||||
}
|
||||
override fun onFailure(errorCode: Int) {
|
||||
if (cont.isActive) cont.resume(null)
|
||||
}
|
||||
})
|
||||
}.onFailure { if (cont.isActive) cont.resume(null) }
|
||||
} ?: return null
|
||||
return runCatching {
|
||||
val scaled = scaleDown(bitmap, MAX_WIDTH)
|
||||
if (scaled !== bitmap) bitmap.recycle()
|
||||
ByteArrayOutputStream().use { out ->
|
||||
scaled.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out)
|
||||
scaled.recycle()
|
||||
out.toByteArray()
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun scaleDown(bmp: Bitmap, maxWidth: Int): Bitmap {
|
||||
if (bmp.width <= maxWidth) return bmp
|
||||
val h = bmp.height * maxWidth / bmp.width
|
||||
return Bitmap.createScaledBitmap(bmp, maxWidth, h, true)
|
||||
}
|
||||
|
||||
private fun showBanner() {
|
||||
if (shareBanner != null) return
|
||||
val wm = getSystemService(WindowManager::class.java)
|
||||
val tv = TextView(this).apply {
|
||||
text = getString(com.jttools.datacontrol.R.string.screen_shared_banner)
|
||||
setBackgroundColor(Color.parseColor("#C62828"))
|
||||
setTextColor(Color.WHITE)
|
||||
setPadding(24, 12, 24, 12)
|
||||
textSize = 13f
|
||||
}
|
||||
val lp = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply { gravity = Gravity.TOP }
|
||||
runCatching { wm.addView(tv, lp); shareBanner = tv }
|
||||
}
|
||||
|
||||
private fun hideBanner() {
|
||||
shareBanner?.let { runCatching { getSystemService(WindowManager::class.java).removeView(it) } }
|
||||
shareBanner = null
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
@@ -152,6 +256,7 @@ class AppGuardService : AccessibilityService() {
|
||||
override fun onInterrupt() = Unit
|
||||
|
||||
override fun onDestroy() {
|
||||
hideBanner()
|
||||
runCatching { unregisterReceiver(screenReceiver) }
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
@@ -160,6 +265,9 @@ class AppGuardService : AccessibilityService() {
|
||||
companion object {
|
||||
private const val CHECK_INTERVAL_MS = 10_000L
|
||||
private const val EXEMPT_TTL_MS = 5 * 60_000L
|
||||
private const val SCREEN_INTERVAL_MS = 1_500L
|
||||
private const val MAX_WIDTH = 720
|
||||
private const val JPEG_QUALITY = 50
|
||||
|
||||
fun isEnabled(context: Context): Boolean {
|
||||
val enabled = Settings.Secure.getString(
|
||||
|
||||
@@ -38,6 +38,16 @@ class FamilyApi(private val apiBase: String, private val token: String) {
|
||||
request("POST", "$apiBase/api/device/apps", token, JSONObject().put("apps", apps))
|
||||
}
|
||||
|
||||
/** Attente longue des commandes du parent (verrouillage, son, partage d'écran). */
|
||||
fun commands(waitSeconds: Int): JSONArray =
|
||||
JSONObject(request("GET", "$apiBase/api/device/commands?wait=$waitSeconds", token, null, waitSeconds * 1000 + 5000))
|
||||
.optJSONArray("commands") ?: JSONArray()
|
||||
|
||||
/** Envoie une image de l'écran (JPEG) pendant un partage. */
|
||||
fun screen(jpeg: ByteArray) {
|
||||
request("POST", "$apiBase/api/device/screen", token, jpeg, "image/jpeg")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONNECT_TIMEOUT_MS = 10_000
|
||||
private const val READ_TIMEOUT_MS = 15_000
|
||||
@@ -50,24 +60,31 @@ class FamilyApi(private val apiBase: String, private val token: String) {
|
||||
return request("POST", "${serverUrl.trimEnd('/')}/api/pair", null, body)
|
||||
}
|
||||
|
||||
fun request(method: String, url: String, token: String?, body: JSONObject?): String {
|
||||
fun request(method: String, url: String, token: String?, body: JSONObject?, readTimeoutMs: Int = READ_TIMEOUT_MS): String {
|
||||
return request(method, url, token, body?.toString()?.toByteArray(Charsets.UTF_8),
|
||||
"application/json; charset=utf-8", readTimeoutMs)
|
||||
}
|
||||
|
||||
fun request(method: String, url: String, token: String?, body: ByteArray?, contentType: String,
|
||||
readTimeoutMs: Int = READ_TIMEOUT_MS): String {
|
||||
val conn = try {
|
||||
(URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = method
|
||||
connectTimeout = CONNECT_TIMEOUT_MS
|
||||
readTimeout = READ_TIMEOUT_MS
|
||||
readTimeout = readTimeoutMs
|
||||
setRequestProperty("Accept", "application/json")
|
||||
token?.let { setRequestProperty("Authorization", "Bearer $it") }
|
||||
if (body != null) {
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
setRequestProperty("Content-Type", contentType)
|
||||
setFixedLengthStreamingMode(body.size)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
throw ApiException(0, "Serveur injoignable : ${e.message}")
|
||||
}
|
||||
try {
|
||||
if (body != null) conn.outputStream.use { it.write(body.toString().toByteArray(Charsets.UTF_8)) }
|
||||
if (body != null) conn.outputStream.use { it.write(body) }
|
||||
val code = conn.responseCode
|
||||
val text = (if (code in 200..299) conn.inputStream else conn.errorStream)
|
||||
?.bufferedReader()?.use { it.readText() }.orEmpty()
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.jttools.datacontrol.remote
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.SystemClock
|
||||
import com.jttools.datacontrol.admin.AdminController
|
||||
import com.jttools.datacontrol.data.SettingsRepository
|
||||
import com.jttools.datacontrol.log.AppLog
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Actions à distance demandées par le parent (via le serveur familyd) : verrouiller / mettre en veille,
|
||||
* couper ou rétablir le son, partager l'écran. La boucle interroge le serveur en attente longue ; le
|
||||
* partage d'écran est réalisé par le service d'accessibilité ([com.jttools.datacontrol.guard.AppGuardService]),
|
||||
* qui observe [screenShareUntil].
|
||||
*
|
||||
* Le partage d'écran est toujours visible du téléphone (notification + bandeau « Écran partagé avec le parent »).
|
||||
*/
|
||||
@Singleton
|
||||
class RemoteControl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val remote: RemoteSync,
|
||||
private val settings: SettingsRepository
|
||||
) {
|
||||
/** Instant (temps monotone) jusqu'auquel l'écran est partagé ; 0 = pas de partage. */
|
||||
private val _screenShareUntil = MutableStateFlow(0L)
|
||||
val screenShareUntil: StateFlow<Long> = _screenShareUntil.asStateFlow()
|
||||
|
||||
val screenSharing: Boolean get() = _screenShareUntil.value > SystemClock.elapsedRealtime()
|
||||
|
||||
private var started = false
|
||||
|
||||
fun start(scope: CoroutineScope) {
|
||||
if (started) return
|
||||
started = true
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
if (!settings.settingsFlow.first().isPaired) {
|
||||
delay(10_000)
|
||||
continue
|
||||
}
|
||||
val commands = remote.pollCommands(WAIT_SECONDS)
|
||||
if (commands == null) {
|
||||
delay(5_000) // tunnel coupé / serveur injoignable : on réessaie posément
|
||||
continue
|
||||
}
|
||||
for (i in 0 until commands.length()) {
|
||||
runCatching { handle(commands.getJSONObject(i)) }
|
||||
.onFailure { AppLog.w("Commande", "Commande ignorée : ${it.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fin automatique du partage d'écran à l'échéance.
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
if (screenSharing) {
|
||||
delay(1_000)
|
||||
} else {
|
||||
if (_screenShareUntil.value != 0L) _screenShareUntil.value = 0L
|
||||
delay(2_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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é")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lock() {
|
||||
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
|
||||
}
|
||||
runCatching { dpm.lockNow() }
|
||||
.onSuccess { AppLog.i("Commande", "Téléphone verrouillé / mis en veille") }
|
||||
.onFailure { AppLog.w("Commande", "Verrouillage refusé : ${it.message}") }
|
||||
}
|
||||
|
||||
private fun setMuted(muted: Boolean) {
|
||||
val am = context.getSystemService(AudioManager::class.java)
|
||||
val dir = if (muted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE
|
||||
val streams = intArrayOf(
|
||||
AudioManager.STREAM_MUSIC, AudioManager.STREAM_RING,
|
||||
AudioManager.STREAM_NOTIFICATION, AudioManager.STREAM_SYSTEM
|
||||
)
|
||||
var any = false
|
||||
for (s in streams) {
|
||||
// Le son de la sonnerie peut exiger l'accès « Ne pas déranger » : on n'échoue pas pour autant.
|
||||
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")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val WAIT_SECONDS = 25
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,20 @@ class RemoteSync @Inject constructor(
|
||||
return FamilyApi(link.apiBase, token)
|
||||
}
|
||||
|
||||
/** Attend les commandes du parent (bloquant jusqu'à [waitSeconds] s). null si le tunnel est coupé ou en erreur. */
|
||||
suspend fun pollCommands(waitSeconds: Int): JSONArray? = withContext(Dispatchers.IO) {
|
||||
if (!tunnel.up.value) return@withContext null
|
||||
val api = api() ?: return@withContext null
|
||||
try { api.commands(waitSeconds) } catch (_: ApiException) { null }
|
||||
}
|
||||
|
||||
/** Envoie une image de l'écran pendant un partage. */
|
||||
suspend fun pushScreen(jpeg: ByteArray): Boolean = withContext(Dispatchers.IO) {
|
||||
if (!tunnel.up.value) return@withContext false
|
||||
val api = api() ?: return@withContext false
|
||||
try { api.screen(jpeg); true } catch (_: ApiException) { false }
|
||||
}
|
||||
|
||||
/** Associe le téléphone au serveur : clés WireGuard générées ici, la clé privée ne quitte jamais l'appareil. */
|
||||
suspend fun pair(input: PairingInput, deviceName: String): Result<FamilyLink> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
|
||||
@@ -52,6 +52,7 @@ class DataMonitorService : LifecycleService() {
|
||||
@Inject lateinit var remoteSync: RemoteSync
|
||||
@Inject lateinit var guardEngine: GuardEngine
|
||||
@Inject lateinit var tunnel: TunnelManager
|
||||
@Inject lateinit var remoteControl: com.jttools.datacontrol.remote.RemoteControl
|
||||
|
||||
private var latestSettings: AppSettings = AppSettings()
|
||||
private var latestTransport: NetworkTransport = NetworkTransport.NONE
|
||||
@@ -106,6 +107,7 @@ class DataMonitorService : LifecycleService() {
|
||||
}
|
||||
guardEngine.start()
|
||||
deviceReporter.start(lifecycleScope)
|
||||
remoteControl.start(lifecycleScope)
|
||||
lifecycleScope.launch {
|
||||
connectivityObserver.observe().collect {
|
||||
latestTransport = it
|
||||
|
||||
@@ -32,6 +32,7 @@ object AboutContent {
|
||||
AboutEntry("Journal de diagnostic", "État du tunnel, mode appliqué et, si le service d'accessibilité est activé, chaque application utilisée. Conservé 15 jours, avec copie et partage."),
|
||||
AboutEntry("Mise à jour intégrée", "Recherche et installation de la dernière version publiée, depuis l'onglet Journal de cette page."),
|
||||
AboutEntry("Mise à jour poussée par le parent", "Depuis l'espace parents : la nouvelle version arrive par le tunnel, son empreinte est vérifiée, puis elle s'installe toute seule en mode propriétaire (sinon, touchez la notification « Mise à jour de DataControl »)."),
|
||||
AboutEntry("Actions à distance du parent", "Depuis l'espace parents : verrouiller / mettre en veille, couper ou rétablir le son, et voir l'écran en direct. Pendant un partage d'écran, un bandeau rouge « Écran partagé avec le parent » reste affiché sur le téléphone et le partage s'arrête tout seul au bout de 5 minutes."),
|
||||
AboutEntry("Reprise automatique", "La surveillance et le tunnel redémarrent après un redémarrage de l'appareil, et sont relancés périodiquement si le système les a arrêtés.")
|
||||
))
|
||||
)
|
||||
@@ -109,7 +110,8 @@ object AboutContent {
|
||||
)),
|
||||
AboutSection("Ce que voit le serveur", listOf(
|
||||
AboutEntry("Noms de domaine et volume", "Comme tout le trafic passe par le serveur du parent, celui-ci voit les sites demandés (journal DNS conservé 7 jours) et le volume échangé."),
|
||||
AboutEntry("Pas le contenu", "Les échanges chiffrés des applications (HTTPS) restent illisibles pour le serveur ; le service d'accessibilité ne lit pas le contenu des écrans.")
|
||||
AboutEntry("Pas le contenu", "Les échanges chiffrés des applications (HTTPS) restent illisibles pour le serveur ; le service d'accessibilité ne lit pas le contenu des écrans."),
|
||||
AboutEntry("Partage d'écran visible", "Le parent ne peut voir l'écran que pendant un partage qu'il déclenche ; le téléphone l'affiche alors en permanence (bandeau rouge) et le partage s'arrête seul au bout de 5 minutes. Aucune capture en dehors de ces moments.")
|
||||
)),
|
||||
AboutSection("Privilèges et licence", listOf(
|
||||
AboutEntry("Rien d'activé à votre insu", "VPN, position, administrateur d'appareil et accessibilité passent chacun par une autorisation explicite d'Android."),
|
||||
|
||||
@@ -135,4 +135,5 @@
|
||||
<string name="family_owner_release">Retirer le mode propriétaire</string>
|
||||
<string name="family_owner_release_error">Retrait impossible : voir le journal.</string>
|
||||
<string name="family_owner_off">Inactif. Pour l\'activer (une seule fois, téléphone branché au PC, tous les comptes retirés le temps de la commande) :</string>
|
||||
<string name="screen_shared_banner">🔴 Écran partagé avec le parent</string>
|
||||
</resources>
|
||||
@@ -4,4 +4,5 @@
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:canRetrieveWindowContent="false"
|
||||
android:description="@string/guard_service_description"
|
||||
android:notificationTimeout="100" />
|
||||
android:notificationTimeout="100"
|
||||
android:canTakeScreenshot="true" />
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<device-admin>
|
||||
<uses-policies />
|
||||
<uses-policies>
|
||||
<force-lock />
|
||||
</uses-policies>
|
||||
</device-admin>
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
versionName=0.4.0
|
||||
versionCode=14
|
||||
versionName=0.5.0
|
||||
versionCode=15
|
||||
Reference in new issue
Block a user