Add app settings with update track and auto-check options
- Add AppSettingsScreen with update track selection and auto-check toggle - Remove checkUpdateAvailable() as all vendors now support update checking - Add missing Chinese translations for update-related strings
This commit is contained in:
@@ -6,6 +6,7 @@ plugins {
|
||||
id "kotlin-parcelize"
|
||||
id "com.google.devtools.ksp"
|
||||
id "org.jetbrains.kotlin.plugin.compose"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.github.triplet.play"
|
||||
id "org.jlleitschuh.gradle.ktlint"
|
||||
}
|
||||
@@ -121,6 +122,7 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:2.10.3"
|
||||
implementation "androidx.browser:browser:1.9.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
|
||||
|
||||
// DO NOT UPDATE (minSdkVersion updated)
|
||||
implementation "com.blacksquircle.ui:editorkit:2.2.0"
|
||||
|
||||
@@ -21,6 +21,8 @@ import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -58,6 +60,7 @@ import io.nekohasekai.sfa.bg.ServiceConnection
|
||||
import io.nekohasekai.sfa.bg.ServiceNotification
|
||||
import io.nekohasekai.sfa.compose.base.GlobalEventBus
|
||||
import io.nekohasekai.sfa.compose.base.UiEvent
|
||||
import io.nekohasekai.sfa.compose.component.UpdateAvailableDialog
|
||||
import io.nekohasekai.sfa.compose.navigation.SFANavHost
|
||||
import io.nekohasekai.sfa.compose.navigation.Screen
|
||||
import io.nekohasekai.sfa.compose.navigation.bottomNavigationScreens
|
||||
@@ -71,6 +74,8 @@ import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.ktx.hasPermission
|
||||
import io.nekohasekai.sfa.ktx.launchCustomTab
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -129,6 +134,16 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
|
||||
connection.reconnect()
|
||||
|
||||
if (Settings.checkUpdateEnabled) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val updateInfo = Vendor.checkUpdateAsync()
|
||||
UpdateState.setUpdate(updateInfo)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
SFATheme {
|
||||
SFAApp()
|
||||
@@ -236,6 +251,16 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
}, onDismiss = { showBackgroundLocationDialog = false })
|
||||
}
|
||||
|
||||
// Handle update available dialog
|
||||
val updateInfo by UpdateState.updateInfo
|
||||
var showUpdateDialog by remember { mutableStateOf(true) }
|
||||
if (showUpdateDialog && updateInfo != null) {
|
||||
UpdateAvailableDialog(
|
||||
updateInfo = updateInfo!!,
|
||||
onDismiss = { showUpdateDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
// Initialize the dashboard view model and store reference
|
||||
val dashboardViewModel: DashboardViewModel = viewModel()
|
||||
if (!::dashboardViewModel.isInitialized) {
|
||||
@@ -253,6 +278,7 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
val isSettingsSubScreen = currentDestination?.route?.startsWith("settings/") == true
|
||||
val settingsScreenTitle =
|
||||
when (currentDestination?.route) {
|
||||
"settings/app" -> stringResource(R.string.title_app_settings)
|
||||
"settings/core" -> stringResource(R.string.core)
|
||||
"settings/service" -> stringResource(R.string.service)
|
||||
"settings/profile_override" -> stringResource(R.string.profile_override)
|
||||
@@ -443,10 +469,19 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
bottomBar = {
|
||||
// Only show bottom bar when not in settings sub-screens
|
||||
if (!isSettingsSubScreen) {
|
||||
val hasUpdate by UpdateState.hasUpdate
|
||||
NavigationBar {
|
||||
bottomNavigationScreens.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(screen.icon, contentDescription = null) },
|
||||
icon = {
|
||||
if (screen == Screen.Settings && hasUpdate) {
|
||||
BadgedBox(badge = { Badge() }) {
|
||||
Icon(screen.icon, contentDescription = null)
|
||||
}
|
||||
} else {
|
||||
Icon(screen.icon, contentDescription = null)
|
||||
}
|
||||
},
|
||||
selected =
|
||||
currentDestination?.hierarchy?.any {
|
||||
it.route == screen.route
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.nekohasekai.sfa.compose.component
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
|
||||
@Composable
|
||||
fun UpdateAvailableDialog(
|
||||
updateInfo: UpdateInfo,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.check_update)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.new_version_available, updateInfo.versionName),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
if (!updateInfo.releaseNotes.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = updateInfo.releaseNotes.take(500) +
|
||||
if (updateInfo.releaseNotes.length > 500) "..." else "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(updateInfo.releaseUrl))
|
||||
context.startActivity(intent)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.update))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import io.nekohasekai.sfa.compose.screen.dashboard.DashboardScreen
|
||||
import io.nekohasekai.sfa.compose.screen.dashboard.DashboardViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.log.LogScreen
|
||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||
import io.nekohasekai.sfa.compose.screen.settings.AppSettingsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.CoreSettingsScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.ProfileOverrideScreen
|
||||
import io.nekohasekai.sfa.compose.screen.settings.ServiceSettingsScreen
|
||||
@@ -57,6 +58,36 @@ fun SFANavHost(
|
||||
}
|
||||
|
||||
// Settings subscreens with slide animations
|
||||
composable(
|
||||
route = "settings/app",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = tween(300),
|
||||
)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = tween(300),
|
||||
)
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = tween(300),
|
||||
)
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = tween(300),
|
||||
)
|
||||
},
|
||||
) {
|
||||
AppSettingsScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable(
|
||||
route = "settings/core",
|
||||
enterTransition = {
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package io.nekohasekai.sfa.compose.screen.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Autorenew
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.NewReleases
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import io.nekohasekai.sfa.vendor.Vendor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSettingsScreen(navController: NavController) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val hasUpdate by UpdateState.hasUpdate
|
||||
val updateInfo by UpdateState.updateInfo
|
||||
val isChecking by UpdateState.isChecking
|
||||
var showTrackDialog by remember { mutableStateOf(false) }
|
||||
var currentTrack by remember { mutableStateOf(Settings.updateTrack) }
|
||||
var checkUpdateEnabled by remember { mutableStateOf(Settings.checkUpdateEnabled) }
|
||||
var showErrorDialog by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
if (showTrackDialog) {
|
||||
UpdateTrackDialog(
|
||||
currentTrack = currentTrack,
|
||||
onTrackSelected = { track ->
|
||||
currentTrack = track
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.updateTrack = track
|
||||
}
|
||||
showTrackDialog = false
|
||||
},
|
||||
onDismiss = { showTrackDialog = false },
|
||||
)
|
||||
}
|
||||
|
||||
showErrorDialog?.let { messageRes ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { showErrorDialog = null },
|
||||
title = { Text(stringResource(R.string.check_update)) },
|
||||
text = { Text(stringResource(messageRes)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showErrorDialog = null }) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
// Info Card
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.app_version_title),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
BuildConfig.VERSION_NAME,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (hasUpdate) {
|
||||
Badge { Text("New") }
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (Vendor.supportsTrackSelection()) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.update_track),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
val trackName = when (UpdateTrack.fromString(currentTrack)) {
|
||||
UpdateTrack.STABLE -> stringResource(R.string.update_track_stable)
|
||||
UpdateTrack.BETA -> stringResource(R.string.update_track_beta)
|
||||
}
|
||||
Text(trackName, style = MaterialTheme.typography.bodyMedium)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.NewReleases,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clickable { showTrackDialog = true },
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.check_update_automatic),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Autorenew,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checkUpdateEnabled,
|
||||
onCheckedChange = { checked ->
|
||||
checkUpdateEnabled = checked
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Settings.checkUpdateEnabled = checked
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)),
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Action Section
|
||||
Text(
|
||||
text = stringResource(R.string.action),
|
||||
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 {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.check_update),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (isChecking) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(
|
||||
if (hasUpdate) {
|
||||
RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)
|
||||
} else {
|
||||
RoundedCornerShape(12.dp)
|
||||
},
|
||||
)
|
||||
.clickable(enabled = !isChecking) {
|
||||
scope.launch {
|
||||
UpdateState.isChecking.value = true
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val result = Vendor.checkUpdateAsync()
|
||||
UpdateState.setUpdate(result)
|
||||
if (result == null) {
|
||||
showErrorDialog = R.string.no_updates_available
|
||||
}
|
||||
} catch (_: UpdateCheckException.TrackNotSupported) {
|
||||
showErrorDialog = R.string.update_track_not_supported
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
UpdateState.isChecking.value = false
|
||||
}
|
||||
},
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
if (hasUpdate && updateInfo != null) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.update),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
updateInfo!!.versionName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||
.clickable {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(updateInfo!!.releaseUrl),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
},
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateTrackDialog(
|
||||
currentTrack: String,
|
||||
onTrackSelected: (String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val tracks = listOf(
|
||||
"stable" to stringResource(R.string.update_track_stable),
|
||||
"beta" to stringResource(R.string.update_track_beta),
|
||||
)
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.update_track)) },
|
||||
text = {
|
||||
Column {
|
||||
tracks.forEach { (value, label) ->
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { onTrackSelected(value) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
RadioButton(
|
||||
selected = currentTrack == value,
|
||||
onClick = { onTrackSelected(value) },
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -17,9 +17,11 @@ import androidx.compose.material.icons.outlined.Code
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material.icons.outlined.Favorite
|
||||
import androidx.compose.material.icons.outlined.FilterAlt
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SwapHoriz
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -29,6 +31,7 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -40,6 +43,7 @@ import androidx.navigation.NavController
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -48,6 +52,7 @@ import kotlinx.coroutines.launch
|
||||
fun SettingsScreen(navController: NavController) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val hasUpdate by UpdateState.hasUpdate
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
@@ -69,6 +74,35 @@ fun SettingsScreen(navController: NavController) {
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.title_app_settings),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (hasUpdate) {
|
||||
Badge()
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||
.clickable { navController.navigate("settings/app") },
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
@@ -85,7 +119,6 @@ fun SettingsScreen(navController: NavController) {
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
|
||||
.clickable { navController.navigate("settings/core") },
|
||||
colors =
|
||||
ListItemDefaults.colors(
|
||||
|
||||
@@ -4,6 +4,7 @@ object SettingsKey {
|
||||
const val SELECTED_PROFILE = "selected_profile"
|
||||
const val SERVICE_MODE = "service_mode"
|
||||
const val CHECK_UPDATE_ENABLED = "check_update_enabled"
|
||||
const val UPDATE_TRACK = "update_track"
|
||||
const val DISABLE_MEMORY_LIMIT = "disable_memory_limit"
|
||||
const val DYNAMIC_NOTIFICATION = "dynamic_notification"
|
||||
const val USE_COMPOSE_UI = "use_compose_ui"
|
||||
|
||||
@@ -40,6 +40,7 @@ object Settings {
|
||||
var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER)
|
||||
|
||||
var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { true }
|
||||
var updateTrack by dataStore.string(SettingsKey.UPDATE_TRACK) { "stable" }
|
||||
var disableMemoryLimit by dataStore.boolean(SettingsKey.DISABLE_MEMORY_LIMIT)
|
||||
var dynamicNotification by dataStore.boolean(SettingsKey.DYNAMIC_NOTIFICATION) { true }
|
||||
var useComposeUI by dataStore.boolean(SettingsKey.USE_COMPOSE_UI) { true }
|
||||
|
||||
@@ -209,14 +209,12 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun startIntegration() {
|
||||
if (Vendor.checkUpdateAvailable()) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (Settings.checkUpdateEnabled) {
|
||||
Vendor.checkUpdate(this@MainActivity, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun startService() {
|
||||
|
||||
@@ -82,10 +82,6 @@ class SettingsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Vendor.checkUpdateAvailable()) {
|
||||
binding.checkUpdateEnabled.isVisible = false
|
||||
binding.checkUpdateButton.isVisible = false
|
||||
}
|
||||
binding.checkUpdateEnabled.addTextChangedListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val newValue = EnabledType.valueOf(requireContext(), it).boolValue
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.nekohasekai.sfa.update
|
||||
|
||||
sealed class UpdateCheckException : Exception() {
|
||||
class TrackNotSupported : UpdateCheckException()
|
||||
}
|
||||
10
app/src/main/java/io/nekohasekai/sfa/update/UpdateInfo.kt
Normal file
10
app/src/main/java/io/nekohasekai/sfa/update/UpdateInfo.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package io.nekohasekai.sfa.update
|
||||
|
||||
data class UpdateInfo(
|
||||
val versionCode: Int,
|
||||
val versionName: String,
|
||||
val downloadUrl: String,
|
||||
val releaseUrl: String,
|
||||
val releaseNotes: String?,
|
||||
val isPrerelease: Boolean,
|
||||
)
|
||||
19
app/src/main/java/io/nekohasekai/sfa/update/UpdateState.kt
Normal file
19
app/src/main/java/io/nekohasekai/sfa/update/UpdateState.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package io.nekohasekai.sfa.update
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
object UpdateState {
|
||||
val hasUpdate = mutableStateOf(false)
|
||||
val updateInfo = mutableStateOf<UpdateInfo?>(null)
|
||||
val isChecking = mutableStateOf(false)
|
||||
|
||||
fun setUpdate(info: UpdateInfo?) {
|
||||
updateInfo.value = info
|
||||
hasUpdate.value = info != null
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
hasUpdate.value = false
|
||||
updateInfo.value = null
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt
Normal file
15
app/src/main/java/io/nekohasekai/sfa/update/UpdateTrack.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package io.nekohasekai.sfa.update
|
||||
|
||||
enum class UpdateTrack {
|
||||
STABLE,
|
||||
BETA;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): UpdateTrack {
|
||||
return when (value.lowercase()) {
|
||||
"beta" -> BETA
|
||||
else -> STABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,9 @@ package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
|
||||
interface VendorInterface {
|
||||
fun checkUpdateAvailable(): Boolean
|
||||
|
||||
fun checkUpdate(
|
||||
activity: Activity,
|
||||
byUser: Boolean,
|
||||
@@ -21,4 +20,16 @@ interface VendorInterface {
|
||||
* @return true if available, false if disabled (e.g., for Play Store builds)
|
||||
*/
|
||||
fun isPerAppProxyAvailable(): Boolean = true
|
||||
|
||||
/**
|
||||
* Check if track selection is available (e.g., stable/beta)
|
||||
* @return true if track selection is supported
|
||||
*/
|
||||
fun supportsTrackSelection(): Boolean = false
|
||||
|
||||
/**
|
||||
* Check for updates asynchronously
|
||||
* @return UpdateInfo if update is available, null otherwise
|
||||
*/
|
||||
fun checkUpdateAsync(): UpdateInfo? = null
|
||||
}
|
||||
|
||||
@@ -86,6 +86,15 @@
|
||||
<string name="check_update_automatic">自动检查更新</string>
|
||||
<string name="check_update">检查更新</string>
|
||||
<string name="no_updates_available">没有可用的更新</string>
|
||||
<string name="new_version_available">有新版本可用:%s</string>
|
||||
<string name="update">更新</string>
|
||||
<string name="update_track">更新轨道</string>
|
||||
<string name="update_track_stable">稳定版</string>
|
||||
<string name="update_track_beta">测试版</string>
|
||||
<string name="update_track_not_supported">当前轨道尚不支持检查更新</string>
|
||||
<string name="app_version">版本 %s</string>
|
||||
<string name="app_version_title">应用版本</string>
|
||||
<string name="action">操作</string>
|
||||
<string name="privacy_policy">隐私政策</string>
|
||||
<string name="title_app_settings">应用</string>
|
||||
<string name="memory_limit">内存限制</string>
|
||||
@@ -225,6 +234,7 @@
|
||||
<string name="update_profile">更新配置文件</string>
|
||||
<string name="more_options">更多选项</string>
|
||||
<string name="edit">编辑</string>
|
||||
<string name="done">完成</string>
|
||||
<string name="save_as_file">另存为文件</string>
|
||||
<string name="share_as_file">分享为文件</string>
|
||||
<string name="service">服务</string>
|
||||
|
||||
@@ -128,6 +128,15 @@
|
||||
<string name="check_update_automatic">Automatic Update Check</string>
|
||||
<string name="check_update">Check Update</string>
|
||||
<string name="no_updates_available">No updates available</string>
|
||||
<string name="new_version_available">New version available: %s</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="update_track">Update Track</string>
|
||||
<string name="update_track_stable">Stable</string>
|
||||
<string name="update_track_beta">Beta</string>
|
||||
<string name="update_track_not_supported">Current track does not support update checking yet</string>
|
||||
<string name="app_version">Version %s</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="action">Action</string>
|
||||
<string name="privacy_policy">Privacy Policy</string>
|
||||
<string name="title_app_settings">App</string>
|
||||
<string name="memory_limit">Memory Limit</string>
|
||||
|
||||
115
app/src/other/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt
vendored
Normal file
115
app/src/other/java/io/nekohasekai/sfa/vendor/GitHubUpdateChecker.kt
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.sfa.BuildConfig
|
||||
import io.nekohasekai.sfa.ktx.unwrap
|
||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
import io.nekohasekai.sfa.utils.HTTPClient
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.Closeable
|
||||
|
||||
class GitHubUpdateChecker : Closeable {
|
||||
companion object {
|
||||
private const val RELEASES_URL = "https://api.github.com/repos/SagerNet/sing-box/releases"
|
||||
private const val METADATA_FILENAME = "SFA-version-metadata.json"
|
||||
}
|
||||
|
||||
private val client = Libbox.newHTTPClient().apply {
|
||||
modernTLS()
|
||||
keepAlive()
|
||||
}
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun checkUpdate(track: UpdateTrack): UpdateInfo? {
|
||||
val includePrerelease = track == UpdateTrack.BETA
|
||||
val release = getLatestRelease(includePrerelease) ?: return null
|
||||
|
||||
if (!release.assets.any { it.name == METADATA_FILENAME }) {
|
||||
throw UpdateCheckException.TrackNotSupported()
|
||||
}
|
||||
|
||||
val metadata = downloadMetadata(release)!!
|
||||
|
||||
if (metadata.versionCode <= BuildConfig.VERSION_CODE) {
|
||||
return null
|
||||
}
|
||||
|
||||
val apkAsset = release.assets.find { asset ->
|
||||
asset.name.endsWith(".apk") && !asset.name.contains("play")
|
||||
}
|
||||
|
||||
return UpdateInfo(
|
||||
versionCode = metadata.versionCode,
|
||||
versionName = metadata.versionName,
|
||||
downloadUrl = apkAsset?.browserDownloadUrl ?: release.htmlUrl,
|
||||
releaseUrl = release.htmlUrl,
|
||||
releaseNotes = release.body,
|
||||
isPrerelease = release.prerelease,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLatestRelease(includePrerelease: Boolean): GitHubRelease? {
|
||||
val request = client.newRequest()
|
||||
request.setURL(RELEASES_URL)
|
||||
request.setHeader("Accept", "application/vnd.github.v3+json")
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
|
||||
val response = request.execute()
|
||||
val content = response.content.unwrap
|
||||
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(content)
|
||||
|
||||
return if (includePrerelease) {
|
||||
releases.firstOrNull()
|
||||
} else {
|
||||
releases.firstOrNull { !it.prerelease && !it.draft }
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadMetadata(release: GitHubRelease): VersionMetadata? {
|
||||
val metadataAsset = release.assets.find { it.name == METADATA_FILENAME }
|
||||
?: return null
|
||||
|
||||
val request = client.newRequest()
|
||||
request.setURL(metadataAsset.browserDownloadUrl)
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
|
||||
val response = request.execute()
|
||||
val content = response.content.unwrap
|
||||
|
||||
return json.decodeFromString<VersionMetadata>(content)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
client.close()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@SerialName("tag_name") val tagName: String = "",
|
||||
val name: String = "",
|
||||
val body: String? = null,
|
||||
val draft: Boolean = false,
|
||||
val prerelease: Boolean = false,
|
||||
@SerialName("html_url") val htmlUrl: String = "",
|
||||
val assets: List<GitHubAsset> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GitHubAsset(
|
||||
val name: String = "",
|
||||
@SerialName("browser_download_url") val browserDownloadUrl: String = "",
|
||||
val size: Long = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VersionMetadata(
|
||||
@SerialName("version_code") val versionCode: Int = 0,
|
||||
@SerialName("version_name") val versionName: String = "",
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,89 @@
|
||||
package io.nekohasekai.sfa.vendor
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.database.Settings
|
||||
import io.nekohasekai.sfa.update.UpdateCheckException
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
import io.nekohasekai.sfa.update.UpdateTrack
|
||||
|
||||
object Vendor : VendorInterface {
|
||||
override fun checkUpdateAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
private const val TAG = "Vendor"
|
||||
|
||||
override fun checkUpdate(
|
||||
activity: Activity,
|
||||
byUser: Boolean,
|
||||
) {
|
||||
try {
|
||||
val updateInfo = checkUpdateAsync()
|
||||
if (updateInfo != null) {
|
||||
activity.runOnUiThread {
|
||||
showUpdateDialog(activity, updateInfo)
|
||||
}
|
||||
} else if (byUser) {
|
||||
activity.runOnUiThread {
|
||||
showNoUpdatesDialog(activity)
|
||||
}
|
||||
}
|
||||
} catch (e: UpdateCheckException.TrackNotSupported) {
|
||||
Log.d(TAG, "checkUpdate: track not supported")
|
||||
if (byUser) {
|
||||
activity.runOnUiThread {
|
||||
showTrackNotSupportedDialog(activity)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "checkUpdate: ", e)
|
||||
if (byUser) {
|
||||
activity.runOnUiThread {
|
||||
showNoUpdatesDialog(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(activity: Activity, updateInfo: UpdateInfo) {
|
||||
val message = buildString {
|
||||
append(activity.getString(R.string.new_version_available, updateInfo.versionName))
|
||||
if (!updateInfo.releaseNotes.isNullOrBlank()) {
|
||||
append("\n\n")
|
||||
append(updateInfo.releaseNotes.take(500))
|
||||
if (updateInfo.releaseNotes.length > 500) {
|
||||
append("...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.check_update)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.update) { _, _ ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(updateInfo.releaseUrl))
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showNoUpdatesDialog(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.check_update)
|
||||
.setMessage(R.string.no_updates_available)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showTrackNotSupportedDialog(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.check_update)
|
||||
.setMessage(R.string.update_track_not_supported)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun createQRCodeAnalyzer(
|
||||
@@ -22,7 +94,17 @@ object Vendor : VendorInterface {
|
||||
}
|
||||
|
||||
override fun isPerAppProxyAvailable(): Boolean {
|
||||
// Per-app Proxy is available for non-Play Store builds
|
||||
return true
|
||||
}
|
||||
|
||||
override fun supportsTrackSelection(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun checkUpdateAsync(): UpdateInfo? {
|
||||
val track = UpdateTrack.fromString(Settings.updateTrack)
|
||||
return GitHubUpdateChecker().use { checker ->
|
||||
checker.checkUpdate(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,12 @@ import com.google.android.play.core.install.model.InstallStatus
|
||||
import com.google.android.play.core.install.model.UpdateAvailability
|
||||
import com.google.mlkit.common.MlKitException
|
||||
import io.nekohasekai.sfa.R
|
||||
import io.nekohasekai.sfa.update.UpdateInfo
|
||||
import io.nekohasekai.sfa.update.UpdateState
|
||||
|
||||
object Vendor : VendorInterface {
|
||||
private const val TAG = "Vendor"
|
||||
|
||||
override fun checkUpdateAvailable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun checkUpdate(
|
||||
activity: Activity,
|
||||
byUser: Boolean,
|
||||
@@ -30,6 +28,7 @@ object Vendor : VendorInterface {
|
||||
when (appUpdateInfo.updateAvailability()) {
|
||||
UpdateAvailability.UPDATE_NOT_AVAILABLE -> {
|
||||
Log.d(TAG, "checkUpdate: not available")
|
||||
UpdateState.clear()
|
||||
if (byUser) activity.showNoUpdatesDialog()
|
||||
}
|
||||
|
||||
@@ -44,6 +43,7 @@ object Vendor : VendorInterface {
|
||||
|
||||
UpdateAvailability.UPDATE_AVAILABLE -> {
|
||||
Log.d(TAG, "checkUpdate: available")
|
||||
UpdateState.hasUpdate.value = true
|
||||
if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
|
||||
appUpdateManager.startUpdateFlow(
|
||||
appUpdateInfo,
|
||||
@@ -97,4 +97,15 @@ object Vendor : VendorInterface {
|
||||
// Per-app Proxy is disabled for Play Store builds due to QUERY_ALL_PACKAGES permission restriction
|
||||
return false
|
||||
}
|
||||
|
||||
override fun supportsTrackSelection(): Boolean {
|
||||
// Play Store doesn't support track selection
|
||||
return false
|
||||
}
|
||||
|
||||
override fun checkUpdateAsync(): UpdateInfo? {
|
||||
// Play Store updates are handled by the Play Core library
|
||||
// We can't get version info in the same way as GitHub
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ plugins {
|
||||
id 'com.google.devtools.ksp' version '2.2.0-2.0.2' apply false
|
||||
id 'com.github.triplet.play' version '3.12.1' apply false
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.0' apply false
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.0' apply false
|
||||
id 'org.jlleitschuh.gradle.ktlint' version '13.1.0' apply false
|
||||
id 'io.gitlab.arturbosch.detekt' version '1.23.8'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user