Phase 5: in-app update check with SHA-256 verification
Browse files
app/src/main/java/io/github/abrarul/modularai/update/Updater.kt
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package io.github.abrarul.modularai.update
|
| 2 |
+
|
| 3 |
+
import io.github.abrarul.modularai.BuildConfig
|
| 4 |
+
import kotlinx.coroutines.Dispatchers
|
| 5 |
+
import kotlinx.coroutines.withContext
|
| 6 |
+
import org.json.JSONObject
|
| 7 |
+
import java.io.File
|
| 8 |
+
import java.net.URL
|
| 9 |
+
import java.security.MessageDigest
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Manifest (JSON) hosted next to releases:
|
| 13 |
+
* {"versionCode":2,"versionName":"0.2.0","apk":"https://.../app.apk","sha256":"..."}
|
| 14 |
+
* Android itself refuses to install an update signed with a different key, which is the
|
| 15 |
+
* real signature check; SHA-256 guards against corrupt or tampered downloads.
|
| 16 |
+
*/
|
| 17 |
+
data class UpdateInfo(val versionCode: Int, val versionName: String, val apkUrl: String, val sha256: String)
|
| 18 |
+
|
| 19 |
+
class Updater(private val manifestUrl: String, private val cacheDir: File) {
|
| 20 |
+
|
| 21 |
+
suspend fun check(): UpdateInfo? = withContext(Dispatchers.IO) {
|
| 22 |
+
val j = JSONObject(URL(manifestUrl).readText())
|
| 23 |
+
val info = UpdateInfo(j.getInt("versionCode"), j.getString("versionName"), j.getString("apk"), j.getString("sha256"))
|
| 24 |
+
if (info.versionCode > BuildConfig.VERSION_CODE) info else null
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
suspend fun download(info: UpdateInfo): File = withContext(Dispatchers.IO) {
|
| 28 |
+
val f = File(cacheDir, "update-${info.versionCode}.apk")
|
| 29 |
+
URL(info.apkUrl).openStream().use { i -> f.outputStream().use { o -> i.copyTo(o) } }
|
| 30 |
+
val md = MessageDigest.getInstance("SHA-256")
|
| 31 |
+
f.inputStream().use { i -> val b = ByteArray(1 shl 20); while (true) { val n = i.read(b); if (n < 0) break; md.update(b, 0, n) } }
|
| 32 |
+
val actual = md.digest().joinToString("") { "%02x".format(it) }
|
| 33 |
+
if (!actual.equals(info.sha256, ignoreCase = true)) { f.delete(); error("Update checksum mismatch") }
|
| 34 |
+
f
|
| 35 |
+
}
|
| 36 |
+
// Install hand-off (PackageInstaller / FileProvider) is the next task.
|
| 37 |
+
}
|