Check the git for a newer release and install it from the Car screen.
This commit is contained in:
1 parent
b95cbefeab
commit
a164d9c135
6 files changed
+188
-2
No files matched your search
@@ -0,0 +1,142 @@
|
||||
package com.racingapp.pitlog.data
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val LATEST_RELEASE =
|
||||
"https://git.ny.daprogs.com/api/v1/repos/RallyBlock/Pit-Log-App/releases/latest"
|
||||
|
||||
private const val MAX_APK_BYTES = 80_000_000
|
||||
|
||||
private data class PostedRelease(val versionCode: Long, val apkUrl: String)
|
||||
|
||||
fun installedVersionCode(context: Context): Long {
|
||||
val info = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
return if (Build.VERSION.SDK_INT >= 28) {
|
||||
info.longVersionCode
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
info.versionCode.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkForAppUpdate(context: Context, onStatus: (String) -> Unit): String {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !context.packageManager.canRequestPackageInstalls()) {
|
||||
val settings = Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${context.packageName}"),
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(settings)
|
||||
return "Allow Pit Log to install updates, then check again."
|
||||
}
|
||||
val release = withContext(Dispatchers.IO) { fetchLatest() } ?: return "Couldn't check for an update."
|
||||
if (release.versionCode == 0L) return "No update has been posted."
|
||||
if (release.versionCode <= installedVersionCode(context)) return "This is the latest version."
|
||||
onStatus("Downloading…")
|
||||
val file = withContext(Dispatchers.IO) { downloadApk(context, release.apkUrl) }
|
||||
?: return "Couldn't download the update."
|
||||
promptInstall(context, file)
|
||||
return "Confirm the install when Android asks."
|
||||
}
|
||||
|
||||
private fun fetchLatest(): PostedRelease? {
|
||||
val connection = open(LATEST_RELEASE) ?: return null
|
||||
return try {
|
||||
val code = connection.responseCode
|
||||
if (code == 404) return PostedRelease(0, "")
|
||||
if (code !in 200..299) return null
|
||||
val body = connection.inputStream.bufferedReader().use { it.readText() }
|
||||
parseRelease(body)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRelease(body: String): PostedRelease? {
|
||||
val json = JSONObject(body)
|
||||
val versionCode = json.optString("tag_name").removePrefix("v").toLongOrNull() ?: return null
|
||||
val assets = json.optJSONArray("assets") ?: return null
|
||||
for (index in 0 until assets.length()) {
|
||||
val asset = assets.optJSONObject(index) ?: continue
|
||||
val name = asset.optString("name")
|
||||
val url = asset.optString("browser_download_url")
|
||||
if (name.endsWith(".apk") && url.startsWith("https://")) {
|
||||
return PostedRelease(versionCode, url)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun downloadApk(context: Context, apkUrl: String): File? {
|
||||
val connection = open(apkUrl) ?: return null
|
||||
return try {
|
||||
if (connection.responseCode !in 200..299) return null
|
||||
val length = connection.contentLengthLong
|
||||
if (length > MAX_APK_BYTES) return null
|
||||
val dir = File(context.cacheDir, "updates").apply { mkdirs() }
|
||||
val file = File(dir, "pit-log.apk")
|
||||
connection.inputStream.use { input ->
|
||||
file.outputStream().use { output ->
|
||||
val buffer = ByteArray(8192)
|
||||
var total = 0L
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) break
|
||||
total += read
|
||||
if (total > MAX_APK_BYTES) {
|
||||
file.delete()
|
||||
return null
|
||||
}
|
||||
output.write(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
}
|
||||
val zip = file.inputStream().use { stream -> stream.read() == 0x50 && stream.read() == 0x4B }
|
||||
if (!zip) {
|
||||
file.delete()
|
||||
return null
|
||||
}
|
||||
file
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptInstall(context: Context, file: File) {
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
clipData = ClipData.newRawUri("Pit Log", uri)
|
||||
}
|
||||
context.startActivity(install)
|
||||
}
|
||||
|
||||
private fun open(address: String): HttpURLConnection? {
|
||||
return try {
|
||||
(URL(address).openConnection() as HttpURLConnection).apply {
|
||||
instanceFollowRedirects = true
|
||||
connectTimeout = 20_000
|
||||
readTimeout = 120_000
|
||||
setRequestProperty("User-Agent", "PitLog")
|
||||
setRequestProperty("Accept", "application/json, application/vnd.android.package-archive")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user