OXYGENHUB
# c53e328b2db4 oxygenhub-android / app / src / main / java / io / oxygenhub / android / update / AppUpdate.kt
3.8 KiB · last changed Panels you can see: the hub's palette by default, clear surface steps and a 1 dp edge on every panel (closes #24) 7 days ago source blame history raw
1
package io.oxygenhub.android.update
2
3
import androidx.compose.foundation.layout.fillMaxWidth
4
import androidx.compose.material.icons.Icons
5
import androidx.compose.material.icons.outlined.SystemUpdate
6
import androidx.compose.material3.Icon
7
import androidx.compose.material3.ListItem
8
import androidx.compose.material3.Text
9
import androidx.compose.material3.TextButton
10
import androidx.compose.runtime.Composable
11
import androidx.compose.runtime.getValue
12
import androidx.compose.runtime.produceState
13
import androidx.compose.runtime.remember
14
import androidx.compose.ui.Modifier
15
import androidx.compose.ui.platform.LocalContext
16
import androidx.compose.ui.platform.LocalUriHandler
17
import io.oxygenhub.android.api.HubApi
18
import io.oxygenhub.android.api.Release
19
import io.oxygenhub.android.ui.common.Panel
20
21
/** Where the app's own releases are (#7), built and signed by OxygenFlow. */
22
const val APP_HUB = "https://oxygenhub.io"
23
const val APP_OWNER = "andymccall"
24
const val APP_REPO = "oxygenhub-android"
25
26
/** `v1.2.3` or `1.2.3` as numbers; null for anything else (a pre-release such as `1.2.3-rc1`). */
27
fun versionOf(text: String): List<Int>? {
28
    val parts = text.trim().removePrefix("v").split(".")
29
    if (parts.size != 3) return null
30
    return parts.map { p -> p.toIntOrNull()?.takeIf { it >= 0 } ?: return null }
31
}
32
33
/** Whether [candidate] is a later version than [installed]. */
34
fun isNewer(candidate: String, installed: String): Boolean {
35
    val a = versionOf(candidate) ?: return false
36
    val b = versionOf(installed) ?: return true
37
    return a.zip(b).firstOrNull { (x, y) -> x != y }?.let { (x, y) -> x > y } ?: false
38
}
39
40
/** An update: the newest full release later than [installed], with its APK's address. */
41
data class Update(val version: String, val apkUrl: String, val pageUrl: String)
42
43
/** The newest full release of [releases] later than [installed] that carries an APK, if any. */
44
fun updateFrom(releases: List<Release>, installed: String, hub: String = APP_HUB): Update? {
45
    val newest = releases
46
        .filter { !it.prerelease && versionOf(it.tag) != null }
47
        .maxWithOrNull { a, b -> if (isNewer(a.tag, b.tag)) 1 else if (isNewer(b.tag, a.tag)) -1 else 0 }
48
        ?: return null
49
    if (!isNewer(newest.tag, installed)) return null
50
    val apk = newest.assets.firstOrNull { it.name.endsWith(".apk") } ?: return null
51
    fun absolute(u: String) = if (u.startsWith("http")) u else hub.trimEnd('/') + "/" + u.trimStart('/')
52
    val page = newest.url.ifEmpty { "/$APP_OWNER/$APP_REPO/releases" }
53
    return Update(newest.tag.removePrefix("v"), absolute(apk.url), absolute(page))
54
}
55
56
/**
57
 * The app's version and, when the hub signed in to is [APP_HUB] and it has
58
 * a later release, a link to its APK: Android installs it from the
59
 * download, as the same key signs every release.
60
 */
61
@Composable
62
fun UpdateCard(api: HubApi, hub: String) {
63
    val context = LocalContext.current
64
    val installed = remember(context) { context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "" }
65
    val update by produceState<Update?>(null, hub) {
66
        if (hub.trimEnd('/') == APP_HUB) {
67
            value = runCatching { updateFrom(api.releases(APP_OWNER, APP_REPO), installed) }.getOrNull()
68
        }
69
    }
70
    val uri = LocalUriHandler.current
71
    Panel(Modifier.fillMaxWidth()) {
72
        ListItem(
73
            leadingContent = { Icon(Icons.Outlined.SystemUpdate, contentDescription = null) },
74
            headlineContent = { Text(update?.let { "Version ${it.version} is out" } ?: "OxygenHub $installed") },
75
            supportingContent = {
76
                Text(if (update != null) "You have $installed. Download it and open the file to update." else "Releases are at $APP_HUB/$APP_OWNER/$APP_REPO.")
77
            },
78
            trailingContent = update?.let { u -> { TextButton(onClick = { uri.openUri(u.apkUrl) }) { Text("Download") } } },
79
        )
80
    }
81
}