Show toast & add option to fetch release notes when app updates (#1182)

## Description
When Wholphin is updated, it will show a short toast message. This works
for both app store and side loaded installs.

You can now click on the "Installed version" in settings to fetch the
release notes for the current version.

### Related issues
Closes #1058

### Testing
Emulator

## Screenshots
![release_notes
Large](https://github.com/user-attachments/assets/2c03eb74-abb2-4b45-8d6f-01e8ef72116c)

## AI or LLM usage
None
This commit is contained in:
Ray 2026-04-03 12:44:39 -04:00 committed by GitHub
parent 46beee00bb
commit feb8c31791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 207 additions and 85 deletions

View file

@ -1,11 +1,11 @@
package com.github.damontecres.wholphin.services
import android.content.Context
import android.content.pm.PackageInfo
import android.os.Build
import androidx.core.content.edit
import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.WholphinApplication
import com.github.damontecres.wholphin.data.SeerrServerDao
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
@ -43,12 +43,10 @@ class AppUpgradeHandler
private val appPreferences: DataStore<AppPreferences>,
private val seerrServerDao: SeerrServerDao,
) {
suspend fun run() {
val pkgInfo =
WholphinApplication.instance.packageManager.getPackageInfo(
WholphinApplication.instance.packageName,
0,
)
val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0)
val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!)
fun needUpgrade(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
@ -60,31 +58,43 @@ class AppUpgradeHandler
} else {
pkgInfo.versionCode.toLong()
}
if (newVersion != previousVersion || newVersionCode != previousVersionCode) {
Timber.i(
"App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
)
// Store the previous and new version info
prefs.edit(true) {
putString(VERSION_NAME_PREVIOUS_KEY, previousVersion)
putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode)
putString(VERSION_NAME_CURRENT_KEY, newVersion)
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
Timber.i(
"App versions: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
)
return newVersion != previousVersion || newVersionCode != previousVersionCode
}
suspend fun run() {
Timber.i("App upgrade started")
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
val newVersion = pkgInfo.versionName!!
val newVersionCode =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pkgInfo.longVersionCode
} else {
pkgInfo.versionCode.toLong()
}
try {
copySubfont(true)
upgradeApp(
Version.fromString(previousVersion ?: "0.0.0"),
Version.fromString(newVersion),
appPreferences,
)
} catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade")
}
Timber.i("App upgrade complete")
} else {
Timber.d("No app update needed")
// Store the previous and new version info
prefs.edit(true) {
putString(VERSION_NAME_PREVIOUS_KEY, previousVersion)
putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode)
putString(VERSION_NAME_CURRENT_KEY, newVersion)
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
}
try {
copySubfont(true)
upgradeApp(
Version.fromString(previousVersion ?: "0.0.0"),
Version.fromString(newVersion),
appPreferences,
)
} catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade")
}
Timber.i("App upgrade complete")
}
/**

View file

@ -128,50 +128,67 @@ class UpdateChecker
return Version.fromString(pkgInfo.versionName!!)
}
suspend fun getRelease(version: Version): Release? {
val url =
"https://api.github.com/repos/damontecres/Wholphin/releases/tags/v${version.major}.${version.minor}.${version.patch}"
return withContext(Dispatchers.IO) {
val request =
Request
.Builder()
.url(url)
.get()
.build()
getRelease(request)
}
}
/**
* Get the latest released version
*/
suspend fun getLatestRelease(updateUrl: String): Release? {
return withContext(Dispatchers.IO) {
suspend fun getLatestRelease(updateUrl: String): Release? =
withContext(Dispatchers.IO) {
val request =
Request
.Builder()
.url(updateUrl)
.get()
.build()
okHttpClient.newCall(request).execute().use {
if (it.isSuccessful && it.body != null) {
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 {
listOf()
}
return@use Release(version, downloadUrl, publishedAt, body, notes)
} else {
Timber.w("Update version parsing failed. name=$name")
}
getRelease(request)
}
private fun getRelease(request: Request): Release? {
return okHttpClient.newCall(request).execute().use {
if (it.isSuccessful && it.body != null) {
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 check failed: ${it.message}")
Timber.w("Update version parsing failed. name=$name")
}
return@use null
} else {
Timber.w("Update check failed ${it.code}: ${it.message}")
}
return@use null
}
}
@ -347,7 +364,19 @@ data class Release(
val publishedAt: String?,
val body: String?,
val notes: List<String>,
)
) {
val content =
"# ${version}\n" +
(
(notes.joinToString("\n").takeIf { it.isNotNullOrBlank() } ?: "") +
(body ?: "")
).replace(
Regex("https://github.com/\\w*/\\w+/pull/(\\d+)"),
"#$1",
)
// Remove the last line for full changelog since its just a link
.replace(Regex("\\*\\*Full Changelog\\*\\*.*"), "")
}
interface DownloadCallback {
fun contentLength(contentLength: Long)