mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Improve music playback stopping
This commit is contained in:
parent
9a8ccc193f
commit
bc12c5e2b4
8 changed files with 126 additions and 35 deletions
|
@ -45,7 +45,8 @@ abstract class PlatformIntegration(
|
||||||
|
|
||||||
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
|
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
|
||||||
abstract fun showAnnoyScreen(annoyDuration: Long)
|
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)
|
abstract fun setShowBlockingOverlay(show: Boolean, blockedElement: String? = null)
|
||||||
// this should throw an SecurityException if the permission is missing
|
// this should throw an SecurityException if the permission is missing
|
||||||
abstract suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp>
|
abstract suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp>
|
||||||
|
|
|
@ -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
|
* 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
|
* 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.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.session.MediaController
|
||||||
import android.media.session.MediaSessionManager
|
import android.media.session.MediaSessionManager
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -52,6 +54,8 @@ import io.timelimit.android.ui.manipulation.AnnoyActivity
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,6 +88,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
private val overlay = OverlayUtil(context as Application)
|
private val overlay = OverlayUtil(context as Application)
|
||||||
private val battery = BatteryStatusUtil(context)
|
private val battery = BatteryStatusUtil(context)
|
||||||
private val connectedNetwork = ConnectedNetworkUtil(context)
|
private val connectedNetwork = ConnectedNetworkUtil(context)
|
||||||
|
private val muteAudioMutex = Mutex()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
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 manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
||||||
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
|
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
|
||||||
|
|
||||||
return sessions.find {
|
return sessions.find { isPlaying(it) }?.packageName
|
||||||
it.playbackState?.state == PlaybackState.STATE_PLAYING ||
|
|
||||||
it.playbackState?.state == PlaybackState.STATE_FAST_FORWARDING ||
|
|
||||||
it.playbackState?.state == PlaybackState.STATE_REWINDING
|
|
||||||
}?.packageName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,24 +271,89 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
AnnoyActivity.start(context, annoyDuration)
|
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {
|
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {
|
||||||
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
muteAudioMutex.withLock {
|
||||||
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
|
if (BuildConfig.DEBUG) {
|
||||||
val sessionsOfTheApp = sessions.filter { it.packageName == packageName }
|
Log.d(LOG_TAG, "muteAudioIfPossible($packageName)")
|
||||||
sessionsOfTheApp.forEach { session ->
|
}
|
||||||
session.dispatchMediaButtonEvent(KeyEvent(
|
|
||||||
KeyEvent.ACTION_DOWN,
|
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP
|
|
||||||
))
|
fun getAppSessions(): List<MediaController> {
|
||||||
session.dispatchMediaButtonEvent(KeyEvent(
|
return manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
|
||||||
KeyEvent.ACTION_UP,
|
.filter { it.packageName == packageName }
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP
|
}
|
||||||
))
|
|
||||||
|
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?) {
|
override fun setShowBlockingOverlay(show: Boolean, blockedElement: String?) {
|
||||||
|
@ -535,4 +601,10 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentNetworkId(): NetworkId = connectedNetwork.getNetworkId()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,6 @@ class NotificationListener: NotificationListenerService() {
|
||||||
lastOngoingNotificationHidden.remove(sbn.packageName)
|
lastOngoingNotificationHidden.remove(sbn.packageName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
appLogic.platformIntegration.muteAudioIfPossible(sbn.packageName)
|
|
||||||
|
|
||||||
val success = try {
|
val success = try {
|
||||||
if (sbn.isOngoing && SUPPORTS_HIDING_ONGOING_NOTIFICATIONS) {
|
if (sbn.isOngoing && SUPPORTS_HIDING_ONGOING_NOTIFICATIONS) {
|
||||||
// only snooze for 5 seconds to show it again soon
|
// only snooze for 5 seconds to show it again soon
|
||||||
|
|
|
@ -98,8 +98,8 @@ class DummyIntegration(
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun muteAudioIfPossible(packageName: String) {
|
override suspend fun muteAudioIfPossible(packageName: String): Boolean {
|
||||||
// ignore
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setShowBlockingOverlay(show: Boolean, blockedElement: String?) {
|
override fun setShowBlockingOverlay(show: Boolean, blockedElement: String?) {
|
||||||
|
|
|
@ -119,6 +119,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
private val usedTimeUpdateHelper = UsedTimeUpdateHelper(appLogic)
|
private val usedTimeUpdateHelper = UsedTimeUpdateHelper(appLogic)
|
||||||
private var previousMainLogicExecutionTime = 0
|
private var previousMainLogicExecutionTime = 0
|
||||||
private var previousMainLoopEndTime = 0L
|
private var previousMainLoopEndTime = 0L
|
||||||
|
private var previousAudioPlaybackBlock: Pair<Long, String>? = null
|
||||||
private val dayChangeTracker = DayChangeTracker(
|
private val dayChangeTracker = DayChangeTracker(
|
||||||
timeApi = appLogic.timeApi,
|
timeApi = appLogic.timeApi,
|
||||||
longDuration = 1000 * 60 * 10 /* 10 minutes */
|
longDuration = 1000 * 60 * 10 /* 10 minutes */
|
||||||
|
@ -703,7 +704,31 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockAudioPlayback && audioPlaybackPackageName != null) {
|
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) {
|
} catch (ex: SecurityException) {
|
||||||
// this is handled by an other main loop (with a delay)
|
// this is handled by an other main loop (with a delay)
|
||||||
|
|
|
@ -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
|
* 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
|
* 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)
|
setContentView(R.layout.lock_activity)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
stopMediaPlayback()
|
|
||||||
}
|
|
||||||
|
|
||||||
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it }
|
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it }
|
||||||
|
|
||||||
currentInstances.add(this)
|
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() {
|
override fun onBackPressed() {
|
||||||
// do nothing because going back would open the blocked app again
|
// do nothing because going back would open the blocked app again
|
||||||
// super.onBackPressed()
|
// super.onBackPressed()
|
||||||
|
|
|
@ -173,6 +173,8 @@
|
||||||
<string name="background_logic_paused_text">Keine Einschränkungen</string>
|
<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_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">
|
<string name="blocked_time_areas">
|
||||||
Sperrzeiten
|
Sperrzeiten
|
||||||
|
|
|
@ -216,6 +216,8 @@
|
||||||
<string name="background_logic_paused_text">No limitations apply</string>
|
<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_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">
|
<string name="blocked_time_areas">
|
||||||
Blocked time areas
|
Blocked time areas
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue