Skip to content

Commit

Permalink
fix(app): fix removal of delayed callbacks from handlers
Browse files Browse the repository at this point in the history
calling Handler#removeCallbacks doesn't seem to work with Kotlin's
function references. An additional token can be used to ensure
that callbacks are removed when using function references.
  • Loading branch information
ashutoshgngwr committed Jul 10, 2020
1 parent 709bb78 commit 784ef10
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.ashutoshgngwr.noice.sound.player

import android.os.Handler
import android.util.Log
import androidx.core.os.HandlerCompat
import com.github.ashutoshgngwr.noice.sound.Sound
import com.github.ashutoshgngwr.noice.sound.player.strategy.PlaybackStrategy
import com.github.ashutoshgngwr.noice.sound.player.strategy.PlaybackStrategyFactory
Expand All @@ -15,6 +16,7 @@ class Player(private val sound: Sound, playbackStrategyFactory: PlaybackStrategy

companion object {
private val TAG = Player::class.simpleName
private val DELAYED_PLAYBACK_CALLBACK_TOKEN = "${Player::class.simpleName}.playback_callback"

const val DEFAULT_VOLUME = 4
const val MAX_VOLUME = 20
Expand Down Expand Up @@ -64,13 +66,16 @@ class Player(private val sound: Sound, playbackStrategyFactory: PlaybackStrategy
*/
private fun playAndRegisterDelayedCallback() {
if (!isPlaying) {
Log.d(TAG, "delayed callback invoked but not playing! won't perform playback.")
return
}

playbackStrategy.play()
val delay = nextInt(MIN_TIME_PERIOD, 1 + timePeriod) * 1000L
handler.postDelayed(this::playAndRegisterDelayedCallback, delay)
Log.d(TAG, "scheduling delayed playback with ${delay}ms delay")
HandlerCompat.postDelayed(
handler, this::playAndRegisterDelayedCallback, DELAYED_PLAYBACK_CALLBACK_TOKEN, delay
)
}

/**
Expand All @@ -80,8 +85,8 @@ class Player(private val sound: Sound, playbackStrategyFactory: PlaybackStrategy
fun pause() {
isPlaying = false
playbackStrategy.pause()
if (sound.isLoopable) {
handler.removeCallbacks(this::playAndRegisterDelayedCallback)
if (!sound.isLoopable) {
handler.removeCallbacksAndMessages(DELAYED_PLAYBACK_CALLBACK_TOKEN)
}
}

Expand All @@ -92,8 +97,8 @@ class Player(private val sound: Sound, playbackStrategyFactory: PlaybackStrategy
fun stop() {
isPlaying = false
playbackStrategy.stop()
if (sound.isLoopable) {
handler.removeCallbacks(this::playAndRegisterDelayedCallback)
if (!sound.isLoopable) {
handler.removeCallbacksAndMessages(DELAYED_PLAYBACK_CALLBACK_TOKEN)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.core.os.HandlerCompat
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
Expand All @@ -33,6 +34,7 @@ class PlayerManager(private val context: Context) :

companion object {
private val TAG = PlayerManager::javaClass.name
private val DELAYED_STOP_CALLBACK_TOKEN = "${PlayerManager::javaClass.name}.stop_callback"
}

var state = State.STOPPED
Expand Down Expand Up @@ -126,7 +128,7 @@ class PlayerManager(private val context: Context) :
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
Log.d(TAG, "Permanently lost audio focus! Stop playback...")
Log.d(TAG, "Permanently lost audio focus! pause and wait to stop playback...")
hasAudioFocus = false
resumeOnFocusGain = true
playbackDelayed = false
Expand Down Expand Up @@ -243,17 +245,21 @@ class PlayerManager(private val context: Context) :
notifyChanges()
}

fun pauseAndWaitBeforeStop() {
private fun pauseAndWaitBeforeStop() {
pause()
handler.postDelayed(this::stop, TimeUnit.MINUTES.toMillis(1))
Log.d(TAG, "scheduling pauseAndWaitBeforeStop callback")
HandlerCompat.postDelayed(
handler, this::stop, DELAYED_STOP_CALLBACK_TOKEN, TimeUnit.MINUTES.toMillis(1)
)
}

/**
* Resumes all [Player]s from the saved state. It requests AudioFocus if not already present.
*/
fun resume() {
if (hasAudioFocus) {
handler.removeCallbacks(this::stop) // remove pauseAndWaitBeforeStop callbacks, if any
Log.d(TAG, "removing pauseAndWaitBeforeStop callback")
handler.removeCallbacksAndMessages(DELAYED_STOP_CALLBACK_TOKEN)
state = State.PLAYING
players.values.forEach { it.play() }
} else if (!playbackDelayed) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,28 @@ class CountdownTextView : MaterialTextView {
*/
private var countdownUntilMillis = SystemClock.uptimeMillis()

/**
* Updates the view content and registers a delayed callback to itself for indefinitely refreshing
* the view content. Use [android.view.View.removeCallbacks] to remove the callback. If won't
* register itself as a delayed callback if countdown timer has finished or if the view is not
* attached to a window anymore.
*/
private val updateCallback = object : Runnable {
override fun run() {
updateCountdown()
if (SystemClock.uptimeMillis() < countdownUntilMillis) {
postDelayed(this, UPDATE_INTERVAL)
}
}
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
updateCountdownWithCallbacks()
post(updateCallback)
}

override fun onDetachedFromWindow() {
removeCallbacks(this::updateCountdownWithCallbacks)
removeCallbacks(updateCallback)
super.onDetachedFromWindow()
}

Expand All @@ -58,22 +73,9 @@ class CountdownTextView : MaterialTextView {
*/
fun startCountdown(millis: Long) {
// remove any pre-registered callbacks. startCountdown() may be called multiple times.
removeCallbacks(this::updateCountdownWithCallbacks)
removeCallbacks(updateCallback)
countdownUntilMillis = SystemClock.uptimeMillis() + millis
updateCountdownWithCallbacks()
}

/**
* Updates the view content and registers a delayed callback to itself for indefinitely refreshing
* the view content. Use [android.view.View.removeCallbacks] to remove the callback. If won't
* register itself as a delayed callback if countdown timer has finished or if the view is not
* attached to a window anymore.
*/
private fun updateCountdownWithCallbacks() {
updateCountdown()
if (SystemClock.uptimeMillis() < countdownUntilMillis) {
postDelayed(this::updateCountdownWithCallbacks, UPDATE_INTERVAL)
}
post(updateCallback)
}

/**
Expand Down

0 comments on commit 784ef10

Please sign in to comment.