Align Keeper Argus publication flow
This commit is contained in:
@@ -161,6 +161,7 @@ enum class BodyTextStyle {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var authPreferences: AuthPreferences
|
||||
private lateinit var keepSettings: KeepSettings
|
||||
private lateinit var versionCleanup: VersionCleanup
|
||||
private lateinit var authClient: QsferaAuthClient
|
||||
private lateinit var repository: NotesRepository
|
||||
private val updateClient = AppUpdateClient(ARGUS_MANIFEST_URL)
|
||||
@@ -183,10 +184,11 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
authPreferences = AuthPreferences(applicationContext)
|
||||
keepSettings = KeepSettings(applicationContext)
|
||||
versionCleanup = VersionCleanup(applicationContext)
|
||||
authClient = QsferaAuthClient(BuildConfig.QSFERA_CLOUD_BASE_URL)
|
||||
repository = NotesRepository(applicationContext, authPreferences, authClient)
|
||||
configureSystemBars()
|
||||
VersionCleanup(applicationContext).cleanupAfterUpdate(BuildConfig.VERSION_CODE, BuildConfig.VERSION_NAME)
|
||||
versionCleanup.cleanupAfterUpdate(BuildConfig.VERSION_CODE, BuildConfig.VERSION_NAME)
|
||||
ReminderScheduler(applicationContext).ensureNotificationChannel()
|
||||
ensureNotificationPermission()
|
||||
|
||||
@@ -799,12 +801,15 @@ class MainActivity : AppCompatActivity() {
|
||||
checkButton.isEnabled = true
|
||||
updateStatus.text = result.fold(
|
||||
onSuccess = { latest ->
|
||||
if (latest.androidVersionCode > BuildConfig.VERSION_CODE) {
|
||||
val installedVersion = versionCleanup.installedVersionName(BuildConfig.VERSION_NAME)
|
||||
if (latest == null) {
|
||||
getString(R.string.no_update_available)
|
||||
} else if (AppVersionComparator.isRemoteNewer(installedVersion, latest.version)) {
|
||||
availableUpdate = latest
|
||||
installButton.visibility = View.VISIBLE
|
||||
getString(R.string.update_available, latest.version, latest.androidVersionCode)
|
||||
getString(R.string.update_available, latest.version)
|
||||
} else {
|
||||
getString(R.string.up_to_date, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
||||
getString(R.string.up_to_date, installedVersion)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
@@ -827,6 +832,7 @@ class MainActivity : AppCompatActivity() {
|
||||
installButton.isEnabled = true
|
||||
result
|
||||
.onSuccess { apk ->
|
||||
versionCleanup.markUpdateStaged(update.version)
|
||||
updateStatus.text = getString(R.string.opening_installer)
|
||||
installApk(apk)
|
||||
}
|
||||
@@ -3099,13 +3105,13 @@ data class PendingOAuth(
|
||||
|
||||
data class AppUpdateInfo(
|
||||
val version: String,
|
||||
val androidVersionCode: Int,
|
||||
val downloadUrl: String,
|
||||
val sha256: String?,
|
||||
val sha256: String,
|
||||
val packageSizeBytes: Long?,
|
||||
)
|
||||
|
||||
class AppUpdateClient(private val manifestUrl: String) {
|
||||
fun latest(): AppUpdateInfo {
|
||||
fun latest(): AppUpdateInfo? {
|
||||
val connection = (URL(manifestUrl).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
@@ -3116,16 +3122,22 @@ class AppUpdateClient(private val manifestUrl: String) {
|
||||
return try {
|
||||
val response = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
} else if (connection.responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
return null
|
||||
} else {
|
||||
val errorText = connection.errorStream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }.orEmpty()
|
||||
throw IOException("HTTP ${connection.responseCode}: $errorText")
|
||||
}
|
||||
val release = JSONObject(response).getJSONObject("release")
|
||||
val sha256 = release.optString("sha256").trim()
|
||||
if (!sha256.matches(Regex("[0-9a-fA-F]{64}"))) {
|
||||
throw IOException("Argus release sha256 is missing or invalid")
|
||||
}
|
||||
AppUpdateInfo(
|
||||
version = release.getString("version"),
|
||||
androidVersionCode = release.getInt("androidVersionCode"),
|
||||
downloadUrl = release.getString("downloadPath").toAbsoluteArgusUrl(),
|
||||
sha256 = release.optString("sha256").takeIf { it.isNotBlank() }
|
||||
sha256 = sha256,
|
||||
packageSizeBytes = release.optLong("packageSizeBytes", -1L).takeIf { it >= 0L }
|
||||
)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
@@ -3139,7 +3151,10 @@ class AppUpdateClient(private val manifestUrl: String) {
|
||||
updateDir.mkdirs()
|
||||
}
|
||||
val apk = File(updateDir, "keeper-android-${update.version}.apk")
|
||||
val tempApk = File(updateDir, "keeper-android-${update.version}.apk.download")
|
||||
tempApk.delete()
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
var downloadedBytes = 0L
|
||||
val connection = (URL(update.downloadUrl).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
@@ -3153,25 +3168,38 @@ class AppUpdateClient(private val manifestUrl: String) {
|
||||
throw IOException("HTTP ${connection.responseCode}: $errorText")
|
||||
}
|
||||
connection.inputStream.use { input ->
|
||||
apk.outputStream().use { output ->
|
||||
tempApk.outputStream().use { output ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) break
|
||||
digest.update(buffer, 0, read)
|
||||
output.write(buffer, 0, read)
|
||||
downloadedBytes += read
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
tempApk.delete()
|
||||
throw error
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
val actualSha256 = digest.digest().toHexString()
|
||||
val expectedSha256 = update.sha256
|
||||
if (expectedSha256 != null && !expectedSha256.equals(actualSha256, ignoreCase = true)) {
|
||||
apk.delete()
|
||||
if (!update.sha256.equals(actualSha256, ignoreCase = true)) {
|
||||
tempApk.delete()
|
||||
throw IOException("Downloaded APK checksum mismatch")
|
||||
}
|
||||
val expectedSize = update.packageSizeBytes
|
||||
if (expectedSize != null && expectedSize != downloadedBytes) {
|
||||
tempApk.delete()
|
||||
throw IOException("Downloaded APK size mismatch")
|
||||
}
|
||||
apk.delete()
|
||||
if (!tempApk.renameTo(apk)) {
|
||||
tempApk.copyTo(apk, overwrite = true)
|
||||
tempApk.delete()
|
||||
}
|
||||
return apk
|
||||
}
|
||||
|
||||
@@ -3184,6 +3212,32 @@ class AppUpdateClient(private val manifestUrl: String) {
|
||||
}
|
||||
}
|
||||
|
||||
object AppVersionComparator {
|
||||
private val semanticVersionPattern = Regex("""^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$""")
|
||||
|
||||
fun isRemoteNewer(installedVersion: String, remoteVersion: String): Boolean {
|
||||
val installedSemanticVersion = installedVersion.toSemanticVersionParts()
|
||||
val remoteSemanticVersion = remoteVersion.toSemanticVersionParts()
|
||||
if (installedSemanticVersion != null && remoteSemanticVersion != null) {
|
||||
installedSemanticVersion.indices.forEach { index ->
|
||||
val comparison = remoteSemanticVersion[index].compareTo(installedSemanticVersion[index])
|
||||
if (comparison != 0) return comparison > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
return remoteVersion.compareTo(installedVersion, ignoreCase = true) > 0
|
||||
}
|
||||
|
||||
private fun String.toSemanticVersionParts(): List<Int>? {
|
||||
val match = semanticVersionPattern.matchEntire(trim()) ?: return null
|
||||
return listOf(
|
||||
match.groupValues[1].toInt(),
|
||||
match.groupValues[2].toInt(),
|
||||
match.groupValues[3].toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class VersionCleanup(private val context: Context) {
|
||||
private val preferences = context.getSharedPreferences("keeper_runtime", Context.MODE_PRIVATE)
|
||||
|
||||
@@ -3193,10 +3247,26 @@ class VersionCleanup(private val context: Context) {
|
||||
cleanupOldPackageFiles(currentVersionName)
|
||||
preferences.edit()
|
||||
.putInt(KEY_LAST_VERSION_CODE, currentVersionCode)
|
||||
.putString(KEY_INSTALLED_VERSION_NAME, currentVersionName)
|
||||
.apply()
|
||||
} else if (preferences.getString(KEY_INSTALLED_VERSION_NAME, null) != currentVersionName) {
|
||||
preferences.edit()
|
||||
.putString(KEY_INSTALLED_VERSION_NAME, currentVersionName)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun installedVersionName(defaultVersionName: String): String {
|
||||
return preferences.getString(KEY_INSTALLED_VERSION_NAME, null) ?: defaultVersionName
|
||||
}
|
||||
|
||||
fun markUpdateStaged(versionName: String) {
|
||||
preferences.edit()
|
||||
.putString(KEY_STAGED_VERSION_NAME, versionName)
|
||||
.putLong(KEY_STAGED_AT, System.currentTimeMillis())
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun cleanupOldPackageFiles(currentVersionName: String) {
|
||||
packageFileRoots().forEach { root ->
|
||||
if (!root.exists()) return@forEach
|
||||
@@ -3229,6 +3299,9 @@ class VersionCleanup(private val context: Context) {
|
||||
|
||||
private companion object {
|
||||
const val KEY_LAST_VERSION_CODE = "last_version_code"
|
||||
const val KEY_INSTALLED_VERSION_NAME = "installed_version_name"
|
||||
const val KEY_STAGED_VERSION_NAME = "staged_version_name"
|
||||
const val KEY_STAGED_AT = "staged_at"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +127,9 @@
|
||||
<string name="checking_updates">Проверяю обновления…</string>
|
||||
<string name="downloading_update">Скачиваю обновление…</string>
|
||||
<string name="opening_installer">Открываю установщик Android…</string>
|
||||
<string name="up_to_date">Установлена актуальная версия %1$s (%2$d).</string>
|
||||
<string name="update_available">Доступна версия %1$s (%2$d).</string>
|
||||
<string name="up_to_date">Установлена актуальная версия %1$s.</string>
|
||||
<string name="no_update_available">В Argus нет релиза для этого приложения.</string>
|
||||
<string name="update_available">Доступна версия %1$s.</string>
|
||||
<string name="update_check_failed">Не удалось проверить обновления.</string>
|
||||
<string name="update_download_failed">Не удалось скачать обновление.</string>
|
||||
<string name="allow_update_install">Разрешите установку из этого источника и повторите установку обновления.</string>
|
||||
|
||||
Reference in New Issue
Block a user