diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateCheckWorker.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateCheckWorker.kt index 4c155d7ac..5f8e89e7e 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateCheckWorker.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateCheckWorker.kt @@ -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() } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt index e6ba93fe1..e071e8ca0 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/updates/UpdateManager.kt @@ -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