Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b138a15513 | ||
|
|
303d2c4cdc | ||
|
|
36c829c14d | ||
|
|
ed8be296ba |
No files matched your search
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.7.0 – 2026-10-25
|
||||||
|
|
||||||
|
- ✨ **Ajouté** — **Déploiement forcé** depuis l'espace parents : réinstalle DataControl via le tunnel WireGuard même si la version proposée n'est pas plus récente que celle installée (réparation / réinstallation à distance).
|
||||||
|
|
||||||
|
## v0.6.0 – 2026-10-25
|
||||||
|
|
||||||
|
- ✨ **Ajouté** — **Faire sonner le téléphone** depuis l'espace parents : une sonnerie d'alarme retentit à plein volume (même si le téléphone est en silencieux), avec vibration et une notification « touchez pour arrêter » ; la sonnerie s'arrête d'elle-même au bout de la durée demandée, ou quand le parent l'arrête.
|
||||||
|
- 🛠️ **Modifié** — Chaque action à distance (verrouillage, son, partage d'écran, sonnerie) est désormais acquittée au serveur — exécutée ou en échec, avec le détail — ce qui complète l'audit des commandes côté espace parents.
|
||||||
|
|
||||||
## v0.5.0 – 2026-10-25
|
## 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é** — **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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# DataControl
|
# DataControl
|
||||||
|
|
||||||
🚀 v0.5.0 · 🤖 Android · 🟣 Kotlin · 🎨 Jetpack Compose · 🔒 WireGuard · 🛡️ mode propriétaire · 👨👩👧 contrôle parental · 🧪 tests · 📄 licence non déclarée
|
🚀 v0.7.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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
@@ -118,6 +119,11 @@
|
|||||||
android:name=".remote.InstallResultReceiver"
|
android:name=".remote.InstallResultReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- Arrêt de la sonnerie « Faire sonner » depuis la notification. -->
|
||||||
|
<receiver
|
||||||
|
android:name=".remote.RingStopReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".service.BootReceiver"
|
android:name=".service.BootReceiver"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -50,12 +50,15 @@ class AppDeployer @Inject constructor(
|
|||||||
suspend fun handle(order: JSONObject, apiBase: String, report: suspend (String, String) -> Unit) =
|
suspend fun handle(order: JSONObject, apiBase: String, report: suspend (String, String) -> Unit) =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val version = order.optString("version")
|
val version = order.optString("version")
|
||||||
if (version.isBlank() || !UpdateManager.isNewer(version, BuildConfig.VERSION_NAME)) return@withContext
|
if (version.isBlank()) return@withContext
|
||||||
|
// Déploiement forcé (option du parent) : réinstalle même version comprise, en ignorant les garde-fous.
|
||||||
|
val force = order.optBoolean("force", false)
|
||||||
|
if (!force && !UpdateManager.isNewer(version, BuildConfig.VERSION_NAME)) return@withContext
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
if (version == lastVersion && now - lastAttemptAt < RETRY_MS) return@withContext
|
if (!force && version == lastVersion && now - lastAttemptAt < RETRY_MS) return@withContext
|
||||||
lastVersion = version
|
lastVersion = version
|
||||||
lastAttemptAt = now
|
lastAttemptAt = now
|
||||||
AppLog.i("Mise à jour", "Déploiement de DataControl $version demandé par le serveur")
|
AppLog.i("Mise à jour", "Déploiement de DataControl $version demandé par le serveur" + if (force) " (forcé)" else "")
|
||||||
try {
|
try {
|
||||||
val file = download(apiBase + order.getString("url"), order.getString("sha256").lowercase(), version)
|
val file = download(apiBase + order.getString("url"), order.getString("sha256").lowercase(), version)
|
||||||
install(file, version)
|
install(file, version)
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ class FamilyApi(private val apiBase: String, private val token: String) {
|
|||||||
request("POST", "$apiBase/api/device/screen", token, jpeg, "image/jpeg")
|
request("POST", "$apiBase/api/device/screen", token, jpeg, "image/jpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Signale au serveur qu'une commande a été exécutée (audit). */
|
||||||
|
fun commandAck(id: String, status: String, detail: String) {
|
||||||
|
request("POST", "$apiBase/api/device/command-ack", token,
|
||||||
|
JSONObject().put("id", id).put("status", status).put("detail", detail.take(300)))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CONNECT_TIMEOUT_MS = 10_000
|
private const val CONNECT_TIMEOUT_MS = 10_000
|
||||||
private const val READ_TIMEOUT_MS = 15_000
|
private const val READ_TIMEOUT_MS = 15_000
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ import javax.inject.Singleton
|
|||||||
class RemoteControl @Inject constructor(
|
class RemoteControl @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val remote: RemoteSync,
|
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. */
|
/** Instant (temps monotone) jusqu'auquel l'écran est partagé ; 0 = pas de partage. */
|
||||||
private val _screenShareUntil = MutableStateFlow(0L)
|
private val _screenShareUntil = MutableStateFlow(0L)
|
||||||
@@ -58,8 +59,11 @@ class RemoteControl @Inject constructor(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (i in 0 until commands.length()) {
|
for (i in 0 until commands.length()) {
|
||||||
runCatching { handle(commands.getJSONObject(i)) }
|
val cmd = commands.getJSONObject(i)
|
||||||
.onFailure { AppLog.w("Commande", "Commande ignorée : ${it.message}") }
|
val outcome = runCatching { handle(cmd) }
|
||||||
|
val (status, detail) = outcome.getOrNull()
|
||||||
|
?: ("failed" to (outcome.exceptionOrNull()?.message ?: "erreur"))
|
||||||
|
remote.ackCommand(cmd.optString("id"), status, detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +80,8 @@ class RemoteControl @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handle(cmd: JSONObject) {
|
/** Renvoie (statut, détail) pour l'audit : "done" ou "failed". */
|
||||||
when (cmd.optString("type")) {
|
private fun handle(cmd: JSONObject): Pair<String, String> = when (cmd.optString("type")) {
|
||||||
"lock" -> lock()
|
"lock" -> lock()
|
||||||
"mute" -> setMuted(true)
|
"mute" -> setMuted(true)
|
||||||
"unmute" -> setMuted(false)
|
"unmute" -> setMuted(false)
|
||||||
@@ -85,26 +89,34 @@ class RemoteControl @Inject constructor(
|
|||||||
val seconds = cmd.optInt("seconds", 120).coerceIn(10, 300)
|
val seconds = cmd.optInt("seconds", 120).coerceIn(10, 300)
|
||||||
_screenShareUntil.value = SystemClock.elapsedRealtime() + seconds * 1000L
|
_screenShareUntil.value = SystemClock.elapsedRealtime() + seconds * 1000L
|
||||||
AppLog.i("Commande", "Partage d'écran démarré ($seconds s)")
|
AppLog.i("Commande", "Partage d'écran démarré ($seconds s)")
|
||||||
|
"done" to "partage démarré"
|
||||||
}
|
}
|
||||||
"screen_stop" -> {
|
"screen_stop" -> {
|
||||||
_screenShareUntil.value = 0L
|
_screenShareUntil.value = 0L
|
||||||
AppLog.i("Commande", "Partage d'écran arrêté")
|
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)
|
val dpm = context.getSystemService(DevicePolicyManager::class.java)
|
||||||
if (!AdminController.isActive(context)) {
|
if (!AdminController.isActive(context)) {
|
||||||
AppLog.w("Commande", "Verrouillage impossible : DataControl n'est pas administrateur de l'appareil")
|
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() }
|
return runCatching { dpm.lockNow() }.fold(
|
||||||
.onSuccess { AppLog.i("Commande", "Téléphone verrouillé / mis en veille") }
|
onSuccess = { AppLog.i("Commande", "Téléphone verrouillé / mis en veille"); "done" to "" },
|
||||||
.onFailure { AppLog.w("Commande", "Verrouillage refusé : ${it.message}") }
|
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 am = context.getSystemService(AudioManager::class.java)
|
||||||
val dir = if (muted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE
|
val dir = if (muted) AudioManager.ADJUST_MUTE else AudioManager.ADJUST_UNMUTE
|
||||||
val streams = intArrayOf(
|
val streams = intArrayOf(
|
||||||
@@ -117,6 +129,7 @@ class RemoteControl @Inject constructor(
|
|||||||
if (runCatching { am.adjustStreamVolume(s, dir, 0) }.isSuccess) any = true
|
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")
|
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 {
|
private companion object {
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ class RemoteSync @Inject constructor(
|
|||||||
try { api.screen(jpeg); true } catch (_: ApiException) { false }
|
try { api.screen(jpeg); true } catch (_: ApiException) { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Accuse réception/exécution d'une commande (audit côté parent). */
|
||||||
|
suspend fun ackCommand(id: String, status: String, detail: String = "") = withContext(Dispatchers.IO) {
|
||||||
|
if (id.isBlank() || !tunnel.up.value) return@withContext
|
||||||
|
val api = api() ?: return@withContext
|
||||||
|
runCatching { api.commandAck(id, status, detail) }
|
||||||
|
}
|
||||||
|
|
||||||
/** Associe le téléphone au serveur : clés WireGuard générées ici, la clé privée ne quitte jamais l'appareil. */
|
/** 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) {
|
suspend fun pair(input: PairingInput, deviceName: String): Result<FamilyLink> = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package com.jttools.datacontrol.remote
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.media.RingtoneManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.jttools.datacontrol.R
|
||||||
|
import com.jttools.datacontrol.log.AppLog
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* « Faire sonner » demandé par le parent : joue une sonnerie d'alarme à plein volume, même si le
|
||||||
|
* téléphone est en silencieux (le flux d'alarme n'est pas concerné par le mode silencieux), vibre et
|
||||||
|
* affiche une notification « Appuyez pour arrêter ». S'arrête tout seul au bout de la durée demandée,
|
||||||
|
* à la demande du parent (commande ring_stop), ou quand l'enfant touche la notification.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class Ringer @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) {
|
||||||
|
private var player: MediaPlayer? = null
|
||||||
|
private var previousVolume = -1
|
||||||
|
private var stopTimer: Timer? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun start(seconds: Int): Pair<String, String> {
|
||||||
|
stop(silent = true) // repart proprement si une sonnerie était déjà en cours
|
||||||
|
val am = context.getSystemService(AudioManager::class.java)
|
||||||
|
val uri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_ALARM)
|
||||||
|
?: RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
|
||||||
|
?: return "failed" to "aucune sonnerie disponible"
|
||||||
|
return try {
|
||||||
|
previousVolume = am.getStreamVolume(AudioManager.STREAM_ALARM)
|
||||||
|
am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), 0)
|
||||||
|
player = MediaPlayer().apply {
|
||||||
|
setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
setDataSource(context, uri)
|
||||||
|
isLooping = true
|
||||||
|
prepare()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
vibrate(true)
|
||||||
|
notify(context)
|
||||||
|
val secs = seconds.coerceIn(5, 300)
|
||||||
|
stopTimer = Timer().apply {
|
||||||
|
schedule(object : TimerTask() {
|
||||||
|
override fun run() = stop(silent = false)
|
||||||
|
}, secs * 1000L)
|
||||||
|
}
|
||||||
|
AppLog.i("Commande", "Sonnerie déclenchée ($secs s)")
|
||||||
|
"done" to "sonnerie déclenchée"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.e("Commande", "Sonnerie impossible", e)
|
||||||
|
stop(silent = true)
|
||||||
|
"failed" to (e.message ?: "sonnerie impossible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun stop(silent: Boolean) {
|
||||||
|
stopTimer?.cancel(); stopTimer = null
|
||||||
|
runCatching { player?.stop() }
|
||||||
|
runCatching { player?.release() }
|
||||||
|
player = null
|
||||||
|
vibrate(false)
|
||||||
|
if (previousVolume >= 0) {
|
||||||
|
runCatching {
|
||||||
|
context.getSystemService(AudioManager::class.java)
|
||||||
|
.setStreamVolume(AudioManager.STREAM_ALARM, previousVolume, 0)
|
||||||
|
}
|
||||||
|
previousVolume = -1
|
||||||
|
}
|
||||||
|
context.getSystemService(NotificationManager::class.java).cancel(NOTIF_ID)
|
||||||
|
if (!silent) AppLog.i("Commande", "Sonnerie arrêtée")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vibrate(on: Boolean) {
|
||||||
|
val vib = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
context.getSystemService(VibratorManager::class.java)?.defaultVibrator
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION") context.getSystemService(Vibrator::class.java)
|
||||||
|
} ?: return
|
||||||
|
if (!on) { runCatching { vib.cancel() }; return }
|
||||||
|
runCatching {
|
||||||
|
val pattern = longArrayOf(0, 600, 400)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
vib.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION") vib.vibrate(pattern, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(context: Context) {
|
||||||
|
val nm = context.getSystemService(NotificationManager::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
nm.createNotificationChannel(
|
||||||
|
NotificationChannel(CHANNEL, "Sonnerie", NotificationManager.IMPORTANCE_HIGH).apply {
|
||||||
|
setSound(null, null) // le son est joué par le lecteur (flux d'alarme)
|
||||||
|
enableVibration(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val stop = PendingIntent.getBroadcast(
|
||||||
|
context, 0, Intent(context, RingStopReceiver::class.java),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
nm.notify(NOTIF_ID, NotificationCompat.Builder(context, CHANNEL)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentTitle("DataControl sonne")
|
||||||
|
.setContentText("Demandé par le parent — touchez pour arrêter.")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(stop)
|
||||||
|
.setFullScreenIntent(stop, true)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANNEL = "datacontrol_ring"
|
||||||
|
private const val NOTIF_ID = 3102
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Arrêt de la sonnerie depuis la notification (touche de l'enfant). */
|
||||||
|
class RingStopReceiver : BroadcastReceiver() {
|
||||||
|
@EntryPoint
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface Deps {
|
||||||
|
fun ringer(): Ringer
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
EntryPointAccessors.fromApplication(context.applicationContext, Deps::class.java).ringer().stop(silent = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
versionName=0.5.0
|
versionName=0.7.0
|
||||||
versionCode=15
|
versionCode=17
|
||||||
Reference in new issue
Block a user