From adbea7e3135a0cb316a2673c23993ef3f77ac6a3 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Mon, 19 May 2025 03:42:39 -0700 Subject: [PATCH] Improve update check code --- .../magisk/dialog/ManagerInstallDialog.kt | 8 +-- .../magisk/ui/install/InstallViewModel.kt | 24 +++---- .../java/com/topjohnwu/magisk/core/Const.kt | 8 +-- ...tworkServices.kt => RetrofitInterfaces.kt} | 10 +-- .../topjohnwu/magisk/core/di/Networking.kt | 3 +- .../magisk/core/di/ServiceLocator.kt | 2 +- .../topjohnwu/magisk/core/model/UpdateInfo.kt | 28 +++++++- .../magisk/core/repository/NetworkService.kt | 70 ++++++++++++------- 8 files changed, 96 insertions(+), 57 deletions(-) rename app/core/src/main/java/com/topjohnwu/magisk/core/data/{NetworkServices.kt => RetrofitInterfaces.kt} (86%) diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt b/app/apk/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt index a88b2784b..23fc3a2e3 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt @@ -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 } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt index b85d3b671..b2b2103a5 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt @@ -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 } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt index 3c99504cd..c3eeef9fc 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/Const.kt @@ -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 { diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/data/NetworkServices.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/data/RetrofitInterfaces.kt similarity index 86% rename from app/core/src/main/java/com/topjohnwu/magisk/core/data/NetworkServices.kt rename to app/core/src/main/java/com/topjohnwu/magisk/core/data/RetrofitInterfaces.kt index 1420be317..5c25450b0 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/data/NetworkServices.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/data/RetrofitInterfaces.kt @@ -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 + ): Response> @GET("/repos/{owner}/{repo}/releases/latest") @Headers("Accept: application/vnd.github+json") diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt index 92738082f..1c262e1b4 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/di/Networking.kt @@ -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) } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/di/ServiceLocator.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/di/ServiceLocator.kt index a621c8184..1429325cb 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/di/ServiceLocator.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/di/ServiceLocator.kt @@ -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), ) } diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt index 3f44647ec..d766fd0d5 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/model/UpdateInfo.kt @@ -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, val body: String, + @Json(name = "created_at") @DateTime val createdTime: LocalDateTime, ) diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/repository/NetworkService.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/repository/NetworkService.kt index 1eec8299f..5bb1bd2fb 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/repository/NetworkService.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/repository/NetworkService.kt @@ -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 safe(factory: () -> T): T? { return try {