Rail shows only root spaces + in-app updater; bump to 1.0.6
Sonar / Sonar Quality Checks (push) Has been cancelled
Post-release / post-release (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Sonar / Sonar Quality Checks (push) Has been cancelled
Post-release / post-release (push) Has been cancelled
Code Quality Checks / Search for forbidden patterns (push) Has been cancelled
Code Quality Checks / Search for invalid screenshot files (push) Has been cancelled
Code Quality Checks / Search for invalid dependencies (push) Has been cancelled
Code Quality Checks / Konsist tests (push) Has been cancelled
Code Quality Checks / Compose tests (push) Has been cancelled
Code Quality Checks / Android lint check (push) Has been cancelled
Code Quality Checks / Detekt checks (push) Has been cancelled
Code Quality Checks / Ktlint checks (push) Has been cancelled
Code Quality Checks / Doc checks (push) Has been cancelled
Code Quality Checks / Check shell scripts (push) Has been cancelled
Code Quality Checks / Run zizmor (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle (Gplay) (push) Has been cancelled
Create release App Bundle and APKs / Create App Bundle Enterprise (push) Has been cancelled
Create release App Bundle and APKs / Create APKs (FDroid) (push) Has been cancelled
Test / Runs unit tests (push) Has been cancelled
Code Quality Checks / Project Check Suite (push) Has been cancelled
Rail: desktop parity (element-web rootSpaces) — spaces nested inside another joined space no longer get a rail entry; their rooms stay reachable through the parent space. Nested set persisted for clean cold starts. Updater: new libraries/blapupdate module checks the git.utn.lol release feed (6h throttle), offers a banner at the top of the chat list, downloads the ABI-matched APK and hands it to the system installer. Per-version dismissal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 Blap
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.blapupdate"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp.okhttp)
|
||||
|
||||
implementation(libs.serialization.json)
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Blap
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.blapupdate
|
||||
|
||||
import android.os.Build
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
|
||||
data class BlapUpdateInfo(
|
||||
val version: String,
|
||||
val apkUrl: String,
|
||||
)
|
||||
|
||||
fun interface BlapUpdateChecker {
|
||||
/** Returns the newer release to offer, or null when up to date (or on any error). */
|
||||
suspend fun check(): BlapUpdateInfo?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultBlapUpdateChecker(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : BlapUpdateChecker {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
override suspend fun check(): BlapUpdateInfo? = withContext(coroutineDispatchers.io) {
|
||||
val release = fetchLatestRelease() ?: return@withContext null
|
||||
val remoteVersion = release.tagName.removePrefix("v")
|
||||
val localVersion = buildMeta.versionName.substringBefore(" ")
|
||||
if (!isNewer(remote = remoteVersion, local = localVersion)) return@withContext null
|
||||
// Prefer the APK matching the device's primary ABI, fall back to universal.
|
||||
val abi = Build.SUPPORTED_ABIS.firstOrNull().orEmpty()
|
||||
val asset = release.assets.firstOrNull { abi.isNotEmpty() && it.name.contains(abi) }
|
||||
?: release.assets.firstOrNull { it.name.contains("universal") }
|
||||
?: return@withContext null
|
||||
BlapUpdateInfo(version = remoteVersion, apkUrl = asset.downloadUrl)
|
||||
}
|
||||
|
||||
private fun fetchLatestRelease(): GiteaRelease? {
|
||||
return runCatching {
|
||||
val request = Request.Builder().url(BlapUpdateConfig.LATEST_RELEASE_URL).build()
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) return null
|
||||
json.decodeFromString<GiteaRelease>(response.body?.string().orEmpty())
|
||||
}
|
||||
}
|
||||
.onFailure { Timber.w("Update check failed: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/** Numeric semver compare; malformed versions are never treated as newer. */
|
||||
private fun isNewer(remote: String, local: String): Boolean {
|
||||
val remoteParts = remote.split(".").map { it.toIntOrNull() ?: return false }
|
||||
val localParts = local.split(".").map { it.toIntOrNull() ?: return false }
|
||||
for (i in 0 until maxOf(remoteParts.size, localParts.size)) {
|
||||
val r = remoteParts.getOrElse(i) { 0 }
|
||||
val l = localParts.getOrElse(i) { 0 }
|
||||
if (r != l) return r > l
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class GiteaRelease(
|
||||
@SerialName("tag_name") val tagName: String,
|
||||
@SerialName("assets") val assets: List<GiteaAsset> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class GiteaAsset(
|
||||
@SerialName("name") val name: String,
|
||||
@SerialName("browser_download_url") val downloadUrl: String,
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Blap
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.blapupdate
|
||||
|
||||
object BlapUpdateConfig {
|
||||
/** Public Gitea API — same feed Obtainium and the desktop updater use. */
|
||||
const val LATEST_RELEASE_URL: String = "https://git.utn.lol/api/v1/repos/enki/blap-android/releases/latest"
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Blap
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.blapupdate
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
fun interface BlapUpdateInstaller {
|
||||
/**
|
||||
* Downloads the APK to cache and hands it to the system installer.
|
||||
* The user still confirms the install (and, once, allows Blap to install apps).
|
||||
*/
|
||||
suspend fun downloadAndInstall(info: BlapUpdateInfo): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultBlapUpdateInstaller(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : BlapUpdateInstaller {
|
||||
override suspend fun downloadAndInstall(info: BlapUpdateInfo): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val dir = File(context.cacheDir, "blap-update").apply { mkdirs() }
|
||||
// Only ever keep the APK being installed right now.
|
||||
dir.listFiles()?.forEach { it.delete() }
|
||||
val apk = File(dir, "blap-${info.version}.apk")
|
||||
val request = Request.Builder().url(info.apkUrl).build()
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
check(response.isSuccessful) { "Download failed: HTTP ${response.code}" }
|
||||
val body = checkNotNull(response.body) { "Empty download response" }
|
||||
apk.outputStream().use { output -> body.byteStream().copyTo(output) }
|
||||
}
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", apk)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}.onFailure { Timber.w(it, "Update install failed") }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user