Improve music playback stopping

This commit is contained in:
Jonas Lochmann 2021-01-25 01:00:00 +01:00
parent 9a8ccc193f
commit bc12c5e2b4
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
8 changed files with 126 additions and 35 deletions

View file

@ -45,7 +45,8 @@ abstract class PlatformIntegration(
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
abstract fun showAnnoyScreen(annoyDuration: Long)
abstract fun muteAudioIfPossible(packageName: String)
// true = success
abstract suspend fun muteAudioIfPossible(packageName: String): Boolean
abstract fun setShowBlockingOverlay(show: Boolean, blockedElement: String? = null)
// this should throw an SecurityException if the permission is missing
abstract suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp>

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -25,6 +25,8 @@ import android.content.*
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import android.os.Build
@ -52,6 +54,8 @@ import io.timelimit.android.ui.manipulation.AnnoyActivity
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.system.exitProcess
@ -84,6 +88,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
private val overlay = OverlayUtil(context as Application)
private val battery = BatteryStatusUtil(context)
private val connectedNetwork = ConnectedNetworkUtil(context)
private val muteAudioMutex = Mutex()
init {
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
@ -151,11 +156,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
return sessions.find {
it.playbackState?.state == PlaybackState.STATE_PLAYING ||
it.playbackState?.state == PlaybackState.STATE_FAST_FORWARDING ||
it.playbackState?.state == PlaybackState.STATE_REWINDING
}?.packageName
return sessions.find { isPlaying(it) }?.packageName
}
}
@ -270,24 +271,89 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
AnnoyActivity.start(context, annoyDuration)
}
override fun muteAudioIfPossible(packageName: String) {
override suspend fun muteAudioIfPossible(packageName: String): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
val sessionsOfTheApp = sessions.filter { it.packageName == packageName }
sessionsOfTheApp.forEach { session ->
session.dispatchMediaButtonEvent(KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_MEDIA_STOP
))
session.dispatchMediaButtonEvent(KeyEvent(
KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_MEDIA_STOP
))
muteAudioMutex.withLock {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "muteAudioIfPossible($packageName)")
}
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
fun getAppSessions(): List<MediaController> {
return manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
.filter { it.packageName == packageName }
}
fun dispatchKey(sessions: List<MediaController>, key: Int) {
sessions.forEach {
it.dispatchMediaButtonEvent(KeyEvent(
KeyEvent.ACTION_DOWN,
key
))
it.dispatchMediaButtonEvent(KeyEvent(
KeyEvent.ACTION_UP,
key
))
}
}
kotlin.run {
val sessions = getAppSessions()
if (sessions.find { isPlaying(it) } == null) return true
if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "try KEYCODE_MEDIA_STOP") }
dispatchKey(sessions, KeyEvent.KEYCODE_MEDIA_STOP)
}
delay(100)
kotlin.run {
val sessions = getAppSessions()
if (sessions.find { isPlaying(it) } == null) return true
if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "try KEYCODE_HEADSETHOOK") }
dispatchKey(sessions, KeyEvent.KEYCODE_HEADSETHOOK)
}
delay(500)
kotlin.run {
val sessions = getAppSessions()
if (sessions.find { isPlaying(it) } == null) return true
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val listener = AudioManager.OnAudioFocusChangeListener {/* ignored */}
if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "try audio focus") }
if (
audioManager.requestAudioFocus(listener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_GRANTED
) {
if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "got audio focus") }
delay(100)
audioManager.abandonAudioFocus(listener)
}
}
kotlin.run {
val sessions = getAppSessions()
if (sessions.find { isPlaying(it) } == null) return true
}
if (BuildConfig.DEBUG) { Log.d(LOG_TAG, "playback still running") }
}
}
}
return false
}
override fun setShowBlockingOverlay(show: Boolean, blockedElement: String?) {
@ -535,4 +601,10 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
}
override fun getCurrentNetworkId(): NetworkId = connectedNetwork.getNetworkId()
private fun isPlaying(session: MediaController): Boolean {
return session.playbackState?.state == PlaybackState.STATE_PLAYING ||
session.playbackState?.state == PlaybackState.STATE_FAST_FORWARDING ||
session.playbackState?.state == PlaybackState.STATE_REWINDING
}
}

View file

