mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-22 07:06:05 +00:00
feat: Run APK download as foreground (dataSync) work
Long transfers — especially over Tor — can exceed WorkManager's ~10-minute background execution window, getting the worker stopped and rescheduled repeatedly. Promote the download to foreground work with a progress notification (cancel action included) so it can run to completion. - setForeground() with FOREGROUND_SERVICE_TYPE_DATA_SYNC; the manifest already holds the FOREGROUND_SERVICE_DATA_SYNC permission, and the WorkManager SystemForegroundService is merged with type dataSync. - If Android 12+ rejects the promotion (app backgrounded), the worker logs and continues as regular background work, relying on Range-resume. - Notification updates are throttled to 5% steps and degrade gracefully without POST_NOTIFICATIONS. Addresses the Codex review finding on ApkDownloadWorker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9700e9cc92
commit
13f940271b
@ -151,5 +151,12 @@
|
||||
android:label="Share BitChat"
|
||||
android:theme="@style/Theme.BitchatAndroid"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<!-- Declare the foreground service type for WorkManager's foreground
|
||||
service so the APK download worker can run as dataSync work -->
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.bitchat.android.R
|
||||
|
||||
/**
|
||||
* WorkManager worker that downloads the universal APK in the background.
|
||||
* Survives app backgrounding and process death. Transient network errors are
|
||||
* retried with backoff; partial downloads resume via HTTP Range requests.
|
||||
*
|
||||
* Runs as foreground (dataSync) work when possible so slow transfers (e.g.
|
||||
* over Tor) are not killed by WorkManager's background execution window.
|
||||
*/
|
||||
class ApkDownloadWorker(
|
||||
appContext: Context,
|
||||
@ -28,15 +39,34 @@ class ApkDownloadWorker(
|
||||
const val KEY_RESUMABLE_PERCENT = "resumable_percent"
|
||||
|
||||
private const val MAX_RETRIES = 3
|
||||
|
||||
private const val CHANNEL_ID = "apk_download"
|
||||
private const val NOTIFICATION_ID = 4201
|
||||
private const val NOTIFY_STEP_PERCENT = 5
|
||||
}
|
||||
|
||||
private val apkManager = UniversalApkManager(applicationContext)
|
||||
private val notificationManager =
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private var lastNotifiedProgress = -NOTIFY_STEP_PERCENT
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d(TAG, "Starting APK download work")
|
||||
|
||||
// Promote to foreground so long transfers aren't stopped by the
|
||||
// ~10-minute background execution window. Android 12+ can reject the
|
||||
// promotion when the app is backgrounded — continue as regular
|
||||
// background work and rely on Range-resume in that case.
|
||||
try {
|
||||
setForeground(createForegroundInfo(apkManager.getPartialDownloadProgress() ?: 0))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not promote download to foreground work", e)
|
||||
}
|
||||
|
||||
val result = apkManager.downloadUniversalApk { progress ->
|
||||
setProgressAsync(Data.Builder().putInt(KEY_PROGRESS, progress).build())
|
||||
updateNotification(progress)
|
||||
}
|
||||
|
||||
return if (result.isSuccess) {
|
||||
@ -69,4 +99,63 @@ class ApkDownloadWorker(
|
||||
Result.failure(outputData)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return createForegroundInfo(apkManager.getPartialDownloadProgress() ?: 0)
|
||||
}
|
||||
|
||||
private fun createForegroundInfo(progress: Int): ForegroundInfo {
|
||||
ensureChannel()
|
||||
val notification = buildNotification(progress)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(progress: Int): android.app.Notification {
|
||||
val cancelIntent = WorkManager.getInstance(applicationContext)
|
||||
.createCancelPendingIntent(id)
|
||||
|
||||
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
.setContentTitle(applicationContext.getString(R.string.apk_download_notification_title))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setProgress(100, progress, progress <= 0)
|
||||
.addAction(
|
||||
android.R.drawable.ic_delete,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
cancelIntent
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateNotification(progress: Int) {
|
||||
if (progress - lastNotifiedProgress < NOTIFY_STEP_PERCENT) return
|
||||
lastNotifiedProgress = progress
|
||||
try {
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification(progress))
|
||||
} catch (e: Exception) {
|
||||
// Missing POST_NOTIFICATIONS permission just drops the update;
|
||||
// the download itself is unaffected.
|
||||
Log.w(TAG, "Could not update download notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
applicationContext.getString(R.string.apk_download_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,6 +185,8 @@
|
||||
<string name="prepare_apk_required">Please prepare the app for sharing first.</string>
|
||||
<string name="prepare_apk_download_interrupted">Download interrupted</string>
|
||||
<string name="prepare_apk_download_cancelled">Download cancelled</string>
|
||||
<string name="apk_download_notification_title">Downloading universal APK</string>
|
||||
<string name="apk_download_channel_name">APK downloads</string>
|
||||
|
||||
<!-- Hotspot Sharing -->
|
||||
<string name="hotspot_share_via">Share via Hotspot</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user