android: updates - honour the charging constraint, real metered check

- drop the in-process 12 h loop: the VPN keeps the process alive, so it
  checked and pre-downloaded on battery, defeating the worker constraint
- the worker now waits for the check and a silent download it starts
- metered is judged by the physical network: the VPN reports itself
  unmetered, so pre-downloads ran over cellular while connected

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-10 09:15:10 +07:00
1 parent 6668dcfb56
commit 5f8b813eb4
2 files changed
+41 -17

No files matched your search

@@ -29,7 +29,12 @@ class UpdateCheckWorker(context: Context, params: WorkerParameters) : CoroutineW
{ {
override suspend fun doWork(): Result override suspend fun doWork(): Result
{ {
UpdateManager.getInstance(applicationContext).checkIfDue() /* wait for the check (and a silent pre-download it starts): returning
* early lets the system reclaim the process mid-request, outside the
* worker's charging/network constraints */
val manager = UpdateManager.getInstance(applicationContext)
manager.checkIfDue()?.join()
manager.awaitDownload()
return Result.success() return Result.success()
} }
@@ -26,6 +26,7 @@ import android.content.SharedPreferences
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
@@ -41,7 +42,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -51,6 +51,7 @@ import org.strongswan.android.R
import org.strongswan.android.logic.DiagLog import org.strongswan.android.logic.DiagLog
import org.strongswan.android.logic.StrongSwanApplication import org.strongswan.android.logic.StrongSwanApplication
import org.strongswan.android.logic.VpnStateService import org.strongswan.android.logic.VpnStateService
import org.strongswan.android.logic.connectivity.UnderlyingNetwork
import org.strongswan.android.ui.MainActivity import org.strongswan.android.ui.MainActivity
import org.strongswan.android.utils.Constants import org.strongswan.android.utils.Constants
import java.io.File import java.io.File
@@ -198,13 +199,16 @@ class UpdateManager private constructor(private val context: Context)
.build() .build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork( WorkManager.getInstance(context).enqueueUniquePeriodicWork(
UpdateCheckWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request) UpdateCheckWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request)
mScope.launch { /* no in-process timer: the VPN service keeps the process alive, so a
while (true) * loop here would check (and pre-download) on battery every 12 h and
{ * defeat the worker's charging constraint. The worker and the check
checkIfDue() * on app resume cover it. */
delay(Constants.UPDATE_CHECK_INTERVAL_MS) }
}
} /** Waits for a running silent download (used by the worker). */
suspend fun awaitDownload()
{
mDownloadJob?.join()
} }
/** /**
@@ -212,17 +216,18 @@ class UpdateManager private constructor(private val context: Context)
* last-check timestamp). Called by the periodic worker, the in-process * last-check timestamp). Called by the periodic worker, the in-process
* loop and on app resume. * loop and on app resume.
*/ */
fun checkIfDue() fun checkIfDue(): Job?
{ {
if (!isAutoCheckEnabled() || mCheckJob?.isActive == true || mDownloadJob?.isActive == true) if (!isAutoCheckEnabled() || mCheckJob?.isActive == true || mDownloadJob?.isActive == true)
{ {
return return null
} }
val last = mPrefs.getLong(Constants.PREF_LAST_UPDATE_CHECK, 0L) val last = mPrefs.getLong(Constants.PREF_LAST_UPDATE_CHECK, 0L)
if (System.currentTimeMillis() - last >= Constants.UPDATE_CHECK_INTERVAL_MS) if (System.currentTimeMillis() - last >= Constants.UPDATE_CHECK_INTERVAL_MS)
{ {
check() return check()
} }
return null
} }
fun isAutoCheckEnabled(): Boolean fun isAutoCheckEnabled(): Boolean
@@ -239,13 +244,13 @@ class UpdateManager private constructor(private val context: Context)
} }
@Synchronized @Synchronized
private fun check() private fun check(): Job?
{ {
if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true) if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true)
{ {
return return mCheckJob?.takeIf { it.isActive }
} }
mCheckJob = mScope.launch { val job = mScope.launch {
mState.value = UpdateState.Checking mState.value = UpdateState.Checking
val info = fetchManifest() ?: run { val info = fetchManifest() ?: run {
mInstallPending = false mInstallPending = false
@@ -274,6 +279,8 @@ class UpdateManager private constructor(private val context: Context)
mState.value = UpdateState.UpToDate mState.value = UpdateState.UpToDate
} }
} }
mCheckJob = job
return job
} }
/** /**
@@ -348,8 +355,20 @@ class UpdateManager private constructor(private val context: Context)
private fun isNetworkMetered(): Boolean private fun isNetworkMetered(): Boolean
{ {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return true
return cm?.isActiveNetworkMetered ?: true /* the VPN marks itself unmetered (setMetered(false)), so with the tunnel
* up the active network always looks unmetered - judge by the physical
* network below it instead */
val underlying = UnderlyingNetwork.resolve(context) ?: return cm.isActiveNetworkMetered
val caps = try
{
cm.getNetworkCapabilities(underlying)
}
catch (_: Exception)
{
null
} ?: return true
return !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
} }
private fun isVpnDown(): Boolean private fun isVpnDown(): Boolean