mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Add zapstore self-updater for free flavor
- ZapstoreUpdater subscribes to wss://relay.zapstore.dev for kind 30063 release events and kind 1063 file metadata events published by the developer. It compares the incoming version against the running BuildConfig.VERSION_NAME and exposes latestRelease as a StateFlow. - When an update is found the updater downloads the APK via OkHttp, verifies the SHA-256 hash from the event tags, and installs it via Android PackageInstaller intent using a FileProvider URI. - SettingsScreen shows a "Check for updates" row that changes to "Update available: vX.Y.Z" when a newer version is detected, with download progress and error state feedback. - All self-update code is gated behind !BuildFlavorChecker.isOfflineFlavor() so F-Droid (offline) builds are completely unaffected. - Added REQUEST_INSTALL_PACKAGES permission and FileProvider declaration to AndroidManifest.xml, and the file_provider_paths.xml resource. https://claude.ai/code/session_01NmFcCFmEbY7NMradkEU5bv
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Amber"
|
android:name=".Amber"
|
||||||
@@ -58,6 +59,16 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
tools:ignore="ExportedContentProvider" />
|
tools:ignore="ExportedContentProvider" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import com.greenart7c3.nostrsigner.service.ConnectivityService
|
|||||||
import com.greenart7c3.nostrsigner.service.NotificationSubscription
|
import com.greenart7c3.nostrsigner.service.NotificationSubscription
|
||||||
import com.greenart7c3.nostrsigner.service.ProfileSubscription
|
import com.greenart7c3.nostrsigner.service.ProfileSubscription
|
||||||
import com.greenart7c3.nostrsigner.service.TorManager
|
import com.greenart7c3.nostrsigner.service.TorManager
|
||||||
|
import com.greenart7c3.nostrsigner.service.ZapstoreUpdater
|
||||||
import com.greenart7c3.nostrsigner.service.crashreports.CrashReportCache
|
import com.greenart7c3.nostrsigner.service.crashreports.CrashReportCache
|
||||||
import com.greenart7c3.nostrsigner.service.crashreports.UnexpectedCrashSaver
|
import com.greenart7c3.nostrsigner.service.crashreports.UnexpectedCrashSaver
|
||||||
import com.greenart7c3.nostrsigner.ui.ToastManager
|
import com.greenart7c3.nostrsigner.ui.ToastManager
|
||||||
@@ -131,6 +132,12 @@ class Amber :
|
|||||||
// This runs on the foreground only
|
// This runs on the foreground only
|
||||||
val profileSubscription = ProfileSubscription(client, this, applicationIOScope)
|
val profileSubscription = ProfileSubscription(client, this, applicationIOScope)
|
||||||
|
|
||||||
|
val zapstoreUpdater: ZapstoreUpdater? = if (!BuildFlavorChecker.isOfflineFlavor()) {
|
||||||
|
ZapstoreUpdater(client, applicationIOScope)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
private var databases = ConcurrentHashMap<String, AppDatabase>()
|
private var databases = ConcurrentHashMap<String, AppDatabase>()
|
||||||
private var logDatabases = ConcurrentHashMap<String, LogDatabase>()
|
private var logDatabases = ConcurrentHashMap<String, LogDatabase>()
|
||||||
private var historyDatabases = ConcurrentHashMap<String, HistoryDatabase>()
|
private var historyDatabases = ConcurrentHashMap<String, HistoryDatabase>()
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.service
|
||||||
|
|
||||||
|
data class ZapstoreRelease(
|
||||||
|
val version: String,
|
||||||
|
val url: String,
|
||||||
|
val hash: String?,
|
||||||
|
val createdAt: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.greenart7c3.nostrsigner.Amber
|
||||||
|
import com.greenart7c3.nostrsigner.BuildConfig
|
||||||
|
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||||
|
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Request
|
||||||
|
|
||||||
|
private const val EOSE_TIMEOUT_MS = 15_000L
|
||||||
|
private const val RELEASE_KIND = 30063
|
||||||
|
private const val FILE_METADATA_KIND = 1063
|
||||||
|
private const val ZAPSTORE_RELAY = "wss://relay.zapstore.dev"
|
||||||
|
private const val APP_IDENTIFIER = "com.greenart7c3.nostrsigner"
|
||||||
|
|
||||||
|
enum class DownloadState {
|
||||||
|
IDLE,
|
||||||
|
DOWNLOADING,
|
||||||
|
INSTALLING,
|
||||||
|
ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZapstoreUpdater(
|
||||||
|
private val client: NostrClient,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) : IRelayClientListener {
|
||||||
|
val latestRelease = MutableStateFlow<ZapstoreRelease?>(null)
|
||||||
|
val isChecking = MutableStateFlow(false)
|
||||||
|
val downloadState = MutableStateFlow(DownloadState.IDLE)
|
||||||
|
val downloadProgress = MutableStateFlow(0f)
|
||||||
|
|
||||||
|
private val releaseSubId = UUID.randomUUID().toString()
|
||||||
|
private val fileSubId = UUID.randomUUID().toString()
|
||||||
|
private val pendingFileEventIds = mutableListOf<String>()
|
||||||
|
private val versionByEventId = mutableMapOf<String, Pair<String, Long>>()
|
||||||
|
private val zapstoreRelay: NormalizedRelayUrl? = RelayUrlNormalizer.normalizeOrNull(ZAPSTORE_RELAY)
|
||||||
|
private var timeoutJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
client.subscribe(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIncomingMessage(relay: IRelayClient, msgStr: String, msg: Message) {
|
||||||
|
when {
|
||||||
|
msg is EventMessage && msg.subId == releaseSubId -> processReleaseEvent(msg.event)
|
||||||
|
msg is EventMessage && msg.subId == fileSubId -> processFileEvent(msg.event)
|
||||||
|
msg is EoseMessage && msg.subId == releaseSubId -> onReleaseEose()
|
||||||
|
msg is EoseMessage && msg.subId == fileSubId -> finishCheck()
|
||||||
|
}
|
||||||
|
super.onIncomingMessage(relay, msgStr, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkForUpdates() {
|
||||||
|
if (BuildFlavorChecker.isOfflineFlavor()) return
|
||||||
|
val relay = zapstoreRelay ?: return
|
||||||
|
if (isChecking.value) return
|
||||||
|
|
||||||
|
isChecking.value = true
|
||||||
|
pendingFileEventIds.clear()
|
||||||
|
versionByEventId.clear()
|
||||||
|
latestRelease.value = null
|
||||||
|
|
||||||
|
// Subscribe to release events (kind 30063) from the developer
|
||||||
|
val releaseFilter = Filter(
|
||||||
|
kinds = listOf(RELEASE_KIND),
|
||||||
|
authors = listOf(Amber.DEVELOPER_HEX_KEY),
|
||||||
|
limit = 10,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.openReqSubscription(releaseSubId, mapOf(relay to listOf(releaseFilter)))
|
||||||
|
|
||||||
|
timeoutJob = scope.launch {
|
||||||
|
delay(EOSE_TIMEOUT_MS)
|
||||||
|
Log.w(Amber.TAG, "ZapstoreUpdater: timeout waiting for EOSE")
|
||||||
|
finishCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processReleaseEvent(event: com.vitorpamplona.quartz.nip01Core.core.Event) {
|
||||||
|
val tags = event.tags
|
||||||
|
|
||||||
|
// Only process events for this app
|
||||||
|
val identifier = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.getOrNull(1) ?: ""
|
||||||
|
val appId = tags.firstOrNull { it.size > 1 && it[0] == "i" }?.getOrNull(1) ?: ""
|
||||||
|
val isForThisApp = identifier.startsWith(APP_IDENTIFIER) || appId == APP_IDENTIFIER
|
||||||
|
if (!isForThisApp) return
|
||||||
|
|
||||||
|
val version = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.getOrNull(1) ?: return
|
||||||
|
if (!isNewerVersion(version)) return
|
||||||
|
|
||||||
|
Log.d(Amber.TAG, "ZapstoreUpdater: found release $version (current: ${BuildConfig.VERSION_NAME})")
|
||||||
|
|
||||||
|
// Try direct URL first (some release events include it)
|
||||||
|
val directUrl = tags.firstOrNull { it.size > 1 && it[0] == "url" }?.getOrNull(1)
|
||||||
|
if (directUrl != null) {
|
||||||
|
val hash = tags.firstOrNull { it.size > 1 && (it[0] == "x" || it[0] == "hash") }?.getOrNull(1)
|
||||||
|
updateLatestRelease(version, directUrl, hash, event.createdAt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect file event IDs from e-tags for second subscription
|
||||||
|
tags.filter { it.size > 1 && it[0] == "e" }.forEach { tag ->
|
||||||
|
val eventId = tag[1]
|
||||||
|
pendingFileEventIds.add(eventId)
|
||||||
|
versionByEventId[eventId] = Pair(version, event.createdAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReleaseEose() {
|
||||||
|
if (pendingFileEventIds.isNotEmpty()) {
|
||||||
|
fetchFileEvents()
|
||||||
|
} else {
|
||||||
|
finishCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchFileEvents() {
|
||||||
|
val relay = zapstoreRelay ?: run {
|
||||||
|
finishCheck()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val fileFilter = Filter(
|
||||||
|
kinds = listOf(FILE_METADATA_KIND),
|
||||||
|
ids = pendingFileEventIds.toList(),
|
||||||
|
)
|
||||||
|
client.openReqSubscription(fileSubId, mapOf(relay to listOf(fileFilter)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processFileEvent(event: com.vitorpamplona.quartz.nip01Core.core.Event) {
|
||||||
|
val tags = event.tags
|
||||||
|
val url = tags.firstOrNull { it.size > 1 && it[0] == "url" }?.getOrNull(1) ?: return
|
||||||
|
val mimeType = tags.firstOrNull { it.size > 1 && it[0] == "m" }?.getOrNull(1)
|
||||||
|
val platform = tags.firstOrNull { it.size > 1 && it[0] == "platform" }?.getOrNull(1)
|
||||||
|
|
||||||
|
// Only process Android APK files
|
||||||
|
val isApk = mimeType?.contains("android") == true || url.endsWith(".apk")
|
||||||
|
val isAndroid = platform == null || platform == "android"
|
||||||
|
if (!isApk || !isAndroid) return
|
||||||
|
|
||||||
|
// Skip ABI-specific builds that don't match the running device ABI (prefer universal)
|
||||||
|
val arch = tags.firstOrNull { it.size > 1 && it[0] == "arch" }?.getOrNull(1)
|
||||||
|
if (arch != null && arch != "universal" && !isMatchingArch(arch)) return
|
||||||
|
|
||||||
|
val hash = tags.firstOrNull { it.size > 1 && it[0] == "x" }?.getOrNull(1)
|
||||||
|
val versionInfo = versionByEventId[event.id] ?: return
|
||||||
|
val (version, createdAt) = versionInfo
|
||||||
|
|
||||||
|
updateLatestRelease(version, url, hash, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isMatchingArch(arch: String): Boolean {
|
||||||
|
val supportedAbis = android.os.Build.SUPPORTED_ABIS.toList()
|
||||||
|
return supportedAbis.any { it.equals(arch, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLatestRelease(version: String, url: String, hash: String?, createdAt: Long) {
|
||||||
|
val current = latestRelease.value
|
||||||
|
if (current == null || createdAt > current.createdAt) {
|
||||||
|
latestRelease.value = ZapstoreRelease(version = version, url = url, hash = hash, createdAt = createdAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishCheck() {
|
||||||
|
timeoutJob?.cancel()
|
||||||
|
timeoutJob = null
|
||||||
|
Amber.instance.intentionalDisconnectTime = System.currentTimeMillis()
|
||||||
|
client.close(releaseSubId)
|
||||||
|
client.close(fileSubId)
|
||||||
|
isChecking.value = false
|
||||||
|
pendingFileEventIds.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadAndInstall(context: Context, release: ZapstoreRelease) {
|
||||||
|
if (BuildFlavorChecker.isOfflineFlavor()) return
|
||||||
|
if (downloadState.value == DownloadState.DOWNLOADING) return
|
||||||
|
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
downloadState.value = DownloadState.DOWNLOADING
|
||||||
|
downloadProgress.value = 0f
|
||||||
|
try {
|
||||||
|
val apkFile = downloadApk(context, release)
|
||||||
|
if (apkFile != null) {
|
||||||
|
downloadState.value = DownloadState.INSTALLING
|
||||||
|
installApk(context, apkFile)
|
||||||
|
} else {
|
||||||
|
downloadState.value = DownloadState.ERROR
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
Log.e(Amber.TAG, "ZapstoreUpdater: download failed", e)
|
||||||
|
downloadState.value = DownloadState.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadApk(context: Context, release: ZapstoreRelease): File? {
|
||||||
|
val useProxy = Amber.instance.settings.torMode != com.greenart7c3.nostrsigner.models.TorMode.DISABLED
|
||||||
|
val client = HttpClientManager.getHttpClient(useProxy)
|
||||||
|
val request = Request.Builder().url(release.url).build()
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
Log.e(Amber.TAG, "ZapstoreUpdater: HTTP ${response.code} for ${release.url}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = response.body ?: return null
|
||||||
|
val contentLength = body.contentLength()
|
||||||
|
val apkFile = File(context.cacheDir, "amber-update-${release.version}.apk")
|
||||||
|
|
||||||
|
val digest = if (release.hash != null) MessageDigest.getInstance("SHA-256") else null
|
||||||
|
var bytesRead = 0L
|
||||||
|
|
||||||
|
apkFile.outputStream().use { output ->
|
||||||
|
body.byteStream().use { input ->
|
||||||
|
val buffer = ByteArray(8 * 1024)
|
||||||
|
var read: Int
|
||||||
|
while (input.read(buffer).also { read = it } != -1) {
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
digest?.update(buffer, 0, read)
|
||||||
|
bytesRead += read
|
||||||
|
if (contentLength > 0) {
|
||||||
|
downloadProgress.value = bytesRead.toFloat() / contentLength.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digest != null && release.hash != null) {
|
||||||
|
val actualHash = digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
val expectedHash = release.hash.removePrefix("sha256:")
|
||||||
|
if (!actualHash.equals(expectedHash, ignoreCase = true)) {
|
||||||
|
Log.e(Amber.TAG, "ZapstoreUpdater: hash mismatch! expected=$expectedHash actual=$actualHash")
|
||||||
|
apkFile.delete()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
Log.d(Amber.TAG, "ZapstoreUpdater: hash verified OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
return apkFile
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installApk(context: Context, apkFile: File) {
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${context.packageName}.fileprovider",
|
||||||
|
apkFile,
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
delay(2000)
|
||||||
|
downloadState.value = DownloadState.IDLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNewerVersion(version: String): Boolean {
|
||||||
|
val current = BuildConfig.VERSION_NAME.substringBefore("-")
|
||||||
|
if (version == current) return false
|
||||||
|
return try {
|
||||||
|
val newParts = version.substringBefore("-").split(".").map { it.toInt() }
|
||||||
|
val curParts = current.split(".").map { it.toInt() }
|
||||||
|
for (i in 0 until maxOf(newParts.size, curParts.size)) {
|
||||||
|
val n = newParts.getOrElse(i) { 0 }
|
||||||
|
val c = curParts.getOrElse(i) { 0 }
|
||||||
|
if (n > c) return true
|
||||||
|
if (n < c) return false
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} catch (_: Exception) {
|
||||||
|
version > current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,9 @@ import androidx.compose.material.icons.filled.Key
|
|||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
import androidx.compose.material.icons.filled.SaveAlt
|
import androidx.compose.material.icons.filled.SaveAlt
|
||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.SystemUpdate
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -48,6 +50,7 @@ import androidx.compose.ui.text.withLink
|
|||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.greenart7c3.nostrsigner.Amber
|
import com.greenart7c3.nostrsigner.Amber
|
||||||
import com.greenart7c3.nostrsigner.BuildConfig
|
import com.greenart7c3.nostrsigner.BuildConfig
|
||||||
@@ -56,6 +59,7 @@ import com.greenart7c3.nostrsigner.LocalPreferences
|
|||||||
import com.greenart7c3.nostrsigner.R
|
import com.greenart7c3.nostrsigner.R
|
||||||
import com.greenart7c3.nostrsigner.models.Account
|
import com.greenart7c3.nostrsigner.models.Account
|
||||||
import com.greenart7c3.nostrsigner.models.TorMode
|
import com.greenart7c3.nostrsigner.models.TorMode
|
||||||
|
import com.greenart7c3.nostrsigner.service.DownloadState
|
||||||
import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog
|
import com.greenart7c3.nostrsigner.ui.actions.LogoutDialog
|
||||||
import com.greenart7c3.nostrsigner.ui.components.AmberButton
|
import com.greenart7c3.nostrsigner.ui.components.AmberButton
|
||||||
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
import com.greenart7c3.nostrsigner.ui.components.IconRow
|
||||||
@@ -270,6 +274,59 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!BuildFlavorChecker.isOfflineFlavor()) {
|
||||||
|
val updater = Amber.instance.zapstoreUpdater
|
||||||
|
if (updater != null) {
|
||||||
|
val latestRelease by updater.latestRelease.collectAsStateWithLifecycle()
|
||||||
|
val isChecking by updater.isChecking.collectAsStateWithLifecycle()
|
||||||
|
val dlState by updater.downloadState.collectAsStateWithLifecycle()
|
||||||
|
val dlProgress by updater.downloadProgress.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
Box(Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Column {
|
||||||
|
IconRow(
|
||||||
|
title = when {
|
||||||
|
latestRelease != null -> stringResource(R.string.update_available, latestRelease!!.version)
|
||||||
|
isChecking -> stringResource(R.string.checking_for_updates)
|
||||||
|
else -> stringResource(R.string.check_for_updates)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.SystemUpdate,
|
||||||
|
tint = if (latestRelease != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground,
|
||||||
|
onClick = {
|
||||||
|
if (latestRelease != null && dlState == DownloadState.IDLE) {
|
||||||
|
updater.downloadAndInstall(context, latestRelease!!)
|
||||||
|
} else if (!isChecking) {
|
||||||
|
updater.checkForUpdates()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (dlState == DownloadState.DOWNLOADING) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { dlProgress },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (dlState == DownloadState.INSTALLING) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.installing_update),
|
||||||
|
modifier = Modifier.padding(start = 54.dp),
|
||||||
|
color = Color.Gray,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (dlState == DownloadState.ERROR) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.update_download_failed),
|
||||||
|
modifier = Modifier.padding(start = 54.dp),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
val primaryColor = MaterialTheme.colorScheme.primary
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|||||||
@@ -561,6 +561,11 @@
|
|||||||
<string name="connected_app_warning">Some applications may not work if you try to reuse your bunker URI. Delete this application and create a new one in case the login is not working.</string>
|
<string name="connected_app_warning">Some applications may not work if you try to reuse your bunker URI. Delete this application and create a new one in case the login is not working.</string>
|
||||||
<string name="zapstore">Zapstore</string>
|
<string name="zapstore">Zapstore</string>
|
||||||
<string name="zapstore_website">https://zapstore.dev</string>
|
<string name="zapstore_website">https://zapstore.dev</string>
|
||||||
|
<string name="check_for_updates">Check for updates</string>
|
||||||
|
<string name="checking_for_updates">Checking for updates…</string>
|
||||||
|
<string name="update_available">Update available: v%1$s</string>
|
||||||
|
<string name="installing_update">Installing update…</string>
|
||||||
|
<string name="update_download_failed">Download failed. Tap to retry.</string>
|
||||||
<string name="discover_more">Discover more Nostr apps for android at</string>
|
<string name="discover_more">Discover more Nostr apps for android at</string>
|
||||||
<string name="backup">Backup</string>
|
<string name="backup">Backup</string>
|
||||||
<string name="qrCode">QR Code</string>
|
<string name="qrCode">QR Code</string>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path
|
||||||
|
name="apk_downloads"
|
||||||
|
path="." />
|
||||||
|
</paths>
|
||||||
Reference in New Issue
Block a user