refactor(android): share the foreground wake lock and start helper between the two services

DownloadService and BackgroundWorkService each built the same partial
wake lock (30-minute timeout, 15-minute renew, not reference-counted),
the same dataSync startForeground SDK branch and the same content
PendingIntent to MainActivity. ForegroundWorkLock now holds the lock and
the shared constants, and two small helpers cover the start branch and
the intent.

The renew strategies stay per service: DownloadService renews on its
progress and maintenance calls once 15 minutes have passed, while
BackgroundWorkService re-acquires from a main-thread Handler timer.
Wake-lock tags, channel IDs, notification IDs, intent flags and request
codes are unchanged, and the verification intent keeps its own request
code and extras.
This commit is contained in:
zarzet committed 2026-10-19 04:58:25 +07:00
1 parent 4791f55ad8
commit 071d72b255
3 files changed
+126 -99

No files matched your search

@@ -3,16 +3,13 @@ package com.zarz.spotiflac
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import androidx.core.app.NotificationCompat
/**
@@ -31,8 +28,6 @@ class BackgroundWorkService : Service() {
private const val CHANNEL_ID = "background_work"
private const val NOTIFICATION_ID = 1002
private const val WAKELOCK_TAG = "SpotiFLAC:BackgroundWorkWakeLock"
private const val WAKELOCK_TIMEOUT_MS = 30 * 60 * 1000L
private const val WAKELOCK_RENEW_INTERVAL_MS = 15 * 60 * 1000L
private val works = LinkedHashMap<String, Work>()
private val mainHandler = Handler(Looper.getMainLooper())
@@ -84,13 +79,13 @@ class BackgroundWorkService : Service() {
fun isActive(kind: String): Boolean = synchronized(works) { works.containsKey(kind) }
}
private var wakeLock: PowerManager.WakeLock? = null
private val wakeLock = ForegroundWorkLock(this, WAKELOCK_TAG, "BackgroundWorkService")
private var inForeground = false
private val renewWakeLock = object : Runnable {
override fun run() {
if (!inForeground) return
wakeLock?.acquire(WAKELOCK_TIMEOUT_MS)
mainHandler.postDelayed(this, WAKELOCK_RENEW_INTERVAL_MS)
wakeLock.reacquire()
mainHandler.postDelayed(this, ForegroundWorkLock.RENEW_INTERVAL_MS)
}
}
@@ -119,15 +114,7 @@ class BackgroundWorkService : Service() {
val current = latestWork() ?: Work("SpotiFLAC", "", -1)
if (!inForeground) {
val notification = buildNotification(current)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
startDataSyncForeground(NOTIFICATION_ID, notification)
inForeground = true
acquireWakeLock()
}
@@ -163,16 +150,7 @@ class BackgroundWorkService : Service() {
private fun stopWork() {
mainHandler.removeCallbacks(renewWakeLock)
wakeLock?.let {
if (it.isHeld) {
try {
it.release()
} catch (e: RuntimeException) {
android.util.Log.w("BackgroundWorkService", "WakeLock release failed: ${e.message}")
}
}
}
wakeLock = null
wakeLock.release()
if (inForeground) {
inForeground = false
stopForeground(STOP_FOREGROUND_REMOVE)
@@ -180,22 +158,12 @@ class BackgroundWorkService : Service() {
}
private fun acquireWakeLock() {
if (wakeLock?.isHeld == true) return
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply {
setReferenceCounted(false)
acquire(WAKELOCK_TIMEOUT_MS)
}
mainHandler.postDelayed(renewWakeLock, WAKELOCK_RENEW_INTERVAL_MS)
if (!wakeLock.acquireIfNotHeld()) return
mainHandler.postDelayed(renewWakeLock, ForegroundWorkLock.RENEW_INTERVAL_MS)
}
private fun buildNotification(work: Work): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val pendingIntent = mainActivityContentIntent()
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(work.title)
.setSmallIcon(android.R.drawable.stat_sys_download)
@@ -7,12 +7,10 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.SystemClock
import android.util.AtomicFile
import androidx.core.app.NotificationCompat
@@ -56,8 +54,6 @@ class DownloadService : Service() {
private const val DOWNLOAD_RESULT_NOTIFICATION_ID = 1
private const val VERIFICATION_REQUIRED_NOTIFICATION_ID = 4
private const val WAKELOCK_TAG = "SpotiFLAC:DownloadWakeLock"
private const val WAKELOCK_RENEW_MS = 30 * 60 * 1000L
private const val WAKELOCK_RENEW_INTERVAL_MS = 15 * 60 * 1000L
const val ACTION_START = "com.zarz.spotiflac.action.START_DOWNLOAD"
const val ACTION_STOP = "com.zarz.spotiflac.action.STOP_DOWNLOAD"
@@ -339,8 +335,7 @@ class DownloadService : Service() {
@Volatile internal var pendingNativeItemsSnapshotJob: Job? = null
private var nativeWorkerRequestChannel: Channel<NativeDownloadRequest>? = null
@Volatile private var nativeWorkerPreparationComplete = true
private var wakeLock: PowerManager.WakeLock? = null
@Volatile private var wakeLockLastRenewedAt = 0L
private val wakeLock = ForegroundWorkLock(this, WAKELOCK_TAG, "DownloadService")
private var currentTrackName = ""
private var currentArtistName = ""
internal var currentStatus = "preparing"
@@ -668,15 +663,7 @@ class DownloadService : Service() {
ensureWakeLock()
val notification = buildNotification(0, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
startDataSyncForeground(NOTIFICATION_ID, notification)
pushWidgetState(0, 0)
}
@@ -1461,42 +1448,12 @@ class DownloadService : Service() {
@Synchronized
private fun ensureWakeLock() {
val existingWakeLock = wakeLock
if (existingWakeLock?.isHeld == true) {
val now = SystemClock.elapsedRealtime()
if (now - wakeLockLastRenewedAt >= WAKELOCK_RENEW_INTERVAL_MS) {
existingWakeLock.acquire(WAKELOCK_RENEW_MS)
wakeLockLastRenewedAt = now
}
return
}
if (existingWakeLock != null) {
wakeLock = null
}
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
WAKELOCK_TAG
).apply {
setReferenceCounted(false)
acquire(WAKELOCK_RENEW_MS)
}
wakeLockLastRenewedAt = SystemClock.elapsedRealtime()
wakeLock.ensure()
}
@Synchronized
private fun releaseWakeLock() {
val existingWakeLock = wakeLock
wakeLock = null
wakeLockLastRenewedAt = 0L
if (existingWakeLock?.isHeld == true) {
try {
existingWakeLock.release()
} catch (e: RuntimeException) {
android.util.Log.w("DownloadService", "WakeLock release failed: ${e.message}")
}
}
wakeLock.release()
}
@Synchronized
@@ -1629,12 +1586,7 @@ class DownloadService : Service() {
}
private fun buildNotification(progress: Long, total: Long): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val pendingIntent = mainActivityContentIntent()
val title = if (queueCount > 1) {
"Downloading $queueCount tracks"
@@ -1690,12 +1642,7 @@ class DownloadService : Service() {
}
private fun showNativeQueueComplete(counts: NativeWorkerCounts) {
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val pendingIntent = mainActivityContentIntent()
val title = if (counts.failed > 0) {
"Downloads Finished (${counts.completed} done, ${counts.failed} failed)"
} else {
@@ -0,0 +1,112 @@
package com.zarz.spotiflac
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.PowerManager
import android.os.SystemClock
/**
* Partial wake lock held by a dataSync foreground service while it works.
*
* The lock is not reference-counted and every acquire uses a 30-minute
* timeout, so a lock that is never released expires on its own. Callers keep
* it alive by renewing it at least every 15 minutes. The class is not
* thread-safe; callers synchronize as their service requires.
*/
internal class ForegroundWorkLock(
private val context: Context,
private val tag: String,
private val logTag: String,
) {
companion object {
const val TIMEOUT_MS = 30 * 60 * 1000L
const val RENEW_INTERVAL_MS = 15 * 60 * 1000L
}
private var wakeLock: PowerManager.WakeLock? = null
@Volatile private var lastRenewedAt = 0L
/**
* Acquires a fresh lock unless one is held. When one is held it is renewed
* once [RENEW_INTERVAL_MS] has passed since the last acquire.
*/
fun ensure() {
val existingWakeLock = wakeLock
if (existingWakeLock?.isHeld == true) {
val now = SystemClock.elapsedRealtime()
if (now - lastRenewedAt >= RENEW_INTERVAL_MS) {
existingWakeLock.acquire(TIMEOUT_MS)
lastRenewedAt = now
}
return
}
if (existingWakeLock != null) {
wakeLock = null
}
acquireNew()
}
/** Acquires a fresh lock unless one is held; returns false when one was held. */
fun acquireIfNotHeld(): Boolean {
if (wakeLock?.isHeld == true) return false
acquireNew()
return true
}
/** Re-acquires the current lock, if any, for another [TIMEOUT_MS]. */
fun reacquire() {
wakeLock?.acquire(TIMEOUT_MS)
}
fun release() {
val existingWakeLock = wakeLock
wakeLock = null
lastRenewedAt = 0L
if (existingWakeLock?.isHeld == true) {
try {
existingWakeLock.release()
} catch (e: RuntimeException) {
android.util.Log.w(logTag, "WakeLock release failed: ${e.message}")
}
}
}
private fun acquireNew() {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
tag
).apply {
setReferenceCounted(false)
acquire(TIMEOUT_MS)
}
lastRenewedAt = SystemClock.elapsedRealtime()
}
}
/** Promotes the service to the foreground with the dataSync type where supported. */
internal fun Service.startDataSyncForeground(notificationId: Int, notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
notificationId,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(notificationId, notification)
}
}
/** Content intent that opens [MainActivity] from a service notification. */
internal fun Context.mainActivityContentIntent(): PendingIntent =
PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)