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 <noreply@anthropic.com>
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_NOTIFICATION_ID = 1001
private const val UPDATES_PROGRESS_NOTIFICATION_ID = 1002 private const val UPDATES_PROGRESS_NOTIFICATION_ID = 1002
@Volatile
private var mInstance: UpdateManager? = null 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 @JvmStatic
fun getInstance(context: Context): UpdateManager 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 mCheckJob: Job? = null
private var mDownloadJob: 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 var mVpnService: VpnStateService? = null
private val mConnection = object : ServiceConnection private val mConnection = object : ServiceConnection
@@ -228,6 +238,7 @@ class UpdateManager private constructor(private val context: Context)
check() check()
} }
@Synchronized
private fun check() private fun check()
{ {
if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true) if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true)
@@ -236,17 +247,29 @@ class UpdateManager private constructor(private val context: Context)
} }
mCheckJob = mScope.launch { mCheckJob = mScope.launch {
mState.value = UpdateState.Checking 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() mPrefs.edit().putLong(Constants.PREF_LAST_UPDATE_CHECK, System.currentTimeMillis()).apply()
if (info.versionCode > currentVersionCode()) if (info.versionCode > currentVersionCode())
{ {
DiagLog.i(TAG, "update: available ${info.versionName} (${info.versionCode}), " + DiagLog.i(TAG, "update: available ${info.versionName} (${info.versionCode}), " +
"installed ${currentVersionName()} (${currentVersionCode()})") "installed ${currentVersionName()} (${currentVersionCode()})")
mState.value = UpdateState.Available(info) mState.value = UpdateState.Available(info)
maybeDownloadSilently(info) if (mInstallPending)
{
mInstallPending = false
downloadAndInstall()
}
else
{
maybeDownloadSilently(info)
}
} }
else else
{ {
mInstallPending = false
DiagLog.i(TAG, "update: up to date (${currentVersionName()} ${currentVersionCode()})") DiagLog.i(TAG, "update: up to date (${currentVersionName()} ${currentVersionCode()})")
mState.value = UpdateState.UpToDate 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 * right away; otherwise a notification tells the user it is ready - one
* tap installs it (a single system confirmation on Android < 12) * tap installs it (a single system confirmation on Android < 12)
*/ */
@Synchronized
private fun maybeDownloadSilently(info: UpdateInfo) private fun maybeDownloadSilently(info: UpdateInfo)
{ {
if (!isSilentUpdatesEnabled()) if (!isSilentUpdatesEnabled())
@@ -292,7 +316,19 @@ class UpdateManager private constructor(private val context: Context)
else else
{ {
DiagLog.i(TAG, "update: ${info.versionName} downloaded, waiting for the user") 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) catch (e: Exception)
@@ -300,6 +336,7 @@ class UpdateManager private constructor(private val context: Context)
Log.e(TAG, "Silent update download failed", e) Log.e(TAG, "Silent update download failed", e)
DiagLog.i(TAG, "update: silent download failed: ${e.javaClass.simpleName}: ${e.message}") DiagLog.i(TAG, "update: silent download failed: ${e.javaClass.simpleName}: ${e.message}")
/* the manual download from the settings screen still works */ /* 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 * Downloads the update APK (with progress) and installs it as an
* in-place update (the app data is preserved) * in-place update (the app data is preserved)
*/ */
@Synchronized
fun downloadAndInstall() fun downloadAndInstall()
{ {
val info = (mState.value as? UpdateState.Available)?.info ?: return
if (mDownloadJob?.isActive == true) 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 return
} }
mDownloadJob = mScope.launch { mDownloadJob = mScope.launch {