diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt index 862f6acc5..e6ba93fe1 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt @@ -116,12 +116,18 @@ class UpdateManager private constructor(private val context: Context) private const val UPDATES_NOTIFICATION_ID = 1001 private const val UPDATES_PROGRESS_NOTIFICATION_ID = 1002 + @Volatile private var mInstance: UpdateManager? = null + /* called from the main thread and the WorkManager thread: a second + * instance would run its own check loop and state */ @JvmStatic fun getInstance(context: Context): UpdateManager { - return mInstance ?: UpdateManager(context.applicationContext).also { mInstance = it } + return mInstance ?: synchronized(this) + { + mInstance ?: UpdateManager(context.applicationContext).also { mInstance = it } + } } /** @@ -154,6 +160,10 @@ class UpdateManager private constructor(private val context: Context) private var mCheckJob: Job? = null private var mDownloadJob: Job? = null + /* the user asked to install (e.g. tapped "update ready") while no update + * was known in this process: install as soon as the check finds it */ + @Volatile + private var mInstallPending = false private var mVpnService: VpnStateService? = null private val mConnection = object : ServiceConnection @@ -228,6 +238,7 @@ class UpdateManager private constructor(private val context: Context) check() } + @Synchronized private fun check() { if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true) @@ -236,17 +247,29 @@ class UpdateManager private constructor(private val context: Context) } mCheckJob = mScope.launch { mState.value = UpdateState.Checking - val info = fetchManifest() ?: return@launch + val info = fetchManifest() ?: run { + mInstallPending = false + return@launch + } mPrefs.edit().putLong(Constants.PREF_LAST_UPDATE_CHECK, System.currentTimeMillis()).apply() if (info.versionCode > currentVersionCode()) { DiagLog.i(TAG, "update: available ${info.versionName} (${info.versionCode}), " + "installed ${currentVersionName()} (${currentVersionCode()})") mState.value = UpdateState.Available(info) - maybeDownloadSilently(info) + if (mInstallPending) + { + mInstallPending = false + downloadAndInstall() + } + else + { + maybeDownloadSilently(info) + } } else { + mInstallPending = false DiagLog.i(TAG, "update: up to date (${currentVersionName()} ${currentVersionCode()})") mState.value = UpdateState.UpToDate } @@ -260,6 +283,7 @@ class UpdateManager private constructor(private val context: Context) * right away; otherwise a notification tells the user it is ready - one * tap installs it (a single system confirmation on Android < 12) */ + @Synchronized private fun maybeDownloadSilently(info: UpdateInfo) { if (!isSilentUpdatesEnabled()) @@ -292,7 +316,19 @@ class UpdateManager private constructor(private val context: Context) else { DiagLog.i(TAG, "update: ${info.versionName} downloaded, waiting for the user") - notifyUpdateReady(info) + /* back to Available: Downloading would block downloadAndInstall() + * and leave the settings row stuck at 100% */ + mState.value = UpdateState.Available(info) + if (mInstallPending) + { + mInstallPending = false + UpdateInstaller.install(context, apk) + mState.value = UpdateState.Idle + } + else + { + notifyUpdateReady(info) + } } } catch (e: Exception) @@ -300,6 +336,7 @@ class UpdateManager private constructor(private val context: Context) Log.e(TAG, "Silent update download failed", e) DiagLog.i(TAG, "update: silent download failed: ${e.javaClass.simpleName}: ${e.message}") /* the manual download from the settings screen still works */ + mState.value = UpdateState.Available(info) } } } @@ -434,11 +471,20 @@ class UpdateManager private constructor(private val context: Context) * Downloads the update APK (with progress) and installs it as an * in-place update (the app data is preserved) */ + @Synchronized fun downloadAndInstall() { - val info = (mState.value as? UpdateState.Available)?.info ?: return if (mDownloadJob?.isActive == true) - { + { /* a silent download is running: install once it is done */ + mInstallPending = true + return + } + val info = (mState.value as? UpdateState.Available)?.info + if (info == null) + { /* e.g. the "update ready" notification was tapped after the process + * restarted: look the update up again and install it when found */ + mInstallPending = true + check() return } mDownloadJob = mScope.launch {