Improve update check code

This commit is contained in:
topjohnwu 2025-05-19 03:42:39 -07:00 committed by John Wu
parent 76962f965e
commit adbea7e313
8 changed files with 96 additions and 57 deletions

View File

@ -11,15 +11,9 @@ import java.io.File
class ManagerInstallDialog : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String {
val str = Info.remote.magisk.note
val text = if (str.startsWith("http", true)) svc.fetchString(str) else str
val text = Info.remote.magisk.note
// Cache the changelog
AppContext.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
it.delete()
}
File(AppContext.cacheDir, "${Info.remote.magisk.versionCode}.md").writeText(text)
return text
}

View File

@ -71,14 +71,11 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
viewModelScope.launch(Dispatchers.IO) {
try {
val file = File(AppContext.cacheDir, "${BuildConfig.APP_VERSION_CODE}.md")
val note = Info.remote.magisk.note
val text = when {
file.exists() -> file.readText()
Const.APP_IS_CANARY && note.isEmpty() -> ""
Const.APP_IS_CANARY && !note.startsWith("http", true) -> note
else -> {
val url = if (Const.APP_IS_CANARY) note else Const.Url.CHANGELOG_URL
val str = svc.fetchString(url)
val str = if (Const.APP_IS_CANARY) Info.remote.magisk.note
else svc.fetchString(Const.Url.CHANGELOG_URL)
file.writeText(str)
str
}
@ -103,13 +100,15 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
}
override fun onSaveState(state: Bundle) {
state.putParcelable(INSTALL_STATE_KEY, InstallState(
methodId,
step,
Config.keepVerity,
Config.keepEnc,
Config.recovery
))
state.putParcelable(
INSTALL_STATE_KEY, InstallState(
methodId,
step,
Config.keepVerity,
Config.keepEnc,
Config.recovery
)
)
}
override fun onRestoreState(state: Bundle) {
@ -127,6 +126,7 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
override fun onActivityLaunch() {
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
}
override fun onActivityResult(result: Uri) {
uri.value = result
}

View File

@ -2,6 +2,7 @@ package com.topjohnwu.magisk.core
import android.os.Build
import android.os.Process
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
@Suppress("DEPRECATION")
object Const {
@ -20,7 +21,7 @@ object Const {
// Misc
val USER_ID = Process.myUid() / 100000
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.APP_VERSION_CODE)
val APP_IS_CANARY get() = Version.isCanary(APP_VERSION_CODE)
object Version {
const val MIN_VERSION = "v22.0"
@ -43,12 +44,11 @@ object Const {
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
const val CHANGELOG_URL = "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.APP_VERSION_CODE}.md"
const val CHANGELOG_URL = "https://topjohnwu.github.io/Magisk/releases/${APP_VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
const val INVALID_URL = "https://example.com/"
}
object Key {

View File

@ -4,6 +4,7 @@ import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
@ -11,7 +12,7 @@ import retrofit2.http.Query
import retrofit2.http.Streaming
import retrofit2.http.Url
interface RawServices {
interface RawUrl {
@GET
@Streaming
@ -24,20 +25,19 @@ interface RawServices {
suspend fun fetchModuleJson(@Url url: String): ModuleJson
@GET
suspend fun fetchUpdateJSON(@Url url: String): UpdateInfo
suspend fun fetchUpdateJson(@Url url: String): UpdateInfo
}
interface GithubApiServices {
@GET("/repos/{owner}/{repo}/releases")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchRelease(
suspend fun fetchReleases(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
@Query("per_page") per: Int = 10,
@Query("page") page: Int = 1,
): List<Release>
): Response<MutableList<Release>>
@GET("/repos/{owner}/{repo}/releases/latest")
@Headers("Accept: application/vnd.github+json")

View File

@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.DateTimeAdapter
import com.topjohnwu.magisk.core.utils.LocaleSetting
import okhttp3.Cache
import okhttp3.ConnectionSpec
@ -77,7 +78,7 @@ fun createOkHttpClient(context: Context): OkHttpClient {
}
fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder().build()
val moshi = Moshi.Builder().add(DateTimeAdapter()).build()
return MoshiConverterFactory.create(moshi)
}

View File

@ -35,7 +35,7 @@ object ServiceLocator {
val markwon by lazy { createMarkwon(AppContext) }
val networkService by lazy {
NetworkService(
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
createApiService(retrofit, Const.Url.INVALID_URL),
createApiService(retrofit, Const.Url.GITHUB_API_URL),
)
}

View File

@ -1,8 +1,14 @@
package com.topjohnwu.magisk.core.model
import android.os.Parcelable
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import kotlinx.parcelize.Parcelize
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME
@JsonClass(generateAdapter = true)
data class UpdateInfo(
@ -29,14 +35,32 @@ data class ModuleJson(
@JsonClass(generateAdapter = true)
data class ReleaseAssets(
val name: String,
val browser_download_url: String,
@Json(name = "browser_download_url") val url: String,
)
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DateTime
class DateTimeAdapter {
@ToJson
fun toJson(@DateTime date: LocalDateTime): String {
return date.toString()
}
@FromJson
@DateTime
fun fromJson(date: String): LocalDateTime {
return LocalDateTime.parse(date, ISO_OFFSET_DATE_TIME)
}
}
@JsonClass(generateAdapter = true)
data class Release(
val tag_name: String,
@Json(name = "tag_name") val tag: String,
val name: String,
val prerelease: Boolean,
val assets: List<ReleaseAssets>,
val body: String,
@Json(name = "created_at") @DateTime val createdTime: LocalDateTime,
)

View File

@ -9,16 +9,17 @@ import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.data.GithubApiServices
import com.topjohnwu.magisk.core.data.RawServices
import com.topjohnwu.magisk.core.data.RawUrl
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateInfo
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
import java.time.format.DateTimeFormatter
class NetworkService(
private val raw: RawServices,
private val raw: RawUrl,
private val api: GithubApiServices,
) {
suspend fun fetchUpdate() = safe {
@ -38,40 +39,59 @@ class NetworkService(
info
}
// UpdateInfo
private suspend fun fetchStableUpdate(rel: Release? = null): UpdateInfo {
val release = rel ?: api.fetchLatestRelease()
val name = release.tag_name.drop(1)
// Keep going through all release pages until we find a match
private suspend inline fun findRelease(predicate: (Release) -> Boolean): Release? {
var page = 1
while (true) {
val response = api.fetchReleases(page = page)
val releases = response.body() ?: throw HttpException(response)
// Make sure it's sorted correctly
releases.sortByDescending { it.createdTime }
releases.find(predicate)?.let { return it }
if (response.headers()["link"]?.contains("rel=\"next\"", ignoreCase = true) == true) {
page += 1
} else {
return null
}
}
}
private fun Release.asPublicInfo(): UpdateInfo {
val version = tag.drop(1)
val date = createdTime.format(DateTimeFormatter.ofPattern("yyyy.M.d"))
val info = MagiskJson(
name, (name.toFloat() * 1000).toInt(),
release.assets[0].browser_download_url, release.body
version = version,
versionCode = (version.toFloat() * 1000).toInt(),
link = assets[0].url,
note = "## $date $name\n\n$body"
)
return UpdateInfo(info)
}
private suspend fun fetchBetaUpdate(): UpdateInfo {
val release = api.fetchRelease().find { it.tag_name[0] == 'v' && it.prerelease }
return fetchStableUpdate(release)
}
private suspend fun fetchCanaryUpdate(): UpdateInfo {
val release = api.fetchRelease().find { it.tag_name.startsWith("canary-") }
private fun Release.asCanaryInfo(assetSelector: String): UpdateInfo {
val info = MagiskJson(
release!!.name.substring(8, 16),
release.tag_name.drop(7).toInt(),
release.assets.find { it.name == "app-release.apk" }!!.browser_download_url,
release.body
version = name.substring(8, 16),
versionCode = tag.drop(7).toInt(),
link = assets.find { it.name == assetSelector }!!.url,
note = "## $name\n\n$body"
)
return UpdateInfo(info)
}
private suspend fun fetchDebugUpdate(): UpdateInfo {
val release = fetchCanaryUpdate()
val link = release.magisk.link.replace("app-release.apk", "app-debug.apk")
return UpdateInfo(release.magisk.copy(link = link))
}
private suspend fun fetchStableUpdate() = api.fetchLatestRelease().asPublicInfo()
private suspend fun fetchCustomUpdate(url: String) = raw.fetchUpdateJSON(url)
private suspend fun fetchBetaUpdate() = findRelease { it.tag[0] == 'v' }!!.asPublicInfo()
private suspend fun fetchCanary() = findRelease { it.tag.startsWith("canary-") }!!
private suspend fun fetchCanaryUpdate() = fetchCanary().asCanaryInfo("app-release.apk")
private suspend fun fetchDebugUpdate() = fetchCanary().asCanaryInfo("app-debug.apk")
private suspend fun fetchCustomUpdate(url: String): UpdateInfo {
val info = raw.fetchUpdateJson(url).magisk
return UpdateInfo(info.let { it.copy(note = raw.fetchString(it.note)) })
}
private inline fun <T> safe(factory: () -> T): T? {
return try {