Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63cc45c57 |
No files matched your search
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.2 – 2026-09-20
|
||||||
|
|
||||||
|
- Mise à jour depuis les Releases Gitea : bouton « Rechercher une mise à jour » dans l'onglet À propos (téléchargement + installation de l'APK).
|
||||||
|
- Notification : le total estimé apparaît aussi dans la vue repliée.
|
||||||
|
- Pronote (`com.indexeducation.pronote`) est dans la liste blanche par défaut (données mobiles et Wi-Fi).
|
||||||
|
- Réinitialisation unique du mot de passe de verrouillage au prochain lancement.
|
||||||
|
|
||||||
## v0.1.1 – 2026-08-30
|
## v0.1.1 – 2026-08-30
|
||||||
|
|
||||||
- `make publish`/`make deploy` : publie l'APK signé comme asset d'une Release Gitea (tag + notes extraites du CHANGELOG), sur le même modèle que les autres apps Android du même auteur.
|
- `make publish`/`make deploy` : publie l'APK signé comme asset d'une Release Gitea (tag + notes extraites du CHANGELOG), sur le même modèle que les autres apps Android du même auteur.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<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" />
|
||||||
<!-- Nécessaire pour lister TOUTES les apps installées dans l'écran "Liste blanche" -->
|
<!-- Nécessaire pour lister TOUTES les apps installées dans l'écran "Liste blanche" -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
@@ -49,6 +50,17 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<!-- FileProvider pour installer l'APK de mise à jour téléchargé -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".service.BootReceiver"
|
android:name=".service.BootReceiver"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import android.app.Application
|
|||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import com.jttools.datacontrol.data.SettingsRepository
|
||||||
import com.jttools.datacontrol.service.DataMonitorRestartWorker
|
import com.jttools.datacontrol.service.DataMonitorRestartWorker
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class DataControlApp : Application() {
|
class DataControlApp : Application() {
|
||||||
|
|
||||||
|
@Inject lateinit var settingsRepository: SettingsRepository
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { settingsRepository.resetPasswordOnce() }
|
||||||
// Filet de sécurité : relance périodiquement DataMonitorService s'il a été tué en arrière-plan.
|
// Filet de sécurité : relance périodiquement DataMonitorService s'il a été tué en arrière-plan.
|
||||||
// enqueueUniquePeriodicWork + KEEP : sans effet si déjà planifié, donc sûr à appeler à chaque démarrage.
|
// enqueueUniquePeriodicWork + KEEP : sans effet si déjà planifié, donc sûr à appeler à chaque démarrage.
|
||||||
val request = PeriodicWorkRequestBuilder<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build()
|
val request = PeriodicWorkRequestBuilder<DataMonitorRestartWorker>(15, TimeUnit.MINUTES).build()
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ data class AppSettings(
|
|||||||
val autoResetEnabled: Boolean = true,
|
val autoResetEnabled: Boolean = true,
|
||||||
val cutoffIntervalMinutes: Int = 15,
|
val cutoffIntervalMinutes: Int = 15,
|
||||||
val baselineMobileBytesAtReset: Long = 0L,
|
val baselineMobileBytesAtReset: Long = 0L,
|
||||||
val whitelistedPackages: Set<String> = emptySet(),
|
val whitelistedPackages: Set<String> = DEFAULT_WHITELIST,
|
||||||
val whitelistedPackagesWifi: Set<String> = emptySet(),
|
val whitelistedPackagesWifi: Set<String> = DEFAULT_WHITELIST,
|
||||||
val wifiFilterEnabled: Boolean = false,
|
val wifiFilterEnabled: Boolean = false,
|
||||||
val showSystemApps: Boolean = false,
|
val showSystemApps: Boolean = false,
|
||||||
val passwordHash: String? = null,
|
val passwordHash: String? = null,
|
||||||
@@ -38,6 +38,13 @@ data class AppSettings(
|
|||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PRONOTE_PACKAGE = "com.indexeducation.pronote"
|
||||||
|
|
||||||
|
/** Applications autorisées par défaut (tant que l'utilisateur n'a pas modifié la liste blanche). */
|
||||||
|
val DEFAULT_WHITELIST: Set<String> = setOf(PRONOTE_PACKAGE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** État calculé de la consommation, dérivé des réglages + de la mesure de données réelle. */
|
/** État calculé de la consommation, dérivé des réglages + de la mesure de données réelle. */
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class SettingsRepository @Inject constructor(
|
|||||||
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
|
val SHOW_SYSTEM_APPS = booleanPreferencesKey("show_system_apps")
|
||||||
val PASSWORD_HASH = stringPreferencesKey("password_hash")
|
val PASSWORD_HASH = stringPreferencesKey("password_hash")
|
||||||
val PASSWORD_SALT = stringPreferencesKey("password_salt")
|
val PASSWORD_SALT = stringPreferencesKey("password_salt")
|
||||||
|
val PASSWORD_RESET_V1_DONE = booleanPreferencesKey("password_reset_v1_done")
|
||||||
val CUTOFF_ANCHOR_MILLIS = longPreferencesKey("cutoff_anchor_millis")
|
val CUTOFF_ANCHOR_MILLIS = longPreferencesKey("cutoff_anchor_millis")
|
||||||
val CUTOFF_IN_PULSE = booleanPreferencesKey("cutoff_in_pulse")
|
val CUTOFF_IN_PULSE = booleanPreferencesKey("cutoff_in_pulse")
|
||||||
val CUTOFF_PULSE_START_MILLIS = longPreferencesKey("cutoff_pulse_start_millis")
|
val CUTOFF_PULSE_START_MILLIS = longPreferencesKey("cutoff_pulse_start_millis")
|
||||||
@@ -152,6 +153,20 @@ class SettingsRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialisation unique (v0.1.2) du mot de passe de verrouillage : le supprime une seule fois,
|
||||||
|
* puis mémorise qu'elle a eu lieu pour ne pas effacer un mot de passe défini ensuite.
|
||||||
|
*/
|
||||||
|
suspend fun resetPasswordOnce() {
|
||||||
|
context.dataStore.edit {
|
||||||
|
if (it[Keys.PASSWORD_RESET_V1_DONE] != true) {
|
||||||
|
it.remove(Keys.PASSWORD_HASH)
|
||||||
|
it.remove(Keys.PASSWORD_SALT)
|
||||||
|
it[Keys.PASSWORD_RESET_V1_DONE] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Vrai si aucun mot de passe n'est défini, ou si [password] correspond au mot de passe stocké. */
|
/** Vrai si aucun mot de passe n'est défini, ou si [password] correspond au mot de passe stocké. */
|
||||||
suspend fun verifyPassword(password: String): Boolean {
|
suspend fun verifyPassword(password: String): Boolean {
|
||||||
val prefs = context.dataStore.data.first()
|
val prefs = context.dataStore.data.first()
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.jttools.datacontrol.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mise à jour via les Releases Gitea du dépôt (dernière release publiée + son asset .apk).
|
||||||
|
* Le dépôt est public : aucun jeton n'est nécessaire.
|
||||||
|
*/
|
||||||
|
class UpdateManager(private val context: Context, private val currentVersion: String) {
|
||||||
|
|
||||||
|
data class UpdateInfo(val versionName: String, val apkUrl: String, val changelog: String)
|
||||||
|
|
||||||
|
sealed interface CheckResult {
|
||||||
|
data object UpToDate : CheckResult
|
||||||
|
data class Available(val info: UpdateInfo) : CheckResult
|
||||||
|
data class Error(val message: String) : CheckResult
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun check(): CheckResult = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val conn = open(RELEASES_URL)
|
||||||
|
try {
|
||||||
|
if (conn.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
return@withContext CheckResult.Error("HTTP ${conn.responseCode}")
|
||||||
|
}
|
||||||
|
val json = JSONObject(conn.inputStream.bufferedReader().use { it.readText() })
|
||||||
|
val version = json.optString("tag_name").removePrefix("v")
|
||||||
|
if (version.isBlank()) return@withContext CheckResult.Error("Release sans tag")
|
||||||
|
|
||||||
|
val assets = json.optJSONArray("assets")
|
||||||
|
var apkUrl = ""
|
||||||
|
if (assets != null) {
|
||||||
|
for (i in 0 until assets.length()) {
|
||||||
|
val asset = assets.getJSONObject(i)
|
||||||
|
if (asset.optString("name").endsWith(".apk")) {
|
||||||
|
apkUrl = asset.optString("browser_download_url")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
!isNewer(version, currentVersion) -> CheckResult.UpToDate
|
||||||
|
apkUrl.isBlank() -> CheckResult.Error("Aucun APK dans la release v$version")
|
||||||
|
else -> CheckResult.Available(UpdateInfo(version, apkUrl, json.optString("body")))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
CheckResult.Error(e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Télécharge l'APK dans le cache ; [onProgress] reçoit 0-100. Retourne le fichier ou lève une exception. */
|
||||||
|
suspend fun download(info: UpdateInfo, onProgress: (Int) -> Unit): File = withContext(Dispatchers.IO) {
|
||||||
|
val dir = File(context.cacheDir, "updates").apply { mkdirs() }
|
||||||
|
dir.listFiles()?.forEach { it.delete() }
|
||||||
|
val file = File(dir, "DataControl-${info.versionName}.apk")
|
||||||
|
val conn = open(info.apkUrl)
|
||||||
|
try {
|
||||||
|
if (conn.responseCode != HttpURLConnection.HTTP_OK) error("HTTP ${conn.responseCode}")
|
||||||
|
val total = conn.contentLengthLong
|
||||||
|
var done = 0L
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
val buf = ByteArray(32 * 1024)
|
||||||
|
var n: Int
|
||||||
|
while (input.read(buf).also { n = it } != -1) {
|
||||||
|
output.write(buf, 0, n)
|
||||||
|
done += n
|
||||||
|
if (total > 0) onProgress((done * 100 / total).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vrai si l'installation a été lancée ; faux si l'utilisateur doit d'abord autoriser l'installation d'APK. */
|
||||||
|
fun install(file: File): Boolean {
|
||||||
|
if (!context.packageManager.canRequestPackageInstalls()) {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}"))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW)
|
||||||
|
.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun open(url: String): HttpURLConnection =
|
||||||
|
(URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
|
connectTimeout = 15_000
|
||||||
|
readTimeout = 30_000
|
||||||
|
setRequestProperty("User-Agent", "DataControl/$currentVersion")
|
||||||
|
setRequestProperty("Accept", "application/json, application/octet-stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val RELEASES_URL = "https://git.tuxtech.fr/api/v1/repos/tuxgyver/DataControl/releases/latest"
|
||||||
|
|
||||||
|
fun isNewer(remote: String, current: String): Boolean {
|
||||||
|
val r = remote.split(".").map { it.toIntOrNull() ?: 0 }
|
||||||
|
val c = current.split(".").map { it.toIntOrNull() ?: 0 }
|
||||||
|
for (i in 0 until maxOf(r.size, c.size)) {
|
||||||
|
val rv = r.getOrElse(i) { 0 }
|
||||||
|
val cv = c.getOrElse(i) { 0 }
|
||||||
|
if (rv != cv) return rv > cv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,8 +274,11 @@ class DataMonitorService : LifecycleService() {
|
|||||||
} else "—"
|
} else "—"
|
||||||
val priceText = String.format("%.2f", snapshot.estimatedTotalPriceEuro)
|
val priceText = String.format("%.2f", snapshot.estimatedTotalPriceEuro)
|
||||||
|
|
||||||
val contentText = "$emoji $stateLabel · ${snapshot.consumedMo}/${snapshot.planLimitMo} Mo ($percentText)"
|
val usageText = "$emoji $stateLabel · ${snapshot.consumedMo}/${snapshot.planLimitMo} Mo ($percentText)"
|
||||||
val bigText = "$contentText\n${getString(R.string.dashboard_cost_total)} : $priceText €"
|
val costText = "${getString(R.string.dashboard_cost_total)} : $priceText €"
|
||||||
|
// Le total est aussi dans la vue repliée : sinon il n'est visible qu'une fois la notification dépliée.
|
||||||
|
val contentText = "$usageText · $priceText €"
|
||||||
|
val bigText = "$usageText\n$costText"
|
||||||
|
|
||||||
val openApp = PendingIntent.getActivity(
|
val openApp = PendingIntent.getActivity(
|
||||||
this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE
|
this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE
|
||||||
|
|||||||
@@ -11,13 +11,20 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@@ -28,6 +35,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.jttools.datacontrol.R
|
import com.jttools.datacontrol.R
|
||||||
|
import com.jttools.datacontrol.data.UpdateManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutScreen() {
|
fun AboutScreen() {
|
||||||
@@ -46,6 +55,13 @@ fun AboutScreen() {
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val updateManager = remember(versionName) { UpdateManager(context.applicationContext, versionName) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var updateStatus by remember { mutableStateOf<String?>(null) }
|
||||||
|
var available by remember { mutableStateOf<UpdateManager.UpdateInfo?>(null) }
|
||||||
|
var busy by remember { mutableStateOf(false) }
|
||||||
|
var progress by remember { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -83,6 +99,65 @@ fun AboutScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
|
||||||
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
Text(stringResource(R.string.update_section), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
HorizontalDivider()
|
||||||
|
val info = available
|
||||||
|
val statusText = when {
|
||||||
|
progress >= 0 -> stringResource(R.string.update_downloading, progress)
|
||||||
|
updateStatus != null -> updateStatus!!
|
||||||
|
info != null -> stringResource(R.string.update_available, info.versionName)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (statusText != null) Text(statusText, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
if (info != null && progress < 0 && info.changelog.isNotBlank()) {
|
||||||
|
Text(info.changelog, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
if (progress >= 0) {
|
||||||
|
LinearProgressIndicator(progress = { progress / 100f }, modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
val needPermission = stringResource(R.string.update_need_permission)
|
||||||
|
val upToDate = stringResource(R.string.update_up_to_date)
|
||||||
|
val errorFmt = stringResource(R.string.update_error, "%s")
|
||||||
|
Button(
|
||||||
|
enabled = !busy,
|
||||||
|
onClick = {
|
||||||
|
busy = true
|
||||||
|
scope.launch {
|
||||||
|
if (info == null) {
|
||||||
|
updateStatus = null
|
||||||
|
when (val r = updateManager.check()) {
|
||||||
|
is UpdateManager.CheckResult.UpToDate -> updateStatus = upToDate
|
||||||
|
is UpdateManager.CheckResult.Available -> available = r.info
|
||||||
|
is UpdateManager.CheckResult.Error -> updateStatus = errorFmt.replace("%s", r.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateStatus = null
|
||||||
|
progress = 0
|
||||||
|
try {
|
||||||
|
val file = updateManager.download(info) { progress = it }
|
||||||
|
if (!updateManager.install(file)) updateStatus = needPermission
|
||||||
|
} catch (e: Exception) {
|
||||||
|
updateStatus = errorFmt.replace("%s", e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
progress = -1
|
||||||
|
}
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
when {
|
||||||
|
busy && info == null -> stringResource(R.string.update_checking)
|
||||||
|
info != null -> stringResource(R.string.update_download_install)
|
||||||
|
else -> stringResource(R.string.update_check)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
|
Card(shape = RoundedCornerShape(20.dp), elevation = CardDefaults.cardElevation(2.dp)) {
|
||||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Text(stringResource(R.string.about_section_description), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.about_section_description), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
|||||||
@@ -89,6 +89,15 @@
|
|||||||
<string name="about_description">DataControl surveille en arrière-plan la consommation de données mobiles et coupe automatiquement l\'accès à Internet en cas de dépassement du forfait, sans jamais toucher au Wi-Fi.</string>
|
<string name="about_description">DataControl surveille en arrière-plan la consommation de données mobiles et coupe automatiquement l\'accès à Internet en cas de dépassement du forfait, sans jamais toucher au Wi-Fi.</string>
|
||||||
<string name="about_section_how">Fonctionnement</string>
|
<string name="about_section_how">Fonctionnement</string>
|
||||||
<string name="about_how_it_works">Le filtrage repose sur un pare-feu local basé sur VpnService (sans root) : le trafic des applications non autorisées est capturé dans un tunnel VPN local puis silencieusement ignoré, tandis que les applications de la liste blanche continuent de communiquer normalement.</string>
|
<string name="about_how_it_works">Le filtrage repose sur un pare-feu local basé sur VpnService (sans root) : le trafic des applications non autorisées est capturé dans un tunnel VPN local puis silencieusement ignoré, tandis que les applications de la liste blanche continuent de communiquer normalement.</string>
|
||||||
|
<string name="update_section">Mise à jour</string>
|
||||||
|
<string name="update_check">Rechercher une mise à jour</string>
|
||||||
|
<string name="update_checking">Recherche en cours…</string>
|
||||||
|
<string name="update_up_to_date">Vous avez la dernière version.</string>
|
||||||
|
<string name="update_available">Version %1$s disponible</string>
|
||||||
|
<string name="update_download_install">Télécharger et installer</string>
|
||||||
|
<string name="update_downloading">Téléchargement… %1$d %%</string>
|
||||||
|
<string name="update_need_permission">Autorisez l\'installation d\'applications pour DataControl, puis relancez la mise à jour.</string>
|
||||||
|
<string name="update_error">Échec : %1$s</string>
|
||||||
<string name="about_author">Auteur</string>
|
<string name="about_author">Auteur</string>
|
||||||
<string name="about_author_value">JT-Tools — Johnny</string>
|
<string name="about_author_value">JT-Tools — Johnny</string>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="updates" path="updates/" />
|
||||||
|
</paths>
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
versionName=0.1.1
|
versionName=0.1.2
|
||||||
versionCode=2
|
versionCode=3
|
||||||
Reference in new issue
Block a user