android: updates - no stuck Downloading state, thread-safe singleton

After a silent pre-download the state stayed Downloading, so tapping the
"update ready" notification did nothing. It now returns to Available;
an install request without a known update (e.g. after a process restart)
re-checks and installs when found.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-10 09:09:40 +07:00
1 parent e6c893219c
commit 24e536b9b9
1 file changed
+52 -6
@@ -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 {