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:
1 parent
e6c893219c
commit
24e536b9b9
1 file changed
+52
-6
+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 {
|
||||||
|
|||||||
Reference in new issue
Block a user