@ -67,8 +67,6 @@ class NotificationListener: NotificationListenerService() {
lastOngoingNotificationHidden.remove(sbn.packageName)
}
} else {
appLogic.platformIntegration.muteAudioIfPossible(sbn.packageName)
val success = try {
if (sbn.isOngoing && SUPPORTS_HIDING_ONGOING_NOTIFICATIONS) {
// only snooze for 5 seconds to show it again soon

View file

@ -98,8 +98,8 @@ class DummyIntegration(
// ignore
}
override fun muteAudioIfPossible(packageName: String) {
// ignore
override suspend fun muteAudioIfPossible(packageName: String): Boolean {
return false
}
override fun setShowBlockingOverlay(show: Boolean, blockedElement: String?) {

View file

@ -119,6 +119,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private val usedTimeUpdateHelper = UsedTimeUpdateHelper(appLogic)
private var previousMainLogicExecutionTime = 0
private var previousMainLoopEndTime = 0L
private var previousAudioPlaybackBlock: Pair<Long, String>? = null
private val dayChangeTracker = DayChangeTracker(
timeApi = appLogic.timeApi,
longDuration = 1000 * 60 * 10 /* 10 minutes */
@ -703,7 +704,31 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
}
if (blockAudioPlayback && audioPlaybackPackageName != null) {
appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)
val currentAudioBlockUptime = appLogic.timeApi.getCurrentUptimeInMillis()
val oldAudioPlaybackBlock = previousAudioPlaybackBlock
val skipAudioBlock = oldAudioPlaybackBlock != null &&
oldAudioPlaybackBlock.second == audioPlaybackPackageName &&
oldAudioPlaybackBlock.first >= currentAudioBlockUptime
if (!skipAudioBlock) {
val newAudioPlaybackBlock = currentAudioBlockUptime + 1000 * 10 /* block for 10 seconds */ to audioPlaybackPackageName
previousAudioPlaybackBlock = newAudioPlaybackBlock
runAsync {
if (appLogic.platformIntegration.muteAudioIfPossible(audioPlaybackPackageName)) {
appLogic.platformIntegration.showOverlayMessage(appLogic.context.getString(R.string.background_logic_toast_block_audio))
// allow blocking again
// no locking needed because everything happens on the main thread
if (previousAudioPlaybackBlock === newAudioPlaybackBlock) {
previousAudioPlaybackBlock = appLogic.timeApi.getCurrentUptimeInMillis() + 1000 * 1 /* block for 1 more second */ to audioPlaybackPackageName
}
} else {
appLogic.platformIntegration.showOverlayMessage(appLogic.context.getString(R.string.background_logic_toast_block_audio_failed))
}
}
}
}
} catch (ex: SecurityException) {
// this is handled by an other main loop (with a delay)

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -90,10 +90,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
setContentView(R.layout.lock_activity)
if (savedInstanceState == null) {
stopMediaPlayback()
}
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it }
currentInstances.add(this)
@ -198,11 +194,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
}
}
private fun stopMediaPlayback() {
val platformIntegration = DefaultAppLogic.with(this).platformIntegration
platformIntegration.muteAudioIfPossible(blockedPackageName)
}
override fun onBackPressed() {
// do nothing because going back would open the blocked app again
// super.onBackPressed()

View file

@ -173,6 +173,8 @@
<string name="background_logic_paused_text">Keine Einschränkungen</string>
<string name="background_logic_toast_sync_apps">TimeLimit: Fehler beim Aktualisieren der App-Liste</string>
<string name="background_logic_toast_block_audio">TimeLimit: Musikwiedergabe beendet</string>
<string name="background_logic_toast_block_audio_failed">TimeLimit: Anhalten der Musikwiedergabe fehlgeschlagen</string>
<string name="blocked_time_areas">
Sperrzeiten

View file

@ -216,6 +216,8 @@
<string name="background_logic_paused_text">No limitations apply</string>
<string name="background_logic_toast_sync_apps">TimeLimit: Failure while trying to update the app list</string>
<string name="background_logic_toast_block_audio">TimeLimit: Music playback stopped</string>
<string name="background_logic_toast_block_audio_failed">TimeLimit: Failed to stop music playback</string>
<string name="blocked_time_areas">
Blocked time areas