diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 188b7020..a9d0f11e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -576,7 +576,7 @@ sealed interface AppPreference { val UpdateUrl = AppStringPreference( title = R.string.update_url, - defaultValue = "https://git.justinvisser.org/api/v1/repos/justinzeus/wolfskin/releases/latest", + defaultValue = "https://api.github.com/repos/damontecres/Wholphin/releases/latest", getter = { it.updateUrl }, setter = { prefs, value -> prefs.update { updateUrl = value } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt index 706285f2..a00de8e4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/UpdateChecker.kt @@ -57,7 +57,7 @@ class UpdateChecker @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { companion object { - const val ASSET_NAME = "Wolfskin" + const val ASSET_NAME = "Wholphin" const val APK_NAME = "$ASSET_NAME.apk" private const val APK_MIME_TYPE = "application/vnd.android.package-archive" @@ -65,53 +65,6 @@ class UpdateChecker private val NOTE_REGEX = Regex("") val ACTIVE = BuildConfig.UPDATING_ENABLED - - private const val RELEASES_URL = "https://git.justinvisser.org/api/v1/repos/justinzeus/wolfskin/releases?limit=50" - - /** - * Find the release in a Forgejo release list whose name parses to the given version - */ - fun findReleaseByVersion( - releases: JsonArray, - version: Version, - ): JsonObject? = - releases - .firstOrNull { release -> - val candidate = Version.tryFromString(release.jsonObject["name"]?.jsonPrimitive?.contentOrNull) - candidate != null && !candidate.isGreaterThan(version) && !version.isGreaterThan(candidate) - }?.jsonObject - - /** - * Parse a single release object (Forgejo or GitHub shape) into a [Release], or null if its name is not a version - */ - fun parseRelease( - json: JsonObject, - debug: Boolean, - supportedABIs: List = Build.SUPPORTED_ABIS.toList(), - ): Release? { - val name = json["name"]?.jsonPrimitive?.contentOrNull - val version = Version.tryFromString(name) - if (version == null) { - Timber.w("Update version parsing failed. name=$name") - return null - } - val publishedAt = json["published_at"]?.jsonPrimitive?.contentOrNull - val body = json["body"]?.jsonPrimitive?.contentOrNull - val downloadUrl = - json["assets"]?.jsonArray?.let { assets -> getDownloadUrl(assets, debug, supportedABIs) } - Timber.v("version=$version, downloadUrl=$downloadUrl") - val notes = - if (body.isNotNullOrBlank()) { - NOTE_REGEX - .findAll(body) - .map { m -> - m.groupValues[1] - }.toList() - } else { - emptyList() - } - return Release(version, downloadUrl, publishedAt, body, notes) - } } /** @@ -174,29 +127,17 @@ class UpdateChecker return Version.fromString(pkgInfo.versionName!!) } - /** - * Get the release whose name parses to the given version. - * - * Wolfskin releases are named by `git describe` (e.g. `v1.0.9-2-g3094fc8c`) rather than tagged `vX.Y.Z`, - * so this searches the release list instead of fetching a tag. - */ suspend fun getRelease(version: Version): Release? { - val request = - Request - .Builder() - .url(RELEASES_URL) - .get() - .build() + val url = + "https://api.github.com/repos/damontecres/Wholphin/releases/tags/v${version.major}.${version.minor}.${version.patch}" return withContext(WholphinDispatchers.IO) { - okHttpClient.newCall(request).execute().use { - if (it.isSuccessful) { - val releases = Json.parseToJsonElement(it.body.string()).jsonArray - findReleaseByVersion(releases, version)?.let { json -> parseRelease(json, BuildConfig.DEBUG) } - } else { - Timber.w("Release lookup failed ${it.code}: ${it.message}") - null - } - } + val request = + Request + .Builder() + .url(url) + .get() + .build() + getRelease(request) } } @@ -217,11 +158,36 @@ class UpdateChecker private fun getRelease(request: Request): Release? { return okHttpClient.newCall(request).execute().use { if (it.isSuccessful) { - parseRelease(Json.parseToJsonElement(it.body.string()).jsonObject, BuildConfig.DEBUG) + val result = Json.parseToJsonElement(it.body.string()) + val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull + val version = Version.tryFromString(name) + val publishedAt = + result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull + val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull + val downloadUrl = + result.jsonObject["assets"] + ?.jsonArray + ?.let { assets -> getDownloadUrl(assets, BuildConfig.DEBUG) } + Timber.v("version=$version, downloadUrl=$downloadUrl") + if (version != null) { + val notes = + if (body.isNotNullOrBlank()) { + NOTE_REGEX + .findAll(body) + .map { m -> + m.groupValues[1] + }.toList() + } else { + emptyList() + } + return@use Release(version, downloadUrl, publishedAt, body, notes) + } else { + Timber.w("Update version parsing failed. name=$name") + } } else { Timber.w("Update check failed ${it.code}: ${it.message}") - null } + return@use null } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt index 2e84e2ca..4cae61ba 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/test/TestUpdateChecker.kt @@ -1,16 +1,11 @@ package com.github.damontecres.wholphin.test -import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.getDownloadUrl -import com.github.damontecres.wholphin.util.Version import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import org.junit.Assert import org.junit.Before import org.junit.Test @@ -24,151 +19,71 @@ class TestUpdateChecker { lateinit var releaseV108Json: JsonObject val assetsV108Json: JsonArray by lazy { releaseV108Json["assets"]!!.jsonArray } - private val forgejoLatest: JsonObject by lazy { read("forgejo_release_latest.json") } - private val forgejoAssets: JsonArray by lazy { forgejoLatest["assets"]!!.jsonArray } - private val forgejoReleases: JsonArray by lazy { readElement("forgejo_releases.json").jsonArray } - private val suffixedAssets: JsonArray by lazy { read("forgejo_release_suffixed.json")["assets"]!!.jsonArray } - - private fun readElement(filename: String): JsonElement { + private fun read(filename: String): JsonObject { val resource = javaClass.classLoader?.getResource(filename) Assert.assertNotNull(resource) val fileContents = Paths.get(resource!!.toURI()).readText() - return Json.parseToJsonElement(fileContents) + return Json.parseToJsonElement(fileContents).jsonObject } - private fun read(filename: String): JsonObject = readElement(filename).jsonObject - - private fun forgejoDownload(name: String) = - "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/$name" - @Before fun setup() { releaseJson = read("release_develop.json") } - // The GitHub fixtures are upstream Wholphin releases: their assets are named Wholphin*.apk, which - // the Wolfskin updater must not select. - @Test - fun `Release does not choose upstream Wholphin assets`() { + fun `Release chooses release`() { val url = getDownloadUrl(assetsJson, false, listOf()) - Assert.assertNull(url) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) } @Test - fun `Choose abi does not choose upstream Wholphin assets`() { + fun `Choose abi`() { val url = getDownloadUrl(assetsJson, false, listOf("arm64-v8a")) - Assert.assertNull(url) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release-arm64-v8a.apk", url) } @Test - fun `Choose unknown abi does not choose upstream Wholphin assets`() { + fun `Choose unknown abi`() { val url = getDownloadUrl(assetsJson, false, listOf("unknown")) - Assert.assertNull(url) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-release.apk", url) } @Test - fun `Debug does not choose upstream Wholphin assets`() { + fun `Debug chooses debug`() { val url = getDownloadUrl(assetsJson, true, listOf()) - Assert.assertNull(url) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug.apk", url) } @Test - fun `Choose debug abi does not choose upstream Wholphin assets`() { + fun `Choose debug abi`() { val url = getDownloadUrl(assetsJson, true, listOf("arm64-v8a")) - Assert.assertNull(url) + Assert.assertEquals("https://github.com/damontecres/Wholphin/releases/download/develop/Wholphin-debug-arm64-v8a.apk", url) } @Test - fun `Test regular release does not choose upstream Wholphin assets`() { + fun `Test regular release`() { releaseV108Json = read("release_v108.json") - Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a"))) - Assert.assertNull(getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a"))) - Assert.assertNull(getDownloadUrl(assetsV108Json, false, emptyList())) - Assert.assertNull(getDownloadUrl(assetsV108Json, true, listOf("arm64-v8a"))) - } - - @Test - fun `Parse Forgejo latest release`() { - val release = UpdateChecker.parseRelease(forgejoLatest, debug = false, supportedABIs = listOf("arm64-v8a")) - checkNotNull(release) - Assert.assertEquals(Version.fromString("v1.0.9-2-g3094fc8c"), release.version) - Assert.assertEquals(forgejoDownload("Wolfskin-arm64-v8a.apk"), release.downloadUrl) - Assert.assertEquals("2026-10-20T09:05:00Z", release.publishedAt) - Assert.assertEquals(listOf("Subtitle preference fix"), release.notes) - } - - @Test - fun `Choose Wolfskin asset for abi`() { - Assert.assertEquals(forgejoDownload("Wolfskin-arm64-v8a.apk"), getDownloadUrl(forgejoAssets, false, listOf("arm64-v8a"))) - Assert.assertEquals(forgejoDownload("Wolfskin-armeabi-v7a.apk"), getDownloadUrl(forgejoAssets, false, listOf("armeabi-v7a"))) - } - - @Test - fun `Choose universal Wolfskin asset for unknown abi`() { - Assert.assertEquals(forgejoDownload("Wolfskin.apk"), getDownloadUrl(forgejoAssets, false, listOf("unknown"))) - Assert.assertEquals(forgejoDownload("Wolfskin.apk"), getDownloadUrl(forgejoAssets, false, emptyList())) - } - - @Test - fun `Debug asset is not offered when release has none`() { - Assert.assertNull(getDownloadUrl(forgejoAssets, true, listOf("arm64-v8a"))) - } - - @Test - fun `Choose debug ABI before universal and fall back for missing ABI`() { - for ((abis, name) in listOf( - listOf("arm64-v8a") to "Wolfskin-debug-arm64-v8a.apk", - listOf("unknown") to "Wolfskin-debug.apk", - emptyList() to "Wolfskin-debug.apk", - )) { - Assert.assertEquals(forgejoDownload(name), getDownloadUrl(suffixedAssets, true, abis)) + getDownloadUrl(assetsV108Json, false, listOf("arm64-v8a")).let { url -> + Assert.assertEquals( + "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-arm64-v8a.apk", + url, + ) } - } - - @Test - fun `Choose release suffixed ABI before universal and unsuffixed assets`() { - val assets = JsonArray(forgejoAssets + suffixedAssets) - for ((abis, name) in listOf( - listOf("arm64-v8a") to "Wolfskin-release-arm64-v8a.apk", - listOf("armeabi-v7a") to "Wolfskin-release.apk", - emptyList() to "Wolfskin-release.apk", - )) { - Assert.assertEquals(forgejoDownload(name), getDownloadUrl(assets, false, abis)) + getDownloadUrl(assetsV108Json, false, listOf("armeabi-v7a")).let { url -> + Assert.assertEquals( + "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin-armeabi-v7a.apk", + url, + ) + } + getDownloadUrl(assetsV108Json, false, emptyList()).let { url -> + Assert.assertEquals( + "https://github.com/damontecres/Wholphin/releases/download/v1.0.8/Wholphin.apk", + url, + ) + } + getDownloadUrl(assetsV108Json, true, listOf("arm64-v8a")).let { url -> + Assert.assertNull(url) } - } - - @Test - fun `Release and debug assets do not cross build types`() { - val debugAssets = JsonArray(suffixedAssets.filter { it.jsonObject["name"]!!.jsonPrimitive.content.contains("-debug") }) - val releaseAssets = JsonArray(suffixedAssets.filter { it.jsonObject["name"]!!.jsonPrimitive.content.contains("-release") }) - Assert.assertNull(getDownloadUrl(debugAssets, false, listOf("arm64-v8a"))) - Assert.assertNull(getDownloadUrl(releaseAssets, true, listOf("arm64-v8a"))) - } - - @Test - fun `Find installed version in release list`() { - val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-2-g3094fc8c")) - Assert.assertEquals("v1.0.9-2-g3094fc8c", release?.get("name")?.jsonPrimitive?.content) - } - - @Test - fun `Find release with different hash abbreviation length`() { - val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-2-g3094fc8")) - Assert.assertEquals("v1.0.9-2-g3094fc8c", release?.get("name")?.jsonPrimitive?.content) - } - - @Test - fun `Find release with absent or zero commit count`() { - val release = UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.10")) - Assert.assertEquals("v1.0.10-0-gfeedbee", release?.get("name")?.jsonPrimitive?.content) - val plainRelease = JsonObject(forgejoReleases.first().jsonObject + ("name" to JsonPrimitive("v1.0.10"))) - val plainReleases = JsonArray(listOf(plainRelease)) - Assert.assertEquals(plainReleases.first(), UpdateChecker.findReleaseByVersion(plainReleases, Version.fromString("1.0.10-0-gfeedbee"))) - } - - @Test - fun `Find no release for version missing from list`() { - Assert.assertNull(UpdateChecker.findReleaseByVersion(forgejoReleases, Version.fromString("1.0.9-3-gffffff"))) } } diff --git a/app/src/test/resources/forgejo_release_latest.json b/app/src/test/resources/forgejo_release_latest.json deleted file mode 100644 index f99734b9..00000000 --- a/app/src/test/resources/forgejo_release_latest.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": 42, - "name": "v1.0.9-2-g3094fc8c", - "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", - "body": "Wolfskin build\n\n**Full Changelog**: https://git.justinvisser.org/justinzeus/wolfskin/compare/v1.0.8...v1.0.9", - "draft": false, - "prerelease": false, - "created_at": "2026-10-20T09:00:00Z", - "published_at": "2026-10-20T09:05:00Z", - "target_commitish": "3094fc8c", - "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.9-2-g3094fc8c", - "assets": [ - { - "id": 101, - "name": "Wolfskin.apk", - "size": 12345678, - "download_count": 3, - "created_at": "2026-10-20T09:05:00Z", - "uuid": "11111111-1111-1111-1111-111111111111", - "type": "attachment", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin.apk" - }, - { - "id": 102, - "name": "Wolfskin-arm64-v8a.apk", - "size": 7654321, - "download_count": 1, - "created_at": "2026-10-20T09:05:00Z", - "uuid": "22222222-2222-2222-2222-222222222222", - "type": "attachment", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-arm64-v8a.apk" - }, - { - "id": 103, - "name": "Wolfskin-armeabi-v7a.apk", - "size": 6543210, - "download_count": 0, - "created_at": "2026-10-20T09:05:00Z", - "uuid": "33333333-3333-3333-3333-333333333333", - "type": "attachment", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-armeabi-v7a.apk" - } - ] -} diff --git a/app/src/test/resources/forgejo_release_suffixed.json b/app/src/test/resources/forgejo_release_suffixed.json deleted file mode 100644 index c2155081..00000000 --- a/app/src/test/resources/forgejo_release_suffixed.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "v1.0.9-2-g3094fc8c", - "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", - "published_at": "2026-10-20T09:05:00Z", - "body": "Wolfskin release and debug builds", - "assets": [ - { - "name": "Wolfskin-debug.apk", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-debug.apk" - }, - { - "name": "Wolfskin-release.apk", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-release.apk" - }, - { - "name": "Wolfskin-debug-arm64-v8a.apk", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-debug-arm64-v8a.apk" - }, - { - "name": "Wolfskin-release-arm64-v8a.apk", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.9-2-g3094fc8c/Wolfskin-release-arm64-v8a.apk" - } - ] -} diff --git a/app/src/test/resources/forgejo_releases.json b/app/src/test/resources/forgejo_releases.json deleted file mode 100644 index ab84e411..00000000 --- a/app/src/test/resources/forgejo_releases.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "id": 44, - "name": "v1.0.10-0-gfeedbee", - "tag_name": "pwolfskin-v1.0.10-0-gfeedbee", - "body": "", - "draft": false, - "prerelease": false, - "created_at": "2026-10-22T08:00:00Z", - "published_at": "2026-10-22T08:05:00Z", - "target_commitish": "feedbee", - "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.10-0-gfeedbee", - "assets": [ - { - "id": 104, - "name": "Wolfskin.apk", - "size": 12345679, - "download_count": 0, - "created_at": "2026-10-22T08:05:00Z", - "uuid": "44444444-4444-4444-4444-444444444444", - "type": "attachment", - "browser_download_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/download/pwolfskin-v1.0.10-0-gfeedbee/Wolfskin.apk" - } - ] - }, - { - "id": 42, - "name": "v1.0.9-2-g3094fc8c", - "tag_name": "pwolfskin-v1.0.9-2-g3094fc8c", - "body": "Wolfskin build", - "draft": false, - "prerelease": false, - "created_at": "2026-10-20T09:00:00Z", - "published_at": "2026-10-20T09:05:00Z", - "target_commitish": "3094fc8c", - "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/pwolfskin-v1.0.9-2-g3094fc8c", - "assets": [] - }, - { - "id": 40, - "name": "nightly", - "tag_name": "nightly", - "body": "", - "draft": false, - "prerelease": true, - "created_at": "2026-10-10T09:00:00Z", - "published_at": "2026-10-10T09:05:00Z", - "target_commitish": "main", - "html_url": "https://git.justinvisser.org/justinzeus/wolfskin/releases/tag/nightly", - "assets": [] - } -] diff --git a/docker/README.md b/docker/README.md index f1a1b7d0..353a716f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -17,11 +17,6 @@ use the catalog's exact Wholphin extension version (currently `0.2.4`). Do not place local extension AARs in `app/libs`: upstream prefers them over the pinned GitHub Packages dependencies. -Upload release assets renamed to `Wolfskin.apk` / `Wolfskin-.apk` for the -in-app updater. Gradle outputs -`Wholphin-default-release--[-].apk`; strip the -flavor, version, and code as well as replacing the prefix. - Upstream enables JVM unit tests only for debug variants; there is no `testDefaultReleaseUnitTest` task. `make test` uses the closest default-flavor task without changing upstream's test configuration.