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:
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user