Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions apps/android/app/src/main/java/net/aurboda/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.KSerializer
import net.aurboda.ui.theme.AurbodaAppTheme
import net.aurboda.update.DownloadState
import net.aurboda.update.UpdateAvailableDialog
import net.aurboda.update.UpdateCheckResult
import net.aurboda.update.UpdateDownloadingDialog
import net.aurboda.update.UpdateErrorDialog
import net.aurboda.update.UpdateReadyToInstallDialog
import net.aurboda.update.VersionInfo
import net.aurboda.update.checkForUpdate
import net.aurboda.update.downloadUpdate
import net.aurboda.update.getExistingDownloadState
import net.aurboda.update.installApk
// Import allRecordTypes from HealthDataModels
import net.aurboda.allRecordTypes
import java.io.File
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.reflect.KClass
Expand Down Expand Up @@ -282,6 +286,8 @@ fun AurbodaApp(initialTab: MainTab? = null) {
var updateAvailable by remember { mutableStateOf<VersionInfo?>(null) }
var showUpdateDialog by remember { mutableStateOf(false) }
var showDownloadingDialog by remember { mutableStateOf(false) }
var showInstallDialog by remember { mutableStateOf(false) }
var downloadedApkFile by remember { mutableStateOf<File?>(null) }
var updateError by remember { mutableStateOf<String?>(null) }

// Check for updates on app launch
Expand All @@ -292,7 +298,22 @@ fun AurbodaApp(initialTab: MainTab? = null) {
is UpdateCheckResult.UpdateAvailable -> {
Log.d("UpdateChecker", "Update available: ${result.versionInfo.versionName}")
updateAvailable = result.versionInfo
showUpdateDialog = true

// Check if we already have this download in progress or finished
when (val downloadState = getExistingDownloadState(context, result.versionInfo.versionName)) {
is DownloadState.Downloaded -> {
Log.d("UpdateChecker", "APK already downloaded: ${downloadState.apkFile.name}")
downloadedApkFile = downloadState.apkFile
showInstallDialog = true
}
is DownloadState.InProgress -> {
Log.d("UpdateChecker", "Download already in progress")
showDownloadingDialog = true
}
is DownloadState.None -> {
showUpdateDialog = true
}
}
}
is UpdateCheckResult.NoUpdate -> {
Log.d("UpdateChecker", "No update available")
Expand All @@ -317,7 +338,8 @@ fun AurbodaApp(initialTab: MainTab? = null) {
onDownloadComplete = { apkFile ->
scope.launch {
showDownloadingDialog = false
installApk(context, apkFile)
downloadedApkFile = apkFile
showInstallDialog = true
}
},
onDownloadFailed = { error ->
Expand All @@ -332,6 +354,17 @@ fun AurbodaApp(initialTab: MainTab? = null) {
)
}

if (showInstallDialog && updateAvailable != null && downloadedApkFile != null) {
UpdateReadyToInstallDialog(
versionInfo = updateAvailable!!,
onInstall = {
showInstallDialog = false
installApk(context, downloadedApkFile!!)
},
onDismiss = { showInstallDialog = false }
)
}

if (showDownloadingDialog) {
UpdateDownloadingDialog(
onDismiss = { showDownloadingDialog = false }
Expand Down
33 changes: 33 additions & 0 deletions apps/android/app/src/main/java/net/aurboda/update/UpdateDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@ fun UpdateAvailableDialog(
)
}

@Composable
fun UpdateReadyToInstallDialog(
versionInfo: VersionInfo,
onInstall: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Update Ready") },
text = {
Column {
Text("Version ${versionInfo.versionName} has been downloaded and is ready to install.")
versionInfo.releaseNotes?.let { notes ->
if (notes.isNotBlank()) {
Spacer(modifier = Modifier.height(8.dp))
Text(notes)
}
}
}
},
confirmButton = {
Button(onClick = onInstall) {
Text("Install")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Later")
}
}
)
}

@Composable
fun UpdateDownloadingDialog(
onDismiss: () -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,83 @@ import androidx.core.content.FileProvider
import java.io.File

private const val TAG = "UpdateDownloader"
private const val UPDATE_PREFS = "AurbodaUpdatePrefs"
private const val KEY_DOWNLOAD_ID = "pendingDownloadId"
private const val KEY_DOWNLOAD_VERSION = "pendingDownloadVersion"

sealed class DownloadState {
data class Downloaded(val apkFile: File) : DownloadState()
data object InProgress : DownloadState()
data object None : DownloadState()
}

fun getExistingDownloadState(context: Context, versionName: String): DownloadState {
val prefs = context.getSharedPreferences(UPDATE_PREFS, Context.MODE_PRIVATE)
val savedVersion = prefs.getString(KEY_DOWNLOAD_VERSION, null)
val savedDownloadId = prefs.getLong(KEY_DOWNLOAD_ID, -1)

// Check if we have a saved download for this version
if (savedVersion == versionName && savedDownloadId != -1L) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query().setFilterById(savedDownloadId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = cursor.getInt(statusIndex)
cursor.close()
return when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
val apkFile = findDownloadedApk(context, versionName)
if (apkFile != null) DownloadState.Downloaded(apkFile)
else {
clearSavedDownload(context)
DownloadState.None
}
}
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING -> {
DownloadState.InProgress
}
else -> {
// Failed or paused — clear and let user retry
clearSavedDownload(context)
DownloadState.None
}
}
}
cursor.close()
// Download ID not found in DownloadManager (e.g., cleared by system)
clearSavedDownload(context)
} else if (savedVersion != versionName) {
// Different version — clear stale download state
clearSavedDownload(context)
}

// No saved download — check if APK file happens to exist
val apkFile = findDownloadedApk(context, versionName)
if (apkFile != null) return DownloadState.Downloaded(apkFile)

return DownloadState.None
}

private fun findDownloadedApk(context: Context, versionName: String): File? {
val downloadDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: return null
val apkFile = File(downloadDir, "aurboda-$versionName.apk")
return if (apkFile.exists() && apkFile.length() > 0) apkFile else null
}

private fun saveDownload(context: Context, downloadId: Long, versionName: String) {
context.getSharedPreferences(UPDATE_PREFS, Context.MODE_PRIVATE).edit()
.putLong(KEY_DOWNLOAD_ID, downloadId)
.putString(KEY_DOWNLOAD_VERSION, versionName)
.apply()
}

private fun clearSavedDownload(context: Context) {
context.getSharedPreferences(UPDATE_PREFS, Context.MODE_PRIVATE).edit()
.remove(KEY_DOWNLOAD_ID)
.remove(KEY_DOWNLOAD_VERSION)
.apply()
}

fun downloadUpdate(
context: Context,
Expand Down Expand Up @@ -41,6 +118,7 @@ fun downloadUpdate(

val downloadId = downloadManager.enqueue(request)
Log.d(TAG, "Started download with ID: $downloadId")
saveDownload(context, downloadId, versionName)

val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
Expand All @@ -60,6 +138,7 @@ fun downloadUpdate(
val reasonIndex = cursor.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = cursor.getInt(reasonIndex)
Log.e(TAG, "Download failed with status $status, reason $reason")
clearSavedDownload(context)
onDownloadFailed("Download failed (reason: $reason)")
}
}
Expand Down
Loading