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
{
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()
}
@@ -26,6 +26,7 @@ import android.content.SharedPreferences
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.IBinder
import android.util.Log
@@ -41,7 +42,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -51,6 +51,7 @@ import org.strongswan.android.R
import org.strongswan.android.logic.DiagLog
import org.strongswan.android.logic.StrongSwanApplication
import org.strongswan.android.logic.VpnStateService
import org.strongswan.android.logic.connectivity.UnderlyingNetwork
import org.strongswan.android.ui.MainActivity
import org.strongswan.android.utils.Constants
import java.io.File
@@ -198,13 +199,16 @@ class UpdateManager private constructor(private val context: Context)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
UpdateCheckWorker.WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request)
mScope.launch {
while (true)
{
checkIfDue()
delay(Constants.UPDATE_CHECK_INTERVAL_MS)
}
}
/* no in-process timer: the VPN service keeps the process alive, so a
* loop here would check (and pre-download) on battery every 12 h and
* defeat the worker's charging constraint. The worker and the check
* on app resume cover it. */
}
/** 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
* loop and on app resume.
*/
fun checkIfDue()
fun checkIfDue(): Job?
{
if (!isAutoCheckEnabled() || mCheckJob?.isActive == true || mDownloadJob?.isActive == true)
{
return
return null
}
val last = mPrefs.getLong(Constants.PREF_LAST_UPDATE_CHECK, 0L)
if (System.currentTimeMillis() - last >= Constants.UPDATE_CHECK_INTERVAL_MS)
{
check()
return check()
}
return null
}
fun isAutoCheckEnabled(): Boolean
@@ -239,13 +244,13 @@ class UpdateManager private constructor(private val context: Context)
}
@Synchronized
private fun check()
private fun check(): Job?
{
if (mCheckJob?.isActive == true || mDownloadJob?.isActive == true)
{
return
return mCheckJob?.takeIf { it.isActive }
}
mCheckJob = mScope.launch {
val job = mScope.launch {
mState.value = UpdateState.Checking
val info = fetchManifest() ?: run {
mInstallPending = false
@@ -274,6 +279,8 @@ class UpdateManager private constructor(private val context: Context)
mState.value = UpdateState.UpToDate
}
}
mCheckJob = job
return job
}
/**
@@ -348,8 +355,20 @@ class UpdateManager private constructor(private val context: Context)
private fun isNetworkMetered(): Boolean
{
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
return cm?.isActiveNetworkMetered ?: true
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return 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