Add F-Droid as update check sources

This commit is contained in:
世界
2026-03-11 16:45:27 +08:00
parent 0d1ee7aa80
commit 868c1de2ff
16 changed files with 874 additions and 76 deletions

View File

@@ -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")

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 -> {}
}
}

View File

@@ -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"

View File

@@ -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 }

View File

@@ -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,
)
}

View 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
}
}
}

View File

@@ -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")
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -92,7 +92,5 @@ object Vendor : VendorInterface {
} }
} }
override fun supportsTrackSelection(): Boolean = false
override fun checkUpdateAsync(): UpdateInfo? = null override fun checkUpdateAsync(): UpdateInfo? = null
} }