mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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:
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
|
||||
)
|
||||
Reference in new issue
Block a user