Add F-Droid as update check sources
This commit is contained in:
@@ -11,8 +11,10 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
|
import io.nekohasekai.sfa.update.UpdateSource
|
||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.update.UpdateTrack
|
import io.nekohasekai.sfa.update.UpdateTrack
|
||||||
|
import io.nekohasekai.sfa.update.checkFDroidUpdate
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class UpdateWorker(private val appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
class UpdateWorker(private val appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
|
||||||
@@ -59,8 +61,13 @@ class UpdateWorker(private val appContext: Context, params: WorkerParameters) :
|
|||||||
Log.d(TAG, "Checking for updates...")
|
Log.d(TAG, "Checking for updates...")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
val updateInfo = when (UpdateSource.fromString(Settings.updateSource)) {
|
||||||
|
UpdateSource.FDROID -> checkFDroidUpdate(appContext)
|
||||||
|
UpdateSource.GITHUB -> {
|
||||||
val track = UpdateTrack.fromString(Settings.updateTrack)
|
val track = UpdateTrack.fromString(Settings.updateTrack)
|
||||||
val updateInfo = GitHubUpdateChecker().use { it.checkUpdate(track) }
|
GitHubUpdateChecker().use { it.checkUpdate(track) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateInfo == null) {
|
if (updateInfo == null) {
|
||||||
Log.d(TAG, "No update available")
|
Log.d(TAG, "No update available")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.nekohasekai.sfa.compose.screen.profile.EditProfileRoute
|
|||||||
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
import io.nekohasekai.sfa.compose.screen.profileoverride.PerAppProxyScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
||||||
|
import io.nekohasekai.sfa.compose.screen.settings.FDroidMirrorScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.PrivilegeSettingsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
||||||
@@ -224,6 +225,16 @@ fun SFANavHost(
|
|||||||
AppSettingsScreen(navController = navController)
|
AppSettingsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "settings/fdroid_mirror",
|
||||||
|
enterTransition = slideInFromRight,
|
||||||
|
exitTransition = slideOutToLeft,
|
||||||
|
popEnterTransition = slideInFromLeft,
|
||||||
|
popExitTransition = slideOutToRight,
|
||||||
|
) {
|
||||||
|
FDroidMirrorScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = "settings/core",
|
route = "settings/core",
|
||||||
enterTransition = slideInFromRight,
|
enterTransition = slideInFromRight,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.text.format.Formatter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -27,6 +28,8 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||||
import androidx.compose.material.icons.outlined.Autorenew
|
import androidx.compose.material.icons.outlined.Autorenew
|
||||||
|
import androidx.compose.material.icons.outlined.DeleteForever
|
||||||
|
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||||
import androidx.compose.material.icons.outlined.Download
|
import androidx.compose.material.icons.outlined.Download
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Language
|
import androidx.compose.material.icons.outlined.Language
|
||||||
@@ -62,6 +65,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
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.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -71,6 +75,7 @@ import androidx.core.os.LocaleListCompat
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import io.nekohasekai.libbox.Libbox
|
||||||
import io.nekohasekai.sfa.Application
|
import io.nekohasekai.sfa.Application
|
||||||
import io.nekohasekai.sfa.BuildConfig
|
import io.nekohasekai.sfa.BuildConfig
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
@@ -78,6 +83,7 @@ import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
|||||||
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||||
|
import io.nekohasekai.sfa.update.UpdateSource
|
||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.update.UpdateTrack
|
import io.nekohasekai.sfa.update.UpdateTrack
|
||||||
import io.nekohasekai.sfa.utils.HookStatusClient
|
import io.nekohasekai.sfa.utils.HookStatusClient
|
||||||
@@ -88,6 +94,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import java.io.File
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import android.provider.Settings as AndroidSettings
|
import android.provider.Settings as AndroidSettings
|
||||||
|
|
||||||
@@ -113,10 +120,12 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
val hasUpdate by UpdateState.hasUpdate
|
val hasUpdate by UpdateState.hasUpdate
|
||||||
val updateInfo by UpdateState.updateInfo
|
val updateInfo by UpdateState.updateInfo
|
||||||
val isChecking by UpdateState.isChecking
|
val isChecking by UpdateState.isChecking
|
||||||
|
var showSourceDialog by remember { mutableStateOf(false) }
|
||||||
|
var currentSource by remember { mutableStateOf(Settings.updateSource) }
|
||||||
var showTrackDialog by remember { mutableStateOf(false) }
|
var showTrackDialog by remember { mutableStateOf(false) }
|
||||||
var currentTrack by remember { mutableStateOf(Settings.updateTrack) }
|
var currentTrack by remember { mutableStateOf(Settings.updateTrack) }
|
||||||
var checkUpdateEnabled by remember { mutableStateOf(Settings.checkUpdateEnabled) }
|
var checkUpdateEnabled by remember { mutableStateOf(Settings.checkUpdateEnabled) }
|
||||||
var showErrorDialog by remember { mutableStateOf<Int?>(null) }
|
var showErrorDialog by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
var silentInstallEnabled by remember { mutableStateOf(Settings.silentInstallEnabled) }
|
var silentInstallEnabled by remember { mutableStateOf(Settings.silentInstallEnabled) }
|
||||||
var silentInstallMethod by remember { mutableStateOf(Settings.silentInstallMethod) }
|
var silentInstallMethod by remember { mutableStateOf(Settings.silentInstallMethod) }
|
||||||
@@ -144,8 +153,22 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
mutableStateOf(if (appLocales.isEmpty) "" else appLocales.toLanguageTags())
|
mutableStateOf(if (appLocales.isEmpty) "" else appLocales.toLanguageTags())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cacheSize by remember { mutableStateOf(0L) }
|
||||||
|
var cacheSizeText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
fun refreshCacheSize() {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
val size = calculateDirSize(context.cacheDir)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cacheSize = size
|
||||||
|
cacheSizeText = Formatter.formatFileSize(context, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
HookStatusClient.refresh()
|
HookStatusClient.refresh()
|
||||||
|
refreshCacheSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-check states when returning from background (e.g., after granting permission)
|
// Re-check states when returning from background (e.g., after granting permission)
|
||||||
@@ -183,6 +206,21 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSourceDialog) {
|
||||||
|
UpdateSourceDialog(
|
||||||
|
currentSource = currentSource,
|
||||||
|
onSourceSelected = { source ->
|
||||||
|
currentSource = source
|
||||||
|
UpdateState.clear()
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
Settings.updateSource = source
|
||||||
|
}
|
||||||
|
showSourceDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showSourceDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showTrackDialog) {
|
if (showTrackDialog) {
|
||||||
UpdateTrackDialog(
|
UpdateTrackDialog(
|
||||||
currentTrack = currentTrack,
|
currentTrack = currentTrack,
|
||||||
@@ -198,11 +236,11 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorDialog?.let { messageRes ->
|
showErrorDialog?.let { message ->
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showErrorDialog = null },
|
onDismissRequest = { showErrorDialog = null },
|
||||||
title = { Text(stringResource(R.string.check_update)) },
|
title = { Text(stringResource(R.string.check_update)) },
|
||||||
text = { Text(stringResource(messageRes)) },
|
text = { Text(message) },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = { showErrorDialog = null }) {
|
TextButton(onClick = { showErrorDialog = null }) {
|
||||||
Text(stringResource(R.string.ok))
|
Text(stringResource(R.string.ok))
|
||||||
@@ -440,13 +478,80 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
|
||||||
.clickable { showLanguageDialog = true },
|
.clickable { showLanguageDialog = true },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.cache_size),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (cacheSizeText.isNotEmpty()) {
|
||||||
|
Text(cacheSizeText, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DeleteSweep,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(
|
||||||
|
if (cacheSize > 0L) {
|
||||||
|
RoundedCornerShape(0.dp)
|
||||||
|
} else {
|
||||||
|
RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (cacheSize > 0L) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.clear_cache),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DeleteForever,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||||
|
.clickable {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.cacheDir?.listFiles()?.forEach { it.deleteRecursively() }
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cacheSize = 0L
|
||||||
|
cacheSizeText = Formatter.formatFileSize(context, 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,14 +660,21 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
|
val isFDroid = UpdateSource.fromString(currentSource) == UpdateSource.FDROID
|
||||||
val updateItemCount =
|
val updateItemCount =
|
||||||
run {
|
run {
|
||||||
var count = 0
|
var count = 0
|
||||||
if (Vendor.supportsTrackSelection()) {
|
if (Vendor.updateSources.size > 1) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
if (Vendor.hasCustomUpdate) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
if (isFDroid) {
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
count += 1
|
count += 1
|
||||||
if (Vendor.supportsSilentInstall()) {
|
if (Vendor.hasCustomUpdate) {
|
||||||
count += 1
|
count += 1
|
||||||
if (silentInstallEnabled) {
|
if (silentInstallEnabled) {
|
||||||
count += 1
|
count += 1
|
||||||
@@ -574,7 +686,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Vendor.supportsAutoUpdate()) {
|
if (Vendor.hasCustomUpdate) {
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
count
|
count
|
||||||
@@ -592,7 +704,39 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Vendor.supportsTrackSelection()) {
|
if (Vendor.updateSources.size > 1) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.update_source),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val sourceName = when (UpdateSource.fromString(currentSource)) {
|
||||||
|
UpdateSource.GITHUB -> stringResource(R.string.update_source_github)
|
||||||
|
UpdateSource.FDROID -> stringResource(R.string.update_source_fdroid)
|
||||||
|
}
|
||||||
|
Text(sourceName, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.NewReleases,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
updateItemModifier()
|
||||||
|
.clickable { showSourceDialog = true },
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Vendor.hasCustomUpdate) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
@@ -601,10 +745,14 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
val trackName = when (UpdateTrack.fromString(currentTrack)) {
|
val trackName = if (isFDroid) {
|
||||||
|
stringResource(R.string.update_track_stable)
|
||||||
|
} else {
|
||||||
|
when (UpdateTrack.fromString(currentTrack)) {
|
||||||
UpdateTrack.STABLE -> stringResource(R.string.update_track_stable)
|
UpdateTrack.STABLE -> stringResource(R.string.update_track_stable)
|
||||||
UpdateTrack.BETA -> stringResource(R.string.update_track_beta)
|
UpdateTrack.BETA -> stringResource(R.string.update_track_beta)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Text(trackName, style = MaterialTheme.typography.bodyMedium)
|
Text(trackName, style = MaterialTheme.typography.bodyMedium)
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
@@ -615,8 +763,63 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
|
updateItemModifier().let {
|
||||||
|
if (isFDroid) it.alpha(0.38f) else it.clickable { showTrackDialog = true }
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFDroid) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.fdroid_mirror),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
val mirrorUrl = Settings.fdroidMirrorUrl
|
||||||
|
val mirrorName = remember(mirrorUrl) {
|
||||||
|
val iter = Libbox.getFDroidMirrors()
|
||||||
|
var name: String? = null
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
val m = iter.next()
|
||||||
|
if (m.url == mirrorUrl) {
|
||||||
|
name = m.name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name == null) {
|
||||||
|
val customMirrors = Settings.fdroidCustomMirrors
|
||||||
|
for (entry in customMirrors) {
|
||||||
|
val parts = entry.split("|", limit = 2)
|
||||||
|
if (parts.size == 2 && parts[1] == mirrorUrl) {
|
||||||
|
name = parts[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name ?: mirrorUrl
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
mirrorName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Speed,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
updateItemModifier()
|
updateItemModifier()
|
||||||
.clickable { showTrackDialog = true },
|
.clickable { navController.navigate("settings/fdroid_mirror") },
|
||||||
colors =
|
colors =
|
||||||
ListItemDefaults.colors(
|
ListItemDefaults.colors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
@@ -656,7 +859,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (Vendor.supportsSilentInstall()) {
|
if (Vendor.hasCustomUpdate) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
@@ -836,7 +1039,7 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Vendor.supportsAutoUpdate()) {
|
if (Vendor.hasCustomUpdate) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
@@ -940,15 +1143,17 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
val result = Vendor.checkUpdateAsync()
|
val result = Vendor.checkUpdateAsync()
|
||||||
UpdateState.setUpdate(result)
|
UpdateState.setUpdate(result)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
showErrorDialog = R.string.no_updates_available
|
showErrorDialog = context.getString(R.string.no_updates_available)
|
||||||
} else {
|
} else {
|
||||||
showUpdateAvailableDialog = true
|
showUpdateAvailableDialog = true
|
||||||
}
|
}
|
||||||
} catch (_: UpdateCheckException.TrackNotSupported) {
|
} catch (_: UpdateCheckException.TrackNotSupported) {
|
||||||
UpdateState.setUpdate(null)
|
UpdateState.setUpdate(null)
|
||||||
showErrorDialog = R.string.update_track_not_supported
|
showErrorDialog = context.getString(R.string.update_track_not_supported)
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("AppSettingsScreen", "checkUpdateAsync failed", e)
|
||||||
UpdateState.setUpdate(null)
|
UpdateState.setUpdate(null)
|
||||||
|
showErrorDialog = e.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UpdateState.isChecking.value = false
|
UpdateState.isChecking.value = false
|
||||||
@@ -998,6 +1203,53 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UpdateSourceDialog(
|
||||||
|
currentSource: String,
|
||||||
|
onSourceSelected: (String) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sources = listOf(
|
||||||
|
"github" to stringResource(R.string.update_source_github),
|
||||||
|
"fdroid" to stringResource(R.string.update_source_fdroid),
|
||||||
|
)
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(R.string.update_source)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
sources.forEach { (value, label) ->
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { onSourceSelected(value) }
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = currentSource == value,
|
||||||
|
onClick = { onSourceSelected(value) },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UpdateTrackDialog(
|
private fun UpdateTrackDialog(
|
||||||
currentTrack: String,
|
currentTrack: String,
|
||||||
@@ -1108,6 +1360,15 @@ private fun LanguageDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculateDirSize(dir: File?): Long {
|
||||||
|
if (dir == null || !dir.exists()) return 0
|
||||||
|
var size = 0L
|
||||||
|
dir.listFiles()?.forEach { file ->
|
||||||
|
size += if (file.isDirectory) calculateDirSize(file) else file.length()
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSupportedLocales(context: Context): List<Locale> {
|
private fun getSupportedLocales(context: Context): List<Locale> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val localeConfig = LocaleConfig(context)
|
val localeConfig = LocaleConfig(context)
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
package io.nekohasekai.sfa.compose.screen.settings
|
||||||
|
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Speed
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import io.nekohasekai.libbox.Libbox
|
||||||
|
import io.nekohasekai.sfa.R
|
||||||
|
import io.nekohasekai.sfa.compose.topbar.OverrideTopBar
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
private data class MirrorEntry(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val country: String,
|
||||||
|
val isCustom: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FDroidMirrorScreen(navController: NavController) {
|
||||||
|
OverrideTopBar {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.fdroid_mirror)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.content_description_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var selectedMirrorUrl by remember { mutableStateOf(Settings.fdroidMirrorUrl) }
|
||||||
|
var isTesting by remember { mutableStateOf(false) }
|
||||||
|
val latencyResults = remember { mutableStateMapOf<String, Int>() }
|
||||||
|
val latencyErrors = remember { mutableStateMapOf<String, Boolean>() }
|
||||||
|
var showAddForm by remember { mutableStateOf(false) }
|
||||||
|
var newMirrorName by remember { mutableStateOf("") }
|
||||||
|
var newMirrorUrl by remember { mutableStateOf("") }
|
||||||
|
var urlError by remember { mutableStateOf<String?>(null) }
|
||||||
|
val invalidUrlMessage = stringResource(R.string.fdroid_mirror_invalid_url)
|
||||||
|
var customMirrors by remember { mutableStateOf(Settings.fdroidCustomMirrors) }
|
||||||
|
|
||||||
|
val builtinMirrors = remember {
|
||||||
|
val mirrors = mutableListOf<MirrorEntry>()
|
||||||
|
val iter = Libbox.getFDroidMirrors()
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
val m = iter.next()
|
||||||
|
mirrors.add(MirrorEntry(url = m.url, name = m.name, country = m.country))
|
||||||
|
}
|
||||||
|
mirrors
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsedCustomMirrors = remember(customMirrors) {
|
||||||
|
customMirrors.map { entry ->
|
||||||
|
val parts = entry.split("|", limit = 2)
|
||||||
|
if (parts.size == 2) {
|
||||||
|
MirrorEntry(url = parts[1], name = parts[0], country = "", isCustom = true)
|
||||||
|
} else {
|
||||||
|
MirrorEntry(url = entry, name = entry, country = "", isCustom = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val allMirrors = builtinMirrors + parsedCustomMirrors
|
||||||
|
|
||||||
|
fun selectMirror(url: String) {
|
||||||
|
selectedMirrorUrl = url
|
||||||
|
Settings.fdroidMirrorUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testAllMirrors() {
|
||||||
|
isTesting = true
|
||||||
|
latencyResults.clear()
|
||||||
|
latencyErrors.clear()
|
||||||
|
scope.launch {
|
||||||
|
allMirrors.map { mirror ->
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
val r = Libbox.pingFDroidMirror(mirror.url)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (r.latencyMs < 0) {
|
||||||
|
latencyErrors[r.url] = true
|
||||||
|
} else {
|
||||||
|
latencyResults[r.url] = r.latencyMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
val fastest = latencyResults.minByOrNull { it.value }
|
||||||
|
if (fastest != null) {
|
||||||
|
selectMirror(fastest.key)
|
||||||
|
}
|
||||||
|
isTesting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val grouped = remember(builtinMirrors) {
|
||||||
|
builtinMirrors.groupBy { it.country }
|
||||||
|
}
|
||||||
|
val countryOrder = remember(grouped) { grouped.keys.toList() }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { testAllMirrors() },
|
||||||
|
enabled = !isTesting,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
if (isTesting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.fdroid_mirror_testing))
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Speed,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.fdroid_mirror_test_all))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
countryOrder.forEach { country ->
|
||||||
|
val mirrors = grouped[country] ?: return@forEach
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = country,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
mirrors.forEachIndexed { index, mirror ->
|
||||||
|
val shape = when {
|
||||||
|
mirrors.size == 1 -> RoundedCornerShape(12.dp)
|
||||||
|
index == 0 -> RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
|
index == mirrors.lastIndex -> RoundedCornerShape(
|
||||||
|
bottomStart = 12.dp,
|
||||||
|
bottomEnd = 12.dp,
|
||||||
|
)
|
||||||
|
else -> RoundedCornerShape(0.dp)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
mirror.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = selectedMirrorUrl == mirror.url,
|
||||||
|
onClick = { selectMirror(mirror.url) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
LatencyBadge(
|
||||||
|
url = mirror.url,
|
||||||
|
latencyResults = latencyResults,
|
||||||
|
latencyErrors = latencyErrors,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape)
|
||||||
|
.clickable { selectMirror(mirror.url) },
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.fdroid_mirror_custom),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
parsedCustomMirrors.forEachIndexed { index, mirror ->
|
||||||
|
val isLast = index == parsedCustomMirrors.lastIndex && !showAddForm
|
||||||
|
val shape = when {
|
||||||
|
index == 0 && isLast -> RoundedCornerShape(12.dp)
|
||||||
|
index == 0 -> RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||||
|
isLast -> RoundedCornerShape(
|
||||||
|
bottomStart = 12.dp,
|
||||||
|
bottomEnd = 12.dp,
|
||||||
|
)
|
||||||
|
else -> RoundedCornerShape(0.dp)
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
mirror.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
mirror.url,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
RadioButton(
|
||||||
|
selected = selectedMirrorUrl == mirror.url,
|
||||||
|
onClick = { selectMirror(mirror.url) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
LatencyBadge(
|
||||||
|
url = mirror.url,
|
||||||
|
latencyResults = latencyResults,
|
||||||
|
latencyErrors = latencyErrors,
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
val encoded = "${mirror.name}|${mirror.url}"
|
||||||
|
val newSet = customMirrors.toMutableSet()
|
||||||
|
newSet.remove(encoded)
|
||||||
|
customMirrors = newSet
|
||||||
|
Settings.fdroidCustomMirrors = newSet
|
||||||
|
if (selectedMirrorUrl == mirror.url) {
|
||||||
|
selectMirror("https://f-droid.org/repo")
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = stringResource(R.string.fdroid_mirror_delete),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(shape)
|
||||||
|
.clickable { selectMirror(mirror.url) },
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddForm) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newMirrorName,
|
||||||
|
onValueChange = { newMirrorName = it },
|
||||||
|
label = { Text(stringResource(R.string.fdroid_mirror_name_hint)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newMirrorUrl,
|
||||||
|
onValueChange = {
|
||||||
|
newMirrorUrl = it
|
||||||
|
urlError = null
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.fdroid_mirror_url_hint)) },
|
||||||
|
singleLine = true,
|
||||||
|
isError = urlError != null,
|
||||||
|
supportingText = urlError?.let { { Text(it) } },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
val url = newMirrorUrl.trim().trimEnd('/')
|
||||||
|
if (!URLUtil.isHttpsUrl(url)) {
|
||||||
|
urlError = invalidUrlMessage
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
val name = newMirrorName.trim().ifEmpty { url }
|
||||||
|
val encoded = "$name|$url"
|
||||||
|
val newSet = customMirrors.toMutableSet()
|
||||||
|
newSet.add(encoded)
|
||||||
|
customMirrors = newSet
|
||||||
|
Settings.fdroidCustomMirrors = newSet
|
||||||
|
newMirrorName = ""
|
||||||
|
newMirrorUrl = ""
|
||||||
|
urlError = null
|
||||||
|
showAddForm = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.fdroid_mirror_add_action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.fdroid_mirror_add),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(
|
||||||
|
if (parsedCustomMirrors.isEmpty()) {
|
||||||
|
RoundedCornerShape(12.dp)
|
||||||
|
} else {
|
||||||
|
RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.clickable { showAddForm = true },
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LatencyBadge(
|
||||||
|
url: String,
|
||||||
|
latencyResults: Map<String, Int>,
|
||||||
|
latencyErrors: Map<String, Boolean>,
|
||||||
|
) {
|
||||||
|
val latency = latencyResults[url]
|
||||||
|
val failed = latencyErrors[url] == true
|
||||||
|
when {
|
||||||
|
latency != null -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.fdroid_mirror_latency, latency),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = when {
|
||||||
|
latency < 100 -> MaterialTheme.colorScheme.primary
|
||||||
|
latency < 500 -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
else -> MaterialTheme.colorScheme.error
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
failed -> {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.fdroid_mirror_failed),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@ object SettingsKey {
|
|||||||
const val SERVICE_MODE = "service_mode"
|
const val SERVICE_MODE = "service_mode"
|
||||||
const val CHECK_UPDATE_ENABLED = "check_update_enabled"
|
const val CHECK_UPDATE_ENABLED = "check_update_enabled"
|
||||||
const val UPDATE_CHECK_PROMPTED = "update_check_prompted"
|
const val UPDATE_CHECK_PROMPTED = "update_check_prompted"
|
||||||
|
const val UPDATE_SOURCE = "update_source"
|
||||||
const val UPDATE_TRACK = "update_track"
|
const val UPDATE_TRACK = "update_track"
|
||||||
|
const val FDROID_MIRROR_URL = "fdroid_mirror_url"
|
||||||
|
const val FDROID_CUSTOM_MIRRORS = "fdroid_custom_mirrors"
|
||||||
const val SILENT_INSTALL_ENABLED = "silent_install_enabled"
|
const val SILENT_INSTALL_ENABLED = "silent_install_enabled"
|
||||||
const val SILENT_INSTALL_METHOD = "silent_install_method"
|
const val SILENT_INSTALL_METHOD = "silent_install_method"
|
||||||
const val AUTO_UPDATE_ENABLED = "auto_update_enabled"
|
const val AUTO_UPDATE_ENABLED = "auto_update_enabled"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ object Settings {
|
|||||||
var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL }
|
var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL }
|
||||||
var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER)
|
var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER)
|
||||||
|
|
||||||
|
var updateSource by dataStore.string(SettingsKey.UPDATE_SOURCE) { "github" }
|
||||||
var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { false }
|
var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { false }
|
||||||
var updateCheckPrompted by dataStore.boolean(SettingsKey.UPDATE_CHECK_PROMPTED) { false }
|
var updateCheckPrompted by dataStore.boolean(SettingsKey.UPDATE_CHECK_PROMPTED) { false }
|
||||||
var updateTrack by dataStore.string(SettingsKey.UPDATE_TRACK) {
|
var updateTrack by dataStore.string(SettingsKey.UPDATE_TRACK) {
|
||||||
@@ -62,6 +63,8 @@ object Settings {
|
|||||||
"SHIZUKU"
|
"SHIZUKU"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var fdroidMirrorUrl by dataStore.string(SettingsKey.FDROID_MIRROR_URL) { "https://f-droid.org/repo" }
|
||||||
|
var fdroidCustomMirrors by dataStore.stringSet(SettingsKey.FDROID_CUSTOM_MIRRORS) { emptySet() }
|
||||||
var autoUpdateEnabled by dataStore.boolean(SettingsKey.AUTO_UPDATE_ENABLED) { false }
|
var autoUpdateEnabled by dataStore.boolean(SettingsKey.AUTO_UPDATE_ENABLED) { false }
|
||||||
var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true }
|
var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true }
|
||||||
var disableDeprecatedWarnings by dataStore.boolean(SettingsKey.DISABLE_DEPRECATED_WARNINGS) { false }
|
var disableDeprecatedWarnings by dataStore.boolean(SettingsKey.DISABLE_DEPRECATED_WARNINGS) { false }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package io.nekohasekai.sfa.update
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.nekohasekai.libbox.Libbox
|
||||||
|
import io.nekohasekai.sfa.database.Settings
|
||||||
|
|
||||||
|
fun checkFDroidUpdate(context: Context): UpdateInfo? {
|
||||||
|
val packageName = context.packageName
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val versionCode = context.packageManager.getPackageInfo(packageName, 0).versionCode
|
||||||
|
val result = Libbox.checkFDroidUpdate(
|
||||||
|
Settings.fdroidMirrorUrl,
|
||||||
|
packageName,
|
||||||
|
versionCode,
|
||||||
|
context.cacheDir.absolutePath,
|
||||||
|
) ?: return null
|
||||||
|
return UpdateInfo(
|
||||||
|
versionCode = result.versionCode,
|
||||||
|
versionName = result.versionName,
|
||||||
|
downloadUrl = result.downloadURL,
|
||||||
|
releaseUrl = "https://f-droid.org/packages/$packageName/",
|
||||||
|
releaseNotes = null,
|
||||||
|
isPrerelease = false,
|
||||||
|
fileSize = result.fileSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
14
app/src/main/java/io/nekohasekai/sfa/update/UpdateSource.kt
Normal file
14
app/src/main/java/io/nekohasekai/sfa/update/UpdateSource.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package io.nekohasekai.sfa.update
|
||||||
|
|
||||||
|
enum class UpdateSource {
|
||||||
|
GITHUB,
|
||||||
|
FDROID,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(value: String): UpdateSource = when (value.lowercase()) {
|
||||||
|
"fdroid" -> FDROID
|
||||||
|
else -> GITHUB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import androidx.camera.core.ImageAnalysis
|
import androidx.camera.core.ImageAnalysis
|
||||||
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
||||||
import io.nekohasekai.sfa.update.UpdateInfo
|
import io.nekohasekai.sfa.update.UpdateInfo
|
||||||
|
import io.nekohasekai.sfa.update.UpdateSource
|
||||||
|
|
||||||
interface VendorInterface {
|
interface VendorInterface {
|
||||||
fun checkUpdate(activity: Activity, byUser: Boolean)
|
fun checkUpdate(activity: Activity, byUser: Boolean)
|
||||||
@@ -14,53 +15,17 @@ interface VendorInterface {
|
|||||||
onCropArea: ((QRCodeCropArea?) -> Unit)? = null,
|
onCropArea: ((QRCodeCropArea?) -> Unit)? = null,
|
||||||
): ImageAnalysis.Analyzer?
|
): ImageAnalysis.Analyzer?
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Per-app Proxy feature is available
|
|
||||||
* @return true if available, false if disabled (e.g., for Play Store builds)
|
|
||||||
*/
|
|
||||||
fun isPerAppProxyAvailable(): Boolean = true
|
fun isPerAppProxyAvailable(): Boolean = true
|
||||||
|
|
||||||
/**
|
val hasCustomUpdate: Boolean get() = false
|
||||||
* Check if track selection is available (e.g., stable/beta)
|
|
||||||
* @return true if track selection is supported
|
val updateSources: List<UpdateSource> get() = listOf(UpdateSource.GITHUB)
|
||||||
*/
|
|
||||||
fun supportsTrackSelection(): Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for updates asynchronously
|
|
||||||
* @return UpdateInfo if update is available, null otherwise
|
|
||||||
*/
|
|
||||||
fun checkUpdateAsync(): UpdateInfo? = null
|
fun checkUpdateAsync(): UpdateInfo? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if silent install feature is available
|
|
||||||
* @return true if silent install is supported (Other flavor only)
|
|
||||||
*/
|
|
||||||
fun supportsSilentInstall(): Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if auto update feature is available
|
|
||||||
* @return true if auto update is supported (Other flavor only)
|
|
||||||
*/
|
|
||||||
fun supportsAutoUpdate(): Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule auto update worker
|
|
||||||
*/
|
|
||||||
fun scheduleAutoUpdate() {}
|
fun scheduleAutoUpdate() {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify if the specified silent install method is available
|
|
||||||
* @param method The install method (SHIZUKU or ROOT)
|
|
||||||
* @return true if the method is available and working
|
|
||||||
*/
|
|
||||||
suspend fun verifySilentInstallMethod(method: String): Boolean = false
|
suspend fun verifySilentInstallMethod(method: String): Boolean = false
|
||||||
|
|
||||||
/**
|
|
||||||
* Download and install an APK update
|
|
||||||
* @param context The context
|
|
||||||
* @param downloadUrl The URL to download the APK from
|
|
||||||
* @throws Exception if download or install fails
|
|
||||||
*/
|
|
||||||
suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String): Unit = throw UnsupportedOperationException("Not supported in this flavor")
|
suspend fun downloadAndInstall(context: android.content.Context, downloadUrl: String): Unit = throw UnsupportedOperationException("Not supported in this flavor")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,8 @@
|
|||||||
<string name="sponsor">赞助</string>
|
<string name="sponsor">赞助</string>
|
||||||
<string name="working_directory">工作目录</string>
|
<string name="working_directory">工作目录</string>
|
||||||
<string name="disable_deprecated_warnings">禁用弃用警告</string>
|
<string name="disable_deprecated_warnings">禁用弃用警告</string>
|
||||||
|
<string name="cache_size">缓存大小</string>
|
||||||
|
<string name="clear_cache">清除缓存</string>
|
||||||
<string name="notification_settings">通知</string>
|
<string name="notification_settings">通知</string>
|
||||||
<string name="enable_notification">启用通知</string>
|
<string name="enable_notification">启用通知</string>
|
||||||
<string name="dynamic_notification">在通知中显示实时网速</string>
|
<string name="dynamic_notification">在通知中显示实时网速</string>
|
||||||
@@ -275,6 +277,22 @@
|
|||||||
<string name="new_version_available">有新版本可用:%s</string>
|
<string name="new_version_available">有新版本可用:%s</string>
|
||||||
<string name="auto_update">自动更新</string>
|
<string name="auto_update">自动更新</string>
|
||||||
<string name="auto_update_description">在后台自动下载和安装更新</string>
|
<string name="auto_update_description">在后台自动下载和安装更新</string>
|
||||||
|
<string name="update_source">更新来源</string>
|
||||||
|
<string name="update_source_github">GitHub</string>
|
||||||
|
<string name="update_source_fdroid">F-Droid</string>
|
||||||
|
<string name="fdroid_mirror">F-Droid 镜像</string>
|
||||||
|
<string name="fdroid_mirror_test_all">根据延迟自动选择</string>
|
||||||
|
<string name="fdroid_mirror_testing">测试中…</string>
|
||||||
|
<string name="fdroid_mirror_latency">%d ms</string>
|
||||||
|
<string name="fdroid_mirror_failed">失败</string>
|
||||||
|
<string name="fdroid_mirror_untested">—</string>
|
||||||
|
<string name="fdroid_mirror_add">添加镜像</string>
|
||||||
|
<string name="fdroid_mirror_name_hint">名称</string>
|
||||||
|
<string name="fdroid_mirror_url_hint">URL</string>
|
||||||
|
<string name="fdroid_mirror_custom">自定义</string>
|
||||||
|
<string name="fdroid_mirror_invalid_url">无效的 URL</string>
|
||||||
|
<string name="fdroid_mirror_add_action">添加</string>
|
||||||
|
<string name="fdroid_mirror_delete">删除</string>
|
||||||
|
|
||||||
<!-- Silent Install -->
|
<!-- Silent Install -->
|
||||||
<string name="silent_install">静默安装</string>
|
<string name="silent_install">静默安装</string>
|
||||||
|
|||||||
@@ -199,6 +199,8 @@
|
|||||||
<string name="sponsor">贊助</string>
|
<string name="sponsor">贊助</string>
|
||||||
<string name="working_directory">工作目錄</string>
|
<string name="working_directory">工作目錄</string>
|
||||||
<string name="disable_deprecated_warnings">停用過時警告</string>
|
<string name="disable_deprecated_warnings">停用過時警告</string>
|
||||||
|
<string name="cache_size">快取大小</string>
|
||||||
|
<string name="clear_cache">清除快取</string>
|
||||||
<string name="notification_settings">通知</string>
|
<string name="notification_settings">通知</string>
|
||||||
<string name="enable_notification">啟用通知</string>
|
<string name="enable_notification">啟用通知</string>
|
||||||
<string name="dynamic_notification">在通知中顯示即時網速</string>
|
<string name="dynamic_notification">在通知中顯示即時網速</string>
|
||||||
@@ -275,6 +277,22 @@
|
|||||||
<string name="new_version_available">有新版本可用:%s</string>
|
<string name="new_version_available">有新版本可用:%s</string>
|
||||||
<string name="auto_update">自動更新</string>
|
<string name="auto_update">自動更新</string>
|
||||||
<string name="auto_update_description">在背景自動下載並安裝更新</string>
|
<string name="auto_update_description">在背景自動下載並安裝更新</string>
|
||||||
|
<string name="update_source">更新來源</string>
|
||||||
|
<string name="update_source_github">GitHub</string>
|
||||||
|
<string name="update_source_fdroid">F-Droid</string>
|
||||||
|
<string name="fdroid_mirror">F-Droid 鏡像</string>
|
||||||
|
<string name="fdroid_mirror_test_all">依延遲自動選擇</string>
|
||||||
|
<string name="fdroid_mirror_testing">測試中…</string>
|
||||||
|
<string name="fdroid_mirror_latency">%d ms</string>
|
||||||
|
<string name="fdroid_mirror_failed">失敗</string>
|
||||||
|
<string name="fdroid_mirror_untested">—</string>
|
||||||
|
<string name="fdroid_mirror_add">新增鏡像</string>
|
||||||
|
<string name="fdroid_mirror_name_hint">名稱</string>
|
||||||
|
<string name="fdroid_mirror_url_hint">URL</string>
|
||||||
|
<string name="fdroid_mirror_custom">自訂</string>
|
||||||
|
<string name="fdroid_mirror_invalid_url">無效的 URL</string>
|
||||||
|
<string name="fdroid_mirror_add_action">新增</string>
|
||||||
|
<string name="fdroid_mirror_delete">刪除</string>
|
||||||
|
|
||||||
<!-- Silent Install -->
|
<!-- Silent Install -->
|
||||||
<string name="silent_install">靜默安裝</string>
|
<string name="silent_install">靜默安裝</string>
|
||||||
|
|||||||
@@ -199,6 +199,8 @@
|
|||||||
<string name="sponsor">Sponsor</string>
|
<string name="sponsor">Sponsor</string>
|
||||||
<string name="working_directory">Working Directory</string>
|
<string name="working_directory">Working Directory</string>
|
||||||
<string name="disable_deprecated_warnings">Disable Deprecated Warnings</string>
|
<string name="disable_deprecated_warnings">Disable Deprecated Warnings</string>
|
||||||
|
<string name="cache_size">Cache Size</string>
|
||||||
|
<string name="clear_cache">Clear Cache</string>
|
||||||
<string name="notification_settings">Notification</string>
|
<string name="notification_settings">Notification</string>
|
||||||
<string name="enable_notification">Enable Notification</string>
|
<string name="enable_notification">Enable Notification</string>
|
||||||
<string name="dynamic_notification">Display realtime speed in notification</string>
|
<string name="dynamic_notification">Display realtime speed in notification</string>
|
||||||
@@ -264,6 +266,9 @@
|
|||||||
<string name="check_update_automatic">Automatic Update Check</string>
|
<string name="check_update_automatic">Automatic Update Check</string>
|
||||||
<string name="check_update_prompt_play">Would you like to enable automatic update checking from **Play Store**?</string>
|
<string name="check_update_prompt_play">Would you like to enable automatic update checking from **Play Store**?</string>
|
||||||
<string name="check_update_prompt_github">Would you like to enable automatic update checking from **GitHub**?</string>
|
<string name="check_update_prompt_github">Would you like to enable automatic update checking from **GitHub**?</string>
|
||||||
|
<string name="update_source">Update Source</string>
|
||||||
|
<string name="update_source_github">GitHub</string>
|
||||||
|
<string name="update_source_fdroid">F-Droid</string>
|
||||||
<string name="update_track">Update Track</string>
|
<string name="update_track">Update Track</string>
|
||||||
<string name="update_track_stable">Stable</string>
|
<string name="update_track_stable">Stable</string>
|
||||||
<string name="update_track_beta">Beta</string>
|
<string name="update_track_beta">Beta</string>
|
||||||
@@ -275,6 +280,19 @@
|
|||||||
<string name="new_version_available">New version available: %s</string>
|
<string name="new_version_available">New version available: %s</string>
|
||||||
<string name="auto_update">Auto Update</string>
|
<string name="auto_update">Auto Update</string>
|
||||||
<string name="auto_update_description">Automatically download and install updates in background</string>
|
<string name="auto_update_description">Automatically download and install updates in background</string>
|
||||||
|
<string name="fdroid_mirror">F-Droid Mirror</string>
|
||||||
|
<string name="fdroid_mirror_test_all">Auto Select by Latency</string>
|
||||||
|
<string name="fdroid_mirror_testing">Testing…</string>
|
||||||
|
<string name="fdroid_mirror_latency">%d ms</string>
|
||||||
|
<string name="fdroid_mirror_failed">Failed</string>
|
||||||
|
<string name="fdroid_mirror_untested">—</string>
|
||||||
|
<string name="fdroid_mirror_add">Add Mirror</string>
|
||||||
|
<string name="fdroid_mirror_name_hint">Name</string>
|
||||||
|
<string name="fdroid_mirror_url_hint">URL</string>
|
||||||
|
<string name="fdroid_mirror_custom">Custom</string>
|
||||||
|
<string name="fdroid_mirror_invalid_url">Invalid URL</string>
|
||||||
|
<string name="fdroid_mirror_add_action">Add</string>
|
||||||
|
<string name="fdroid_mirror_delete">Delete</string>
|
||||||
|
|
||||||
<!-- Silent Install -->
|
<!-- Silent Install -->
|
||||||
<string name="silent_install">Silent Install</string>
|
<string name="silent_install">Silent Install</string>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
<cache-path
|
<external-files-path
|
||||||
name="cache"
|
name="external_files"
|
||||||
path="/" />
|
path="/" />
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import io.nekohasekai.sfa.compose.screen.qrscan.QRCodeCropArea
|
|||||||
import io.nekohasekai.sfa.database.Settings
|
import io.nekohasekai.sfa.database.Settings
|
||||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||||
import io.nekohasekai.sfa.update.UpdateInfo
|
import io.nekohasekai.sfa.update.UpdateInfo
|
||||||
|
import io.nekohasekai.sfa.update.UpdateSource
|
||||||
import io.nekohasekai.sfa.update.UpdateState
|
import io.nekohasekai.sfa.update.UpdateState
|
||||||
import io.nekohasekai.sfa.update.UpdateTrack
|
import io.nekohasekai.sfa.update.UpdateTrack
|
||||||
|
import io.nekohasekai.sfa.update.checkFDroidUpdate
|
||||||
|
|
||||||
object Vendor : VendorInterface {
|
object Vendor : VendorInterface {
|
||||||
private const val TAG = "Vendor"
|
private const val TAG = "Vendor"
|
||||||
@@ -93,18 +95,19 @@ object Vendor : VendorInterface {
|
|||||||
onCropArea: ((QRCodeCropArea?) -> Unit)?,
|
onCropArea: ((QRCodeCropArea?) -> Unit)?,
|
||||||
): ImageAnalysis.Analyzer? = null
|
): ImageAnalysis.Analyzer? = null
|
||||||
|
|
||||||
override fun supportsTrackSelection(): Boolean = true
|
override val hasCustomUpdate = true
|
||||||
|
|
||||||
override fun checkUpdateAsync(): UpdateInfo? {
|
override val updateSources = listOf(UpdateSource.GITHUB, UpdateSource.FDROID)
|
||||||
|
|
||||||
|
override fun checkUpdateAsync(): UpdateInfo? = when (UpdateSource.fromString(Settings.updateSource)) {
|
||||||
|
UpdateSource.FDROID -> checkFDroidUpdate(Application.application)
|
||||||
|
UpdateSource.GITHUB -> {
|
||||||
val track = UpdateTrack.fromString(Settings.updateTrack)
|
val track = UpdateTrack.fromString(Settings.updateTrack)
|
||||||
return GitHubUpdateChecker().use { checker ->
|
GitHubUpdateChecker().use { checker ->
|
||||||
checker.checkUpdate(track)
|
checker.checkUpdate(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun supportsSilentInstall(): Boolean = true
|
|
||||||
|
|
||||||
override fun supportsAutoUpdate(): Boolean = true
|
|
||||||
|
|
||||||
override fun scheduleAutoUpdate() {
|
override fun scheduleAutoUpdate() {
|
||||||
UpdateWorker.schedule(io.nekohasekai.sfa.Application.application)
|
UpdateWorker.schedule(io.nekohasekai.sfa.Application.application)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ object Vendor : VendorInterface {
|
|||||||
onCropArea: ((QRCodeCropArea?) -> Unit)?,
|
onCropArea: ((QRCodeCropArea?) -> Unit)?,
|
||||||
): ImageAnalysis.Analyzer? = null
|
): ImageAnalysis.Analyzer? = null
|
||||||
|
|
||||||
override fun supportsTrackSelection(): Boolean = true
|
override val hasCustomUpdate = true
|
||||||
|
|
||||||
override fun checkUpdateAsync(): UpdateInfo? {
|
override fun checkUpdateAsync(): UpdateInfo? {
|
||||||
val track = UpdateTrack.fromString(Settings.updateTrack)
|
val track = UpdateTrack.fromString(Settings.updateTrack)
|
||||||
@@ -102,10 +102,6 @@ object Vendor : VendorInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun supportsSilentInstall(): Boolean = true
|
|
||||||
|
|
||||||
override fun supportsAutoUpdate(): Boolean = true
|
|
||||||
|
|
||||||
override fun scheduleAutoUpdate() {
|
override fun scheduleAutoUpdate() {
|
||||||
UpdateWorker.schedule(io.nekohasekai.sfa.Application.application)
|
UpdateWorker.schedule(io.nekohasekai.sfa.Application.application)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,5 @@ object Vendor : VendorInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun supportsTrackSelection(): Boolean = false
|
|
||||||
|
|
||||||
override fun checkUpdateAsync(): UpdateInfo? = null
|
override fun checkUpdateAsync(): UpdateInfo? = null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user