diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 368e5f6..f8765b2 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -85,7 +85,6 @@ dependencies {
implementation(libs.accompanist.systemuicontroller)
- implementation(libs.android.media)
implementation(libs.exoplayer)
implementation(libs.room)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2e31c0c..e75d75e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
@@ -79,21 +80,10 @@
android:name=".service.PlayerService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt
index 5f32952..5038f5c 100644
--- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt
+++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt
@@ -1,12 +1,17 @@
package it.vfsfitvnm.vimusic
+import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.IBinder
+import android.os.PowerManager
+import android.provider.Settings
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi
@@ -22,11 +27,12 @@ import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.unit.dp
+import androidx.core.content.getSystemService
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.valentinilk.shimmer.LocalShimmerTheme
import com.valentinilk.shimmer.defaultShimmerTheme
@@ -35,6 +41,7 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
import it.vfsfitvnm.vimusic.ui.components.rememberMenuState
+import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.screens.HomeScreen
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
import it.vfsfitvnm.vimusic.ui.styling.*
@@ -71,6 +78,7 @@ class MainActivity : ComponentActivity() {
super.onStop()
}
+ @SuppressLint("BatteryLife")
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -138,6 +146,43 @@ class MainActivity : ComponentActivity() {
.fillMaxSize()
.background(colorPalette.background)
) {
+ var isIgnoringBatteryOptimizations by rememberSaveable {
+ mutableStateOf(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ getSystemService()?.isIgnoringBatteryOptimizations(packageName) ?: true
+ } else {
+ true
+ })
+ }
+
+ if (!isIgnoringBatteryOptimizations) {
+ ConfirmationDialog(
+ text = "(Temporary) ViMusic needs to ignore battery optimizations to avoid being killed when the playback is paused.",
+ confirmText = "Grant",
+ onDismiss = {
+ isIgnoringBatteryOptimizations = true
+ },
+ onConfirm = {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return@ConfirmationDialog
+
+ val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = Uri.parse("package:$packageName")
+ }
+
+ if (intent.resolveActivity(packageManager) != null) {
+ startActivity(intent)
+ } else {
+ val fallbackIntent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
+
+ if (fallbackIntent.resolveActivity(packageManager) != null) {
+ startActivity(fallbackIntent)
+ } else {
+ Toast.makeText(this@MainActivity, "Couldn't find battery optimization settings, please whitelist ViMusic manually", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ )
+ }
+
when (val uri = uri) {
null -> {
HomeScreen()
diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt
index db98b2e..f6910de 100644
--- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt
+++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt
@@ -1,32 +1,21 @@
package it.vfsfitvnm.vimusic.service
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.Service
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
+import android.app.*
+import android.content.*
import android.content.res.Configuration
import android.graphics.Color
+import android.media.MediaMetadata
+import android.media.session.MediaSession
+import android.media.session.PlaybackState
import android.net.Uri
import android.os.Build
-import android.os.Handler
-import android.os.Looper
-import android.support.v4.media.MediaMetadataCompat
-import android.support.v4.media.session.MediaControllerCompat
-import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
-import android.support.v4.media.session.PlaybackStateCompat.*
-import androidx.annotation.DrawableRes
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.NotificationCompat
-import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.startForegroundService
+import androidx.core.content.getSystemService
import androidx.core.net.toUri
-import androidx.media.session.MediaButtonReceiver
import androidx.media3.common.*
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
@@ -60,28 +49,29 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.StateFlow
import kotlin.math.roundToInt
import kotlin.system.exitProcess
+import android.os.Binder as AndroidBinder
+@Suppress("DEPRECATION")
class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback,
SharedPreferences.OnSharedPreferenceChangeListener {
- private lateinit var mediaSession: MediaSessionCompat
+ private lateinit var mediaSession: MediaSession
private lateinit var cache: SimpleCache
private lateinit var player: ExoPlayer
- private val stateBuilder = Builder()
- .setState(STATE_NONE, 0, 1f)
+ private val stateBuilder = PlaybackState.Builder()
.setActions(
- ACTION_PLAY
- or ACTION_PAUSE
- or ACTION_SKIP_TO_PREVIOUS
- or ACTION_SKIP_TO_NEXT
- or ACTION_PLAY_PAUSE
- or ACTION_SEEK_TO
+ PlaybackState.ACTION_PLAY
+ or PlaybackState.ACTION_PAUSE
+ or PlaybackState.ACTION_SKIP_TO_PREVIOUS
+ or PlaybackState.ACTION_SKIP_TO_NEXT
+ or PlaybackState.ACTION_PLAY_PAUSE
+ or PlaybackState.ACTION_SEEK_TO
)
- private val metadataBuilder = MediaMetadataCompat.Builder()
+ private val metadataBuilder = MediaMetadata.Builder()
- private lateinit var notificationManager: NotificationManager
+ private var notificationManager: NotificationManager? = null
private var timerJob: TimerJob? = null
@@ -93,74 +83,19 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
private val songPendingLoudnessDb = mutableMapOf()
- private var hack: Hack? = null
-
- private var isTaskRemoved = false
-
private var isVolumeNormalizationEnabled = false
private var isPersistentQueueEnabled = false
- private val handler = Handler(Looper.getMainLooper())
-
- private val mediaControllerCallback = object : MediaControllerCompat.Callback() {
- override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
- when (state?.state) {
- STATE_PLAYING -> {
- ContextCompat.startForegroundService(
- this@PlayerService,
- intent()
- )
- startForeground(NotificationId, notification())
-
- hack?.stop()
- }
- STATE_PAUSED -> {
- if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) {
- if (isPersistentQueueEnabled) {
- if (isTaskRemoved) {
- stopForeground(false)
- }
- } else {
- stopForeground(false)
- }
-
- notificationManager.notify(NotificationId, notification())
-
- hack?.start()
- }
- }
- STATE_NONE -> {
- onPlaybackStateChanged(Player.STATE_READY)
- onIsPlayingChanged(player.playWhenReady)
- }
- STATE_ERROR -> {
- notificationManager.notify(NotificationId, notification())
- }
- else -> {}
- }
- }
- }
-
private val binder = Binder()
- override fun onBind(intent: Intent?): Binder {
- hack?.stop()
- hack = null
+ private var isNotificationStarted = false
+
+ private lateinit var notificationActionReceiver: NotificationActionReceiver
+
+ override fun onBind(intent: Intent?): AndroidBinder {
return binder
}
- override fun onRebind(intent: Intent?) {
- isTaskRemoved = false
- hack?.stop()
- hack = null
- super.onRebind(intent)
- }
-
- override fun onUnbind(intent: Intent?): Boolean {
- hack = Hack()
- return true
- }
-
override fun onCreate() {
super.onCreate()
@@ -207,22 +142,30 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
maybeRestorePlayerQueue()
- mediaSession = MediaSessionCompat(baseContext, "PlayerService")
+ mediaSession = MediaSession(baseContext, "PlayerService")
mediaSession.setCallback(SessionCallback(player))
mediaSession.setPlaybackState(stateBuilder.build())
mediaSession.isActive = true
- mediaSession.controller.registerCallback(mediaControllerCallback)
- }
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- MediaButtonReceiver.handleIntent(mediaSession, intent)
- return START_STICKY
+ notificationActionReceiver = NotificationActionReceiver(player)
+
+ val filter = IntentFilter().apply {
+ addAction(Action.play.value)
+ addAction(Action.pause.value)
+ addAction(Action.next.value)
+ addAction(Action.previous.value)
+ }
+
+ registerReceiver(notificationActionReceiver, filter)
}
override fun onTaskRemoved(rootIntent: Intent?) {
- isTaskRemoved = true
if (!player.playWhenReady) {
- stopSelf()
+ if (isPersistentQueueEnabled) {
+ broadCastPendingIntent().send()
+ } else {
+ stopSelf()
+ }
}
super.onTaskRemoved(rootIntent)
}
@@ -235,14 +178,12 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
Context.MODE_PRIVATE
).unregisterOnSharedPreferenceChangeListener(this)
- hack?.stop()
- hack = null
-
player.removeListener(this)
player.stop()
player.release()
- mediaSession.controller.unregisterCallback(mediaControllerCallback)
+ unregisterReceiver(notificationActionReceiver)
+
mediaSession.isActive = false
mediaSession.release()
cache.release()
@@ -252,7 +193,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
override fun onConfigurationChanged(newConfig: Configuration) {
if (bitmapProvider.setDefaultBitmap() && player.currentMediaItem != null) {
- notificationManager.notify(NotificationId, notification())
+ notificationManager?.notify(NotificationId, notification())
}
super.onConfigurationChanged(newConfig)
}
@@ -304,29 +245,29 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
private fun maybeRestorePlayerQueue() {
if (!isPersistentQueueEnabled) return
- coroutineScope.launch(Dispatchers.IO) {
+ query {
val queuedSong = Database.queue()
Database.clearQueue()
- if (queuedSong.isEmpty()) return@launch
+ if (queuedSong.isEmpty()) return@query
val index = queuedSong.indexOfFirst { it.position != null }.coerceAtLeast(0)
- withContext(Dispatchers.Main) {
+ runBlocking(Dispatchers.Main) {
player.setMediaItems(
- queuedSong
- .map { mediaItem ->
- mediaItem.mediaItem.buildUpon()
- .setUri(mediaItem.mediaItem.mediaId)
- .setCustomCacheKey(mediaItem.mediaItem.mediaId)
- .build()
- },
+ queuedSong.map { mediaItem ->
+ mediaItem.mediaItem.buildUpon()
+ .setUri(mediaItem.mediaItem.mediaId)
+ .setCustomCacheKey(mediaItem.mediaItem.mediaId)
+ .build()
+ },
true
)
player.seekTo(index, queuedSong[index].position ?: 0)
player.prepare()
- ContextCompat.startForegroundService(this@PlayerService, intent())
+ isNotificationStarted = true
+ startForegroundService(this@PlayerService, intent())
startForeground(NotificationId, notification())
}
}
@@ -344,53 +285,64 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
- override fun onPlaybackStateChanged(@Player.State playbackState: Int) {
- if (playbackState == Player.STATE_READY) {
- if (player.duration != C.TIME_UNSET) {
- metadataBuilder
- .putText(
- MediaMetadataCompat.METADATA_KEY_TITLE,
- player.currentMediaItem?.mediaMetadata?.title
- )
- .putText(
- MediaMetadataCompat.METADATA_KEY_ARTIST,
- player.currentMediaItem?.mediaMetadata?.artist
- )
- .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player.duration)
- mediaSession.setMetadata(metadataBuilder.build())
+ private val Player.androidPlaybackState: Int
+ get() = when (playbackState) {
+ Player.STATE_BUFFERING -> if (playWhenReady) PlaybackState.STATE_BUFFERING else PlaybackState.STATE_PAUSED
+ Player.STATE_READY -> if (playWhenReady) PlaybackState.STATE_PLAYING else PlaybackState.STATE_PAUSED
+ Player.STATE_ENDED -> PlaybackState.STATE_STOPPED
+ Player.STATE_IDLE -> PlaybackState.STATE_NONE
+ else -> PlaybackState.STATE_NONE
+ }
+
+ override fun onEvents(player: Player, events: Player.Events) {
+ if (player.duration != C.TIME_UNSET) {
+ metadataBuilder
+ .putText(
+ MediaMetadata.METADATA_KEY_TITLE,
+ player.currentMediaItem?.mediaMetadata?.title
+ )
+ .putText(
+ MediaMetadata.METADATA_KEY_ARTIST,
+ player.currentMediaItem?.mediaMetadata?.artist
+ )
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
+ .build().let(mediaSession::setMetadata)
+ }
+
+ stateBuilder
+ .setState(player.androidPlaybackState, player.currentPosition, 1f)
+ .setBufferedPosition(player.bufferedPosition)
+
+ mediaSession.setPlaybackState(stateBuilder.build())
+
+ if (events.containsAny(
+ Player.EVENT_PLAYBACK_STATE_CHANGED,
+ Player.EVENT_PLAY_WHEN_READY_CHANGED,
+ Player.EVENT_IS_PLAYING_CHANGED,
+ Player.EVENT_POSITION_DISCONTINUITY
+ )
+ ) {
+ val notification = notification()
+
+ if (notification == null) {
+ stopSelf()
+ return
+ }
+
+ if (player.shouldBePlaying && !isNotificationStarted) {
+ isNotificationStarted = true
+ startForegroundService(this@PlayerService, intent())
+ startForeground(NotificationId, notification())
+ } else {
+ if (!player.shouldBePlaying) {
+ isNotificationStarted = false
+ stopForeground(false)
+ }
+ notificationManager?.notify(NotificationId, notification)
}
}
}
- override fun onPlayerErrorChanged(error: PlaybackException?) {
- if (error != null) {
- stateBuilder
- .setState(STATE_ERROR, player.currentPosition, 1f)
-
- mediaSession.setPlaybackState(stateBuilder.build())
- }
- }
-
- override fun onPositionDiscontinuity(
- oldPosition: Player.PositionInfo,
- newPosition: Player.PositionInfo,
- @Player.DiscontinuityReason reason: Int
- ) {
- stateBuilder
- .setState(STATE_NONE, newPosition.positionMs, 1f)
- .setBufferedPosition(player.bufferedPosition)
-
- mediaSession.setPlaybackState(stateBuilder.build())
- }
-
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- stateBuilder
- .setState(if (isPlaying) STATE_PLAYING else STATE_PAUSED, player.currentPosition, 1f)
- .setBufferedPosition(player.bufferedPosition)
-
- mediaSession.setPlaybackState(stateBuilder.build())
- }
-
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
Preferences.Keys.persistentQueue -> isPersistentQueueEnabled =
@@ -400,24 +352,21 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
- private fun notification(): Notification {
- fun NotificationCompat.Builder.addMediaAction(
- @DrawableRes resId: Int,
- description: String,
- @MediaKeyAction command: Long
- ): NotificationCompat.Builder {
- return addAction(
- NotificationCompat.Action(
- resId,
- description,
- MediaButtonReceiver.buildMediaButtonPendingIntent(this@PlayerService, command)
- )
- )
- }
+ private fun notification(): Notification? {
+ if (player.currentMediaItem == null) return null
+
+ val playIntent = Action.play.pendingIntent
+ val pauseIntent = Action.pause.pendingIntent
+ val nextIntent = Action.next.pendingIntent
+ val prevIntent = Action.previous.pendingIntent
val mediaMetadata = player.mediaMetadata
- val builder = NotificationCompat.Builder(applicationContext, NotificationChannelId)
+ val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification.Builder(applicationContext, NotificationChannelId)
+ } else {
+ Notification.Builder(applicationContext)
+ }
.setContentTitle(mediaMetadata.title)
.setContentText(mediaMetadata.artist)
.setSubText(player.playerError?.message)
@@ -429,45 +378,46 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
?: R.drawable.app_icon)
.setOngoing(false)
.setContentIntent(activityPendingIntent())
- .setDeleteIntent(broadCastPendingIntent())
- .setChannelId(NotificationChannelId)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setSilent(true)
+ .setDeleteIntent(broadCastPendingIntent())
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setStyle(
- androidx.media.app.NotificationCompat.MediaStyle()
+ Notification.MediaStyle()
.setShowActionsInCompactView(0, 1, 2)
.setMediaSession(mediaSession.sessionToken)
)
- .addMediaAction(R.drawable.play_skip_back, "Skip back", ACTION_SKIP_TO_PREVIOUS)
- .addMediaAction(
- if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) R.drawable.play else R.drawable.pause,
- if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) "Play" else "Pause",
- if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) ACTION_PLAY else ACTION_PAUSE
+ .addAction(R.drawable.play_skip_back, "Skip back", prevIntent)
+ .addAction(
+ if (player.shouldBePlaying) R.drawable.pause else R.drawable.play,
+ if (player.shouldBePlaying) "Pause" else "Play",
+ if (player.shouldBePlaying) pauseIntent else playIntent
)
- .addMediaAction(R.drawable.play_skip_forward, "Skip forward", ACTION_SKIP_TO_NEXT)
+ .addAction(R.drawable.play_skip_forward, "Skip forward", nextIntent)
bitmapProvider.load(mediaMetadata.artworkUri) { bitmap ->
- notificationManager.notify(NotificationId, builder.setLargeIcon(bitmap).build())
+ notificationManager?.notify(NotificationId, builder.setLargeIcon(bitmap).build())
}
return builder.build()
}
private fun createNotificationChannel() {
- notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager = getSystemService()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
- with(notificationManager) {
+ notificationManager?.run {
if (getNotificationChannel(NotificationChannelId) == null) {
createNotificationChannel(
NotificationChannel(
NotificationChannelId,
"Now playing",
- NotificationManager.IMPORTANCE_DEFAULT
- )
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ setSound(null, null)
+ enableLights(false)
+ enableVibration(false)
+ }
)
}
@@ -476,8 +426,12 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
NotificationChannel(
SleepTimerNotificationChannelId,
"Sleep timer",
- NotificationManager.IMPORTANCE_DEFAULT
- )
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ setSound(null, null)
+ enableLights(false)
+ enableVibration(false)
+ }
)
}
}
@@ -602,7 +556,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
- inner class Binder : android.os.Binder() {
+ inner class Binder : AndroidBinder() {
val player: ExoPlayer
get() = this@PlayerService.player
@@ -631,7 +585,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
.setSmallIcon(R.drawable.app_icon)
.build()
- notificationManager.notify(SleepTimerNotificationId, notification)
+ notificationManager?.notify(SleepTimerNotificationId, notification)
exitProcess(0)
}
@@ -677,7 +631,7 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
}
}
- private class SessionCallback(private val player: Player) : MediaSessionCompat.Callback() {
+ private class SessionCallback(private val player: Player) : MediaSession.Callback() {
override fun onPlay() = player.play()
override fun onPause() = player.pause()
override fun onSkipToPrevious() = player.seekToPrevious()
@@ -685,48 +639,47 @@ class PlayerService : Service(), Player.Listener, PlaybackStatsListener.Callback
override fun onSeekTo(pos: Long) = player.seekTo(pos)
}
- class StopServiceBroadcastReceiver : BroadcastReceiver() {
+ private class NotificationActionReceiver(private val player: Player) : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Action.pause.value -> player.pause()
+ Action.play.value -> player.play()
+ Action.next.value -> player.seekToNext()
+ Action.previous.value -> player.seekToPrevious()
+ }
+ }
+ }
+
+ class NotificationDismissReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.stopService(context.intent())
}
}
- // https://stackoverflow.com/q/53502244/16885569
- private inner class Hack : Runnable {
- private var isStarted = false
- private val intervalMs = 30_000L
+ @JvmInline
+ private value class Action(val value: String) {
+ context(Context)
+ val pendingIntent: PendingIntent
+ get() = PendingIntent.getBroadcast(
+ this@Context,
+ 100,
+ Intent(value).setPackage(packageName),
+ PendingIntent.FLAG_UPDATE_CURRENT.or(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
+ )
- @Synchronized
- fun start() {
- if (!isStarted) {
- isStarted = true
- handler.postDelayed(this, intervalMs)
- }
- }
-
- @Synchronized
- fun stop() {
- if (isStarted) {
- handler.removeCallbacks(this)
- isStarted = false
- }
- }
-
- override fun run() {
- if (player.playbackState == Player.STATE_ENDED || !player.playWhenReady) {
- startForeground(NotificationId, notification())
- stopForeground(false)
- handler.postDelayed(this, intervalMs)
- }
+ companion object {
+ val pause = Action("it.vfsfitvnm.vimusic.pause")
+ val play = Action("it.vfsfitvnm.vimusic.play")
+ val next = Action("it.vfsfitvnm.vimusic.next")
+ val previous = Action("it.vfsfitvnm.vimusic.previous")
}
}
- companion object {
- private const val NotificationId = 1001
- private const val NotificationChannelId = "default_channel_id"
+ private companion object {
+ const val NotificationId = 1001
+ const val NotificationChannelId = "default_channel_id"
- private const val SleepTimerNotificationId = 1002
- private const val SleepTimerNotificationChannelId = "sleep_timer_channel_id"
+ const val SleepTimerNotificationId = 1002
+ const val SleepTimerNotificationChannelId = "sleep_timer_channel_id"
}
}
-
diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Player.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Player.kt
index 32263d9..af076cc 100644
--- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Player.kt
+++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Player.kt
@@ -10,6 +10,8 @@ val Timeline.mediaItems: List
getWindow(index, Timeline.Window()).mediaItem
}
+val Player.shouldBePlaying: Boolean
+ get() = !(playbackState == Player.STATE_ENDED || !playWhenReady)
fun Player.forcePlay(mediaItem: MediaItem) {
setMediaItem(mediaItem, true)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 661ef5c..09a30d2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,8 +14,6 @@ dependencyResolutionManagement {
version("kotlin", "1.7.0")
plugin("kotlin-serialization","org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin")
- library("android-media", "androidx.media", "media").version("1.6.0")
-
version("compose-compiler", "1.2.0")
version("compose", "1.3.0-alpha01")