app: use github api to check updates

This commit is contained in:
vvb2060 2025-05-09 09:22:43 +00:00 committed by John Wu
parent a4b8c5e46b
commit 76962f965e
9 changed files with 88 additions and 66 deletions

View File

@ -14,7 +14,8 @@ class ManagerInstallDialog : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String { override suspend fun getMarkdownText(): String {
val text = svc.fetchString(Info.remote.magisk.note) val str = 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 { AppContext.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
it.delete() it.delete()

View File

@ -71,11 +71,14 @@ 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.Url.CHANGELOG_URL.isEmpty() -> "" Const.APP_IS_CANARY && note.isEmpty() -> ""
Const.APP_IS_CANARY && !note.startsWith("http", true) -> note
else -> { else -> {
val str = svc.fetchString(Const.Url.CHANGELOG_URL) val url = if (Const.APP_IS_CANARY) note else Const.Url.CHANGELOG_URL
val str = svc.fetchString(url)
file.writeText(str) file.writeText(str)
str str
} }

View File

@ -43,8 +43,7 @@ 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"
val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note const val CHANGELOG_URL = "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.APP_VERSION_CODE}.md"
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.APP_VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/" 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/"

View File

@ -1,25 +1,16 @@
package com.topjohnwu.magisk.core.data package com.topjohnwu.magisk.core.data
import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.ModuleJson import com.topjohnwu.magisk.core.model.ModuleJson
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.http.GET import retrofit2.http.GET
import retrofit2.http.Headers import retrofit2.http.Headers
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Streaming import retrofit2.http.Streaming
import retrofit2.http.Url import retrofit2.http.Url
private const val BRANCH = "branch"
private const val REPO = "repo"
private const val FILE = "file"
interface GithubPageServices {
@GET
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
}
interface RawServices { interface RawServices {
@GET @GET
@ -32,14 +23,26 @@ interface RawServices {
@GET @GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson suspend fun fetchModuleJson(@Url url: String): ModuleJson
@GET
suspend fun fetchUpdateJSON(@Url url: String): UpdateInfo
} }
interface GithubApiServices { interface GithubApiServices {
@GET("repos/{$REPO}/branches/{$BRANCH}") @GET("/repos/{owner}/{repo}/releases")
@Headers("Accept: application/vnd.github.v3+json") @Headers("Accept: application/vnd.github+json")
suspend fun fetchBranch( suspend fun fetchRelease(
@Path(REPO, encoded = true) repo: String, @Path("owner") owner: String = "topjohnwu",
@Path(BRANCH) branch: String @Path("repo") repo: String = "Magisk",
): BranchInfo @Query("per_page") per: Int = 10,
@Query("page") page: Int = 1,
): List<Release>
@GET("/repos/{owner}/{repo}/releases/latest")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchLatestRelease(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
): Release
} }

View File

@ -35,8 +35,8 @@ 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_PAGE_URL),
createApiService(retrofit, Const.Url.GITHUB_RAW_URL), createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
createApiService(retrofit, Const.Url.GITHUB_API_URL),
) )
} }
} }

View File

