Add Android alert effect playback
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.alerts
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class AndroidAlertEffectPlayerTest {
|
||||||
|
@Test
|
||||||
|
fun vibrationCompletesWithoutLeakingPlayback() {
|
||||||
|
val completion = CountDownLatch(1)
|
||||||
|
val player = AndroidAlertEffectPlayer(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
|
||||||
|
player.play(
|
||||||
|
QueuedAlert("test", AlertSnapshot(null, listOf(0, 10), false, false)),
|
||||||
|
completion::countDown,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(completion.await(2, TimeUnit.SECONDS))
|
||||||
|
player.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<!-- Required for the all-installed-apps filter; this app is not distributed through Google Play. -->
|
<!-- Required for the all-installed-apps filter; this app is not distributed through Google Play. -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.alerts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.VibrationAttributes
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.VibratorManager
|
||||||
|
|
||||||
|
/** Plays resolved notification-class effects and releases all platform resources on stop. */
|
||||||
|
class AndroidAlertEffectPlayer(context: Context) : AlertEffectPlayer {
|
||||||
|
private val context = context.applicationContext
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private val vibrator = context.getSystemService(VibratorManager::class.java).defaultVibrator
|
||||||
|
private var generation = 0L
|
||||||
|
private var remainingEffects = 0
|
||||||
|
private var player: MediaPlayer? = null
|
||||||
|
private var completion: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun play(alert: QueuedAlert, onFinished: () -> Unit) = synchronized(this) {
|
||||||
|
stopLocked()
|
||||||
|
val token = generation
|
||||||
|
completion = onFinished
|
||||||
|
val snapshot = alert.snapshot
|
||||||
|
val vibrationDuration = snapshot.vibrationPattern.sum()
|
||||||
|
remainingEffects = (if (snapshot.soundUri == null) 0 else 1) + (if (vibrationDuration > 0) 1 else 0)
|
||||||
|
snapshot.soundUri?.let { startSound(Uri.parse(it), token) }
|
||||||
|
if (vibrationDuration > 0) {
|
||||||
|
vibrator.vibrate(
|
||||||
|
VibrationEffect.createWaveform(snapshot.vibrationPattern.toLongArray(), -1),
|
||||||
|
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_NOTIFICATION).build(),
|
||||||
|
)
|
||||||
|
handler.postDelayed({ finishEffect(token) }, vibrationDuration)
|
||||||
|
}
|
||||||
|
if (remainingEffects == 0) finishEffect(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() = synchronized(this) { stopLocked() }
|
||||||
|
|
||||||
|
private fun startSound(uri: Uri, token: Long) {
|
||||||
|
val mediaPlayer = MediaPlayer()
|
||||||
|
try {
|
||||||
|
player = mediaPlayer
|
||||||
|
mediaPlayer.setAudioAttributes(
|
||||||
|
AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build(),
|
||||||
|
)
|
||||||
|
mediaPlayer.setDataSource(context, uri)
|
||||||
|
mediaPlayer.setOnCompletionListener { finishSound(token, mediaPlayer) }
|
||||||
|
mediaPlayer.setOnErrorListener { _, _, _ ->
|
||||||
|
finishSound(token, mediaPlayer)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
mediaPlayer.prepareAsync()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
if (player === mediaPlayer) player = null
|
||||||
|
mediaPlayer.release()
|
||||||
|
finishEffect(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishSound(token: Long, mediaPlayer: MediaPlayer): Unit = synchronized(this) {
|
||||||
|
if (player !== mediaPlayer) return
|
||||||
|
player = null
|
||||||
|
mediaPlayer.release()
|
||||||
|
finishEffect(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishEffect(token: Long): Unit = synchronized(this) {
|
||||||
|
if (token != generation || remainingEffects == 0) return
|
||||||
|
if (--remainingEffects != 0) return
|
||||||
|
completion.also { completion = null }?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopLocked() {
|
||||||
|
generation++
|
||||||
|
remainingEffects = 0
|
||||||
|
completion = null
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
player?.release()
|
||||||
|
player = null
|
||||||
|
vibrator.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user