Add settings to disable battery optimizations and to make the player service unkillable

This commit is contained in:
vfsfitvnm
2022-07-11 20:05:24 +02:00
parent 3acde3a3ba
commit 742e8702e5
9 changed files with 354 additions and 49 deletions

View File

@@ -6,6 +6,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.content.getSystemService
inline fun <reified T> Context.intent(): Intent =
Intent(this@Context, T::class.java)
@@ -20,4 +23,11 @@ inline fun <reified T: Activity> Context.activityPendingIntent(
requestCode: Int = 0,
flags: Int = if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0,
): PendingIntent =
PendingIntent.getActivity(this, requestCode, intent<T>(), flags)
PendingIntent.getActivity(this, requestCode, intent<T>(), flags)
val Context.isIgnoringBatteryOptimizations: Boolean
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getSystemService<PowerManager>()?.isIgnoringBatteryOptimizations(packageName) ?: true
} else {
true
}

View File

@@ -0,0 +1,116 @@
package it.vfsfitvnm.vimusic.utils
import android.app.Notification
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Binder
import android.os.Build
import android.os.Handler
import android.os.Looper
// https://stackoverflow.com/q/53502244/16885569
// I found four ways to make the system not kill the stopped foreground service: e.g. when
// the player is paused:
// 1 - Use the solution below - hacky;
// 2 - Do not call stopForeground but provide a button to dismiss the notification - bad UX;
// 3 - Lower the targetSdk (e.g. to 23) - security concerns;
// 4 - Host the service in a separate process - overkill and pathetic.
abstract class InvincibleService : Service() {
protected val handler = Handler(Looper.getMainLooper())
protected abstract val isInvincibilityEnabled: Boolean
protected abstract val notificationId: Int
private var invincibility: Invincibility? = null
private val isAllowedToStartForegroundServices: Boolean
get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || isIgnoringBatteryOptimizations
override fun onBind(intent: Intent?): Binder? {
invincibility?.stop()
invincibility = null
return null
}
override fun onRebind(intent: Intent?) {
invincibility?.stop()
invincibility = null
super.onRebind(intent)
}
override fun onUnbind(intent: Intent?): Boolean {
if (isInvincibilityEnabled && isAllowedToStartForegroundServices) {
invincibility = Invincibility()
}
return true
}
override fun onDestroy() {
invincibility?.stop()
invincibility = null
super.onDestroy()
}
protected fun makeInvincible(isInvincible: Boolean = true) {
if (isInvincible) {
invincibility?.start()
} else {
invincibility?.stop()
}
}
protected abstract fun shouldBeInvincible(): Boolean
protected abstract fun notification(): Notification?
private inner class Invincibility : BroadcastReceiver(), Runnable {
private var isStarted = false
private val intervalMs = 30_000L
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_ON -> handler.post(this)
Intent.ACTION_SCREEN_OFF -> notification()?.let { notification ->
handler.removeCallbacks(this)
startForeground(notificationId, notification)
}
}
}
@Synchronized
fun start() {
if (!isStarted) {
isStarted = true
handler.postDelayed(this, intervalMs)
registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
}
}
@Synchronized
fun stop() {
if (isStarted) {
handler.removeCallbacks(this)
unregisterReceiver(this)
isStarted = false
}
}
override fun run() {
if (shouldBeInvincible() && isAllowedToStartForegroundServices) {
notification()?.let { notification ->
startForeground(notificationId, notification)
stopForeground(false)
handler.postDelayed(this, intervalMs)
}
}
}
}
}

View File

@@ -24,6 +24,7 @@ class Preferences(
initialSkipSilence: Boolean,
initialVolumeNormalization: Boolean,
initialPersistentQueue: Boolean,
initialIsInvincibilityEnabled: Boolean,
) {
constructor(preferences: SharedPreferences) : this(
edit = { action: SharedPreferences.Editor.() -> Unit ->
@@ -39,7 +40,8 @@ class Preferences(
initialExoPlayerDiskCacheMaxSizeBytes = preferences.getLong(Keys.exoPlayerDiskCacheMaxSizeBytes, 512L * 1024 * 1024),
initialSkipSilence = preferences.getBoolean(Keys.skipSilence, false),
initialVolumeNormalization = preferences.getBoolean(Keys.volumeNormalization, false),
initialPersistentQueue = preferences.getBoolean(Keys.persistentQueue, false)
initialPersistentQueue = preferences.getBoolean(Keys.persistentQueue, false),
initialIsInvincibilityEnabled = preferences.getBoolean(Keys.isInvincibilityEnabled, false),
)
var songSortBy = initialSongSortBy
@@ -75,6 +77,9 @@ class Preferences(
var persistentQueue = initialPersistentQueue
set(value) = edit { putBoolean(Keys.persistentQueue, value) }
var isInvincibilityEnabled = initialIsInvincibilityEnabled
set(value) = edit { putBoolean(Keys.isInvincibilityEnabled, value) }
object Keys {
const val songSortOrder = "songSortOrder"
const val songSortBy = "songSortBy"
@@ -87,6 +92,7 @@ class Preferences(
const val skipSilence = "skipSilence"
const val volumeNormalization = "volumeNormalization"
const val persistentQueue = "persistentQueue"
const val isInvincibilityEnabled = "isInvincibilityEnabled"
}
companion object {