@ -27,11 +27,16 @@ data class ModuleJson(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CommitInfo( data class ReleaseAssets(
val sha: String val name: String,
val browser_download_url: String,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class BranchInfo( data class Release(
val commit: CommitInfo val tag_name: String,
val name: String,
val prerelease: Boolean,
val assets: List<ReleaseAssets>,
val body: String,
) )

View File

@ -8,15 +8,18 @@ import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL 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.GithubPageServices import com.topjohnwu.magisk.core.data.GithubApiServices
import com.topjohnwu.magisk.core.data.RawServices import com.topjohnwu.magisk.core.data.RawServices
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 retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
class NetworkService( class NetworkService(
private val pages: GithubPageServices, private val raw: RawServices,
private val raw: RawServices private val api: GithubApiServices,
) { ) {
suspend fun fetchUpdate() = safe { suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) { var info = when (Config.updateChannel) {
@ -36,11 +39,39 @@ class NetworkService(
} }
// UpdateInfo // UpdateInfo
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json") private suspend fun fetchStableUpdate(rel: Release? = null): UpdateInfo {
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json") val release = rel ?: api.fetchLatestRelease()
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json") val name = release.tag_name.drop(1)
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json") val info = MagiskJson(
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url) name, (name.toFloat() * 1000).toInt(),
release.assets[0].browser_download_url, release.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-") }
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
)
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 fetchCustomUpdate(url: String) = raw.fetchUpdateJSON(url)
private inline fun <T> safe(factory: () -> T): T? { private inline fun <T> safe(factory: () -> T): T? {
return try { return try {

View File

@ -13,24 +13,28 @@ android {
namespace = "com.topjohnwu.magisk" namespace = "com.topjohnwu.magisk"
val canary = !Config.version.contains(".") val canary = !Config.version.contains(".")
val base = "https://github.com/topjohnwu/Magisk/releases/download/"
val url = if (canary) null val url = base + "v${Config.version}/Magisk-v${Config.version}.apk"
else "https://github.com/topjohnwu/Magisk/releases/download/v${Config.version}/Magisk-v${Config.version}.apk" val canaryUrl = base + "canary-${Config.versionCode}/"
defaultConfig { defaultConfig {
applicationId = "com.topjohnwu.magisk" applicationId = "com.topjohnwu.magisk"
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
buildConfigField("String", "APK_URL", url?.let { "\"$it\"" } ?: "null" ) buildConfigField("String", "APK_URL", "\"$url\"")
buildConfigField("int", "STUB_VERSION", Config.stubVersion) buildConfigField("int", "STUB_VERSION", Config.stubVersion)
} }
buildTypes { buildTypes {
release { release {
if (canary) buildConfigField("String", "APK_URL", "\"${canaryUrl}app-release.apk\"")
proguardFiles("proguard-rules.pro") proguardFiles("proguard-rules.pro")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = false isShrinkResources = false
} }
debug {
if (canary) buildConfigField("String", "APK_URL", "\"${canaryUrl}app-debug.apk\"")
}
} }
buildFeatures { buildFeatures {

View File

@ -27,8 +27,6 @@ import com.topjohnwu.magisk.net.Networking;
import com.topjohnwu.magisk.net.Request; import com.topjohnwu.magisk.net.Request;
import com.topjohnwu.magisk.utils.APKInstall; import com.topjohnwu.magisk.utils.APKInstall;
import org.json.JSONException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -48,13 +46,8 @@ import javax.crypto.spec.SecretKeySpec;
public class DownloadActivity extends Activity { public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk"; private static final String APP_NAME = "Magisk";
private static final String JSON_URL = BuildConfig.DEBUG ?
"https://topjohnwu.github.io/magisk-files/debug.json" :
"https://topjohnwu.github.io/magisk-files/canary.json";
private String apkLink = BuildConfig.APK_URL;
private Context themed; private Context themed;
private ProgressDialog dialog;
private boolean dynLoad; private boolean dynLoad;
@Override @Override
@ -75,11 +68,7 @@ public class DownloadActivity extends Activity {
ProviderInstaller.install(this); ProviderInstaller.install(this);
if (Networking.checkNetworkStatus(this)) { if (Networking.checkNetworkStatus(this)) {
if (BuildConfig.APK_URL == null) {
fetchCanary();
} else {
showDialog(); showDialog();
}
} else { } else {
new AlertDialog.Builder(themed) new AlertDialog.Builder(themed)
.setCancelable(false) .setCancelable(false)
@ -115,23 +104,10 @@ public class DownloadActivity extends Activity {
.show(); .show();
} }
private void fetchCanary() {
dialog = ProgressDialog.show(themed, "", "", true);
request(JSON_URL).getAsJSONObject(json -> {
dialog.dismiss();
try {
apkLink = json.getJSONObject("magisk").getString("link");
showDialog();
} catch (JSONException e) {
error(e);
}
});
}
private void dlAPK() { private void dlAPK() {
dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true); ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
// Download and upgrade the app // Download and upgrade the app
var request = request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR); var request = request(BuildConfig.APK_URL).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (dynLoad) { if (dynLoad) {
request.getAsFile(StubApk.current(this), file -> StubApk.restartProcess(this)); request.getAsFile(StubApk.current(this), file -> StubApk.restartProcess(this));
} else { } else {