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

View File

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

View File

@ -2,6 +2,7 @@ package com.topjohnwu.magisk.core
import android.os.Build import android.os.Build
import android.os.Process import android.os.Process
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object Const { object Const {
@ -20,7 +21,7 @@ object Const {
// Misc // Misc
val USER_ID = Process.myUid() / 100000 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 { object Version {
const val MIN_VERSION = "v22.0" const val MIN_VERSION = "v22.0"
@ -43,12 +44,11 @@ object Const {
const val PATREON_URL = "https://www.patreon.com/topjohnwu" const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk" 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_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/" 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 { 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.Release
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers import retrofit2.http.Headers
import retrofit2.http.Path import retrofit2.http.Path
@ -11,7 +12,7 @@ import retrofit2.http.Query
import retrofit2.http.Streaming import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
interface RawServices { interface RawUrl {
@GET @GET
@Streaming @Streaming
@ -24,20 +25,19 @@ interface RawServices {
suspend fun fetchModuleJson(@Url url: String): ModuleJson suspend fun fetchModuleJson(@Url url: String): ModuleJson
@GET @GET
suspend fun fetchUpdateJSON(@Url url: String): UpdateInfo suspend fun fetchUpdateJson(@Url url: String): UpdateInfo
} }
interface GithubApiServices { interface GithubApiServices {
@GET("/repos/{owner}/{repo}/releases") @GET("/repos/{owner}/{repo}/releases")
@Headers("Accept: application/vnd.github+json") @Headers("Accept: application/vnd.github+json")
suspend fun fetchRelease( suspend fun fetchReleases(
@Path("owner") owner: String = "topjohnwu", @Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk", @Path("repo") repo: String = "Magisk",
@Query("per_page") per: Int = 10, @Query("per_page") per: Int = 10,
@Query("page") page: Int = 1, @Query("page") page: Int = 1,
): List<Release> ): Response<MutableList<Release>>
@GET("/repos/{owner}/{repo}/releases/latest") @GET("/repos/{owner}/{repo}/releases/latest")
@Headers("Accept: application/vnd.github+json") @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.ProviderInstaller
import com.topjohnwu.magisk.core.BuildConfig import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.DateTimeAdapter
import com.topjohnwu.magisk.core.utils.LocaleSetting import com.topjohnwu.magisk.core.utils.LocaleSetting
import okhttp3.Cache import okhttp3.Cache
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
@ -77,7 +78,7 @@ fun createOkHttpClient(context: Context): OkHttpClient {
} }
fun createMoshiConverterFactory(): MoshiConverterFactory { fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder().build() val moshi = Moshi.Builder().add(DateTimeAdapter()).build()
return MoshiConverterFactory.create(moshi) return MoshiConverterFactory.create(moshi)
} }

View File

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

View File

@ -1,8 +1,14 @@
package com.topjohnwu.magisk.core.model package com.topjohnwu.magisk.core.model
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class UpdateInfo( data class UpdateInfo(
@ -29,14 +35,32 @@ data class ModuleJson(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ReleaseAssets( data class ReleaseAssets(
val name: String, 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) @JsonClass(generateAdapter = true)
data class Release( data class Release(
val tag_name: String, @Json(name = "tag_name") val tag: String,
val name: String, val name: String,
val prerelease: Boolean, val prerelease: Boolean,
val assets: List<ReleaseAssets>, val assets: List<ReleaseAssets>,
val body: String, 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.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.data.GithubApiServices 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.MagiskJson
import com.topjohnwu.magisk.core.model.Release import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.time.format.DateTimeFormatter
class NetworkService( class NetworkService(
private val raw: RawServices, private val raw: RawUrl,
private val api: GithubApiServices, private val api: GithubApiServices,
) { ) {
suspend fun fetchUpdate() = safe { suspend fun fetchUpdate() = safe {
@ -38,40 +39,59 @@ class NetworkService(
info info
} }
// UpdateInfo // Keep going through all release pages until we find a match
private suspend fun fetchStableUpdate(rel: Release? = null): UpdateInfo { private suspend inline fun findRelease(predicate: (Release) -> Boolean): Release? {
val release = rel ?: api.fetchLatestRelease() var page = 1
val name = release.tag_name.drop(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( val info = MagiskJson(
name, (name.toFloat() * 1000).toInt(), version = version,
release.assets[0].browser_download_url, release.body versionCode = (version.toFloat() * 1000).toInt(),
link = assets[0].url,
note = "## $date $name\n\n$body"
) )
return UpdateInfo(info) return UpdateInfo(info)
} }
private suspend fun fetchBetaUpdate(): UpdateInfo { private fun Release.asCanaryInfo(assetSelector: String): 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-") }
val info = MagiskJson( val info = MagiskJson(
release!!.name.substring(8, 16), version = name.substring(8, 16),
release.tag_name.drop(7).toInt(), versionCode = tag.drop(7).toInt(),
release.assets.find { it.name == "app-release.apk" }!!.browser_download_url, link = assets.find { it.name == assetSelector }!!.url,
release.body note = "## $name\n\n$body"
) )
return UpdateInfo(info) return UpdateInfo(info)
} }
private suspend fun fetchDebugUpdate(): UpdateInfo { private suspend fun fetchStableUpdate() = api.fetchLatestRelease().asPublicInfo()
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 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? { private inline fun <T> safe(factory: () -> T): T? {
return try { return try {