From d3358fa7b57ff0061882ac95bfda5379bb933d3c Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Thu, 6 Jan 2022 23:43:50 +0100
Subject: [PATCH] Add option to disable music widget filtering
---
i18n/src/main/res/values-de/strings.xml | 10 +-
i18n/src/main/res/values/strings.xml | 9 +
music/build.gradle.kts | 1 +
.../java/de/mm20/launcher2/music/Module.kt | 2 +-
.../mm20/launcher2/music/MusicRepository.kt | 248 +++++++++++-------
.../notifications/NotificationService.kt | 14 +-
.../de/mm20/launcher2/preferences/Defaults.kt | 5 +
preferences/src/main/proto/settings.proto | 5 +
.../ui/launcher/widgets/music/MusicWidget.kt | 241 ++++++++++-------
.../launcher/widgets/music/MusicWidgetVM.kt | 13 +-
.../launcher2/ui/legacy/widget/MusicWidget.kt | 2 +-
.../launcher2/ui/settings/SettingsActivity.kt | 4 +
.../musicwidget/MusicWidgetSettingsScreen.kt | 66 +++++
.../MusicWidgetSettingsScreenVM.kt | 46 ++++
.../settings/widgets/WidgetsSettingsScreen.kt | 8 +
.../de/mm20/launcher2/ui/widget/WidgetCard.kt | 1 +
16 files changed, 477 insertions(+), 198 deletions(-)
create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreen.kt
create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreenVM.kt
diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml
index 0df9825d..1769c4c4 100644
--- a/i18n/src/main/res/values-de/strings.xml
+++ b/i18n/src/main/res/values-de/strings.xml
@@ -419,7 +419,8 @@
Spaltenanzahl
Gewähren
- Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln
+ Standortzugriff wird benötigt um den Standort automatisch zu ermitteln
+ Benachrichtigungszugriff wird benötigt um Medienwiedergabe zu steuern
Standort festlegen
Debug
@@ -429,7 +430,14 @@
Widgets konfigurieren
Wetter
+ Musik
Crash-Reporter
Fehler- und Absturzberichte
+
+ Auf Musik-Apps begrenzen
+ Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind
+
+ %1$s spielt Medien
+ Bisher wurden keine Medien abgespielt
\ No newline at end of file
diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml
index 749ef5b3..4a28e84b 100644
--- a/i18n/src/main/res/values/strings.xml
+++ b/i18n/src/main/res/values/strings.xml
@@ -458,6 +458,7 @@
Grant
Location access is required to determine the location automatically
+ Notification access is required to control media playback
Set location
Debug
@@ -467,7 +468,15 @@
Configure widgets
Weather
+ Music
Crash reporter
Error and crash reports
+
+ Restrict to music apps
+ Ignore media sessions of apps that are not music apps
+
+ %1$s is playing media
+
+ No media has been played yet
diff --git a/music/build.gradle.kts b/music/build.gradle.kts
index 70c3da3f..e15a99bc 100644
--- a/music/build.gradle.kts
+++ b/music/build.gradle.kts
@@ -43,5 +43,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":ktx"))
+ implementation(project(":preferences"))
}
\ No newline at end of file
diff --git a/music/src/main/java/de/mm20/launcher2/music/Module.kt b/music/src/main/java/de/mm20/launcher2/music/Module.kt
index 5bf012ec..61e25a4e 100644
--- a/music/src/main/java/de/mm20/launcher2/music/Module.kt
+++ b/music/src/main/java/de/mm20/launcher2/music/Module.kt
@@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val musicModule = module {
- single { MusicRepository(androidContext()) }
+ single { MusicRepositoryImpl(androidContext()) }
}
\ No newline at end of file
diff --git a/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt b/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt
index 55006085..9e3a7261 100644
--- a/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt
+++ b/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt
@@ -7,7 +7,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.support.v4.media.session.MediaSessionCompat
-import android.util.Log
import android.view.KeyEvent
import androidx.core.content.edit
import androidx.core.graphics.scale
@@ -16,39 +15,96 @@ import androidx.media2.common.MediaMetadata
import androidx.media2.common.SessionPlayer
import androidx.media2.session.MediaController
import androidx.media2.session.SessionCommandGroup
+import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.sync.Semaphore
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
import java.io.File
import java.util.concurrent.Executors
-class MusicRepository(val context: Context) {
+interface MusicRepository {
+ val playbackState: Flow
+ val title: Flow
+ val artist: Flow
+ val album: Flow
+ val albumArt: Flow
+
+ fun setMediaSession(token: MediaSessionCompat.Token, packageName: String)
+
+ fun next()
+ fun previous()
+ fun pause()
+ fun play()
+ fun togglePause()
+ fun openPlayer(): PendingIntent?
+
+ fun openPlayerChooser(context: Context)
+
+ fun resetPlayer()
+}
+
+class MusicRepositoryImpl(val context: Context) : MusicRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
+ private val dataStore: LauncherDataStore by inject()
- val playbackState = MutableStateFlow(PlaybackState.Stopped)
- val title = MutableStateFlow(null)
- val artist = MutableStateFlow(null)
- val album = MutableStateFlow(null)
- val albumArt = MutableStateFlow(null)
+ override val playbackState = MutableStateFlow(PlaybackState.Stopped)
+ override val title = MutableStateFlow(null)
+ override val artist = MutableStateFlow(null)
+ override val album = MutableStateFlow(null)
+ override val albumArt = MutableStateFlow(null)
private var lastPlayer: String? = null
- set(value) {
- context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
- putString(PREFS_KEY_LAST_PLAYER, value)
- }
- field = value
- }
private var lastToken: String? = null
- fun setMediaSession(token: MediaSessionCompat.Token) {
+ private val semaphore = Semaphore(permits = 1)
+
+ override fun setMediaSession(token: MediaSessionCompat.Token, packageName: String) {
if (token.toString() == lastToken.toString()) return
- mediaController?.close()
- mediaController = MediaController.Builder(context)
- .setSessionCompatToken(token)
- .setControllerCallback(Executors.newSingleThreadExecutor(), mediaSessionCallback)
- .build()
- lastToken = token.toString()
+
+ scope.launch {
+ val filterMusicApps = dataStore.data.map { it.musicWidget.filterSources }.first()
+ if (filterMusicApps) {
+ val intent =
+ Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
+ if (context.packageManager.queryIntentActivities(intent, 0)
+ .none { it.activityInfo.packageName == packageName }
+ ) {
+ return@launch
+ }
+ }
+
+ semaphore.acquire()
+ mediaController?.close()
+ val appName = context.packageManager.getPackageInfo(
+ packageName,
+ 0
+ ).applicationInfo.loadLabel(context.packageManager)
+ mediaController = MediaController.Builder(context)
+ .setSessionCompatToken(token)
+ .setControllerCallback(
+ Executors.newSingleThreadExecutor(),
+ mediaSessionCallback
+ )
+ .build()
+
+ setMetadata(
+ title = context.getString(R.string.music_widget_default_title, appName),
+ artist = null,
+ album = null,
+ albumArt = null,
+ packageName
+ )
+ lastToken = token.toString()
+ semaphore.release()
+
+ }
}
private var mediaController: MediaController? = null
@@ -65,91 +121,52 @@ class MusicRepository(val context: Context) {
allowedCommands: SessionCommandGroup
) {
super.onConnected(controller, allowedCommands)
- updateMetadata()
- updateState()
+ if (!controller.isConnected) return
+ updateMetadata(controller.currentMediaItem, controller.connectedToken?.packageName)
+ updateState(controller.playerState)
}
override fun onCurrentMediaItemChanged(controller: MediaController, item: MediaItem?) {
super.onCurrentMediaItemChanged(controller, item)
- updateMetadata()
+ if (!controller.isConnected) return
+ updateMetadata(item, controller.connectedToken?.packageName)
}
override fun onPlayerStateChanged(controller: MediaController, state: Int) {
super.onPlayerStateChanged(controller, state)
- updateState()
- }
-
- override fun onPlaybackInfoChanged(
- controller: MediaController,
- info: MediaController.PlaybackInfo
- ) {
- super.onPlaybackInfoChanged(controller, info)
- Log.d("MM20", "CurrentPosition" + controller.currentPosition.toString())
+ if (!controller.isConnected) return
+ updateState(state)
}
override fun onDisconnected(controller: MediaController) {
super.onDisconnected(controller)
mediaController = null
}
-
- /*override fun onMetadataChanged(metadata: MediaController?) {
- super.onMetadataChanged(metadata)
- updateState()
- }
-
- override fun onSessionDestroyed() {
- super.onSessionDestroyed()
- mediaController = null
- hasActiveSession.value = false
- playbackState.value = PlaybackState.Stopped
- }*/
}
- private fun updateMetadata() {
- val metadata = mediaController?.currentMediaItem?.metadata ?: return
+ private fun updateMetadata(mediaItem: MediaItem?, playerPackage: String?) {
+ val metadata = mediaItem?.metadata ?: return
val title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE)
- ?: metadata.getString(MediaMetadata.METADATA_KEY_TITLE)
- val artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
+ ?: metadata.getString(MediaMetadata.METADATA_KEY_TITLE) ?: return
+ val artist = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)
+ ?: metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)
?: metadata.getString(MediaMetadata.METADATA_KEY_COMPOSER)
?: metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR)
?: metadata.getString(MediaMetadata.METADATA_KEY_WRITER)
?: return
val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM)
+ val albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
- lastPlayer = mediaController?.connectedToken?.packageName ?: lastPlayer
- this@MusicRepository.title.value = title
- this@MusicRepository.artist.value = artist
- this@MusicRepository.album.value = album
+ // Hack for Spotify sending inconsistent metadata updates
+ if (playerPackage == "com.spotify.music" && album == null) return
scope.launch {
- withContext(Dispatchers.IO) {
- val albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
- context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
- putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull")
- }
- if (albumArt == null) {
- this@MusicRepository.albumArt.value = null
- return@withContext
- }
- val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size)
- val scaledBitmap = albumArt.scale(size, size)
- val file = File(context.cacheDir, "album_art")
- val outStream = file.outputStream()
- scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
- outStream.close()
- this@MusicRepository.albumArt.value = scaledBitmap
- }
- }
-
- context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
- putString(PREFS_KEY_TITLE, title)
- putString(PREFS_KEY_ARTIST, artist)
- putString(PREFS_KEY_ALBUM, album)
+ setMetadata(title, artist, album, albumArt, playerPackage)
}
}
- private fun updateState() {
- val playbackState = when (mediaController?.playerState) {
+ private fun updateState(playerState: Int) {
+ val playbackState = when (playerState) {
SessionPlayer.PLAYER_STATE_PLAYING -> PlaybackState.Playing
SessionPlayer.PLAYER_STATE_PAUSED -> PlaybackState.Paused
else -> PlaybackState.Stopped
@@ -157,8 +174,6 @@ class MusicRepository(val context: Context) {
this.playbackState.value = playbackState
}
- val hasActiveSession: Boolean = mediaController?.isConnected != null
-
init {
loadLastPlaybackMetadata()
}
@@ -176,12 +191,12 @@ class MusicRepository(val context: Context) {
val albumArt = withContext(Dispatchers.IO) {
BitmapFactory.decodeFile(File(context.cacheDir, "album_art").absolutePath)
}
- this@MusicRepository.albumArt.value = albumArt
+ this@MusicRepositoryImpl.albumArt.value = albumArt
}
playbackState.value = PlaybackState.Stopped
}
- fun previous() {
+ override fun previous() {
if (mediaController?.skipToPreviousPlaylistItem()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
@@ -191,7 +206,7 @@ class MusicRepository(val context: Context) {
}
}
- fun next() {
+ override fun next() {
if (mediaController?.skipToNextPlaylistItem()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
@@ -201,7 +216,7 @@ class MusicRepository(val context: Context) {
}
}
- fun play() {
+ override fun play() {
if (mediaController?.play()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)
@@ -211,7 +226,7 @@ class MusicRepository(val context: Context) {
}
}
- fun pause() {
+ override fun pause() {
if (mediaController?.pause()?.get() == null) {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
@@ -221,18 +236,24 @@ class MusicRepository(val context: Context) {
}
}
- fun togglePause() {
+ override fun togglePause() {
if (playbackState.value != PlaybackState.Playing) play() else pause()
}
- fun getLaunchIntent(): PendingIntent {
+ override fun openPlayer(): PendingIntent? {
mediaController?.sessionActivity?.let {
return it
}
- val intent = Intent(Intent.ACTION_MAIN)
- .setPackage(lastPlayer)
- .addCategory(Intent.CATEGORY_APP_MUSIC)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ val intent = lastPlayer?.let {
+ context.packageManager.getLaunchIntentForPackage(it)?.apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ } ?: return null
+
+ if (context.packageManager.resolveActivity(intent, 0) == null) {
+ return null
+ }
return PendingIntent.getActivity(
context,
@@ -242,7 +263,7 @@ class MusicRepository(val context: Context) {
)
}
- fun openPlayerChooser(context: Context) {
+ override fun openPlayerChooser(context: Context) {
context.startActivity(
Intent.createChooser(
Intent(Intent.ACTION_MAIN)
@@ -255,6 +276,51 @@ class MusicRepository(val context: Context) {
)
}
+ private suspend fun setMetadata(
+ title: String?,
+ artist: String?,
+ album: String?,
+ albumArt: Bitmap?,
+ playerPackage: String?
+ ) {
+ withContext(Dispatchers.IO) {
+ if (albumArt == null) {
+ this@MusicRepositoryImpl.albumArt.value = null
+ } else {
+ val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size)
+ val scaledBitmap = albumArt.scale(size, size)
+ val file = File(context.cacheDir, "album_art")
+ val outStream = file.outputStream()
+ scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
+ outStream.close()
+ this@MusicRepositoryImpl.albumArt.value = scaledBitmap
+
+ }
+
+ context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
+ putString(PREFS_KEY_TITLE, title)
+ putString(PREFS_KEY_ARTIST, artist)
+ putString(PREFS_KEY_ALBUM, album)
+ putString(PREFS_KEY_LAST_PLAYER, playerPackage)
+ putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull")
+ }
+
+
+ lastPlayer = playerPackage ?: lastPlayer
+ this@MusicRepositoryImpl.title.value = title
+ this@MusicRepositoryImpl.artist.value = artist
+ this@MusicRepositoryImpl.album.value = album
+ }
+ }
+
+ override fun resetPlayer() {
+ scope.launch {
+ mediaController?.close()
+ mediaController = null
+ setMetadata(null, null, null, null, null)
+ }
+ }
+
companion object {
private const val PREFS = "music"
diff --git a/notifications/src/main/java/de/mm20/launcher2/notifications/NotificationService.kt b/notifications/src/main/java/de/mm20/launcher2/notifications/NotificationService.kt
index 1a0073a2..50c176d4 100644
--- a/notifications/src/main/java/de/mm20/launcher2/notifications/NotificationService.kt
+++ b/notifications/src/main/java/de/mm20/launcher2/notifications/NotificationService.kt
@@ -34,13 +34,13 @@ class NotificationService : NotificationListenerService() {
Log.d("MM20", "Notification listener connected")
permissionsManager.reportNotificationListenerState(true)
instance = WeakReference(this)
- val notifications = getNotifications().sortedByDescending { it.postTime }
+ val notifications = getNotifications().sortedBy { it.postTime }
for (n in notifications) {
- val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
- if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue
+ /*val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
+ if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue*/
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: continue
- musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
+ musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token), n.packageName)
}
if (LauncherPreferences.instance.notificationBadges) {
generateBadges()
@@ -79,11 +79,11 @@ class NotificationService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
if (sbn.notification.category == Notification.CATEGORY_TRANSPORT || sbn.notification.category == Notification.CATEGORY_SERVICE) {
- val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
- if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return
+ /*val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_APP_MUSIC) }
+ if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return*/
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
?: return
- musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
+ musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token), sbn.packageName)
}
if (LauncherPreferences.instance.notificationBadges) {
val pkg = sbn.packageName
diff --git a/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt b/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt
index f243a087..2b09b9ca 100644
--- a/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt
+++ b/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt
@@ -16,5 +16,10 @@ fun createFactorySettings(context: Context): Settings {
.setImperialUnits(context.resources.getBoolean(R.bool.default_imperialUnits))
.build()
)
+ .setMusicWidget(Settings.MusicWidgetSettings
+ .newBuilder()
+ .setFilterSources(true)
+ .build()
+ )
.build()
}
\ No newline at end of file
diff --git a/preferences/src/main/proto/settings.proto b/preferences/src/main/proto/settings.proto
index 81b08a27..169f294f 100644
--- a/preferences/src/main/proto/settings.proto
+++ b/preferences/src/main/proto/settings.proto
@@ -32,4 +32,9 @@ message Settings {
}
WeatherSettings weather = 5;
+ message MusicWidgetSettings {
+ bool filter_sources = 1;
+ }
+ MusicWidgetSettings music_widget = 6;
+
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
index 88b9ce4f..41bf4402 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
@@ -1,5 +1,7 @@
-package de.mm20.launcher2.ui.widget
+package de.mm20.launcher2.ui.launcher.widgets.music
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
@@ -10,6 +12,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Audiotrack
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material.icons.rounded.SkipPrevious
@@ -24,13 +27,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.ktx.conditional
-import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidgetVM
@OptIn(
ExperimentalFoundationApi::class,
@@ -49,107 +53,152 @@ fun MusicWidget() {
val context = LocalContext.current
- Row(
- modifier = Modifier.height(IntrinsicSize.Min)
+ Column(
+ modifier = Modifier.fillMaxWidth()
) {
- Column(
- modifier = Modifier
- .padding(top = 16.dp)
- .fillMaxHeight()
- .weight(2f),
- verticalArrangement = Arrangement.SpaceBetween
- ) {
- Column(
- modifier = Modifier.padding(horizontal = 16.dp)
- ) {
- Text(
- text = title ?: "---",
- style = MaterialTheme.typography.titleMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- Text(
- text = artist ?: "---",
- modifier = Modifier.padding(vertical = 2.dp),
- style = MaterialTheme.typography.bodySmall,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- Text(
- text = album ?: "---",
- style = MaterialTheme.typography.bodySmall,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 4.dp, end = 4.dp),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- IconButton(
- onClick = {
- viewModel.skipPrevious()
- }) {
- Icon(
- imageVector = Icons.Rounded.SkipPrevious,
- null
- )
+ val hasPermission by viewModel.hasPermission.observeAsState()
+ AnimatedVisibility(hasPermission == false) {
+ MissingPermissionBanner(
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(R.string.missing_permission_music_widget),
+ onClick = {
+ viewModel.requestPermission(context as AppCompatActivity)
}
- val playPauseIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
- IconButton(onClick = { viewModel.togglePause() }) {
- Icon(
- painter = rememberAnimatedVectorPainter(playPauseIcon, atEnd = playbackState == PlaybackState.Playing),
- contentDescription = ""
- )
- }
- IconButton(onClick = {
- viewModel.skipNext()
- }) {
- Icon(
- imageVector = Icons.Rounded.SkipNext,
- null
- )
- }
- }
+ )
}
- Box(
- modifier = Modifier
- .size(144.dp)
- .combinedClickable(
- onClick = {
- viewModel.openPlayer()
- },
- onLongClick = {
- viewModel.openPlayerSelector(context)
+ if (title == null && artist == null && album == null) {
+ NoData()
+ } else {
+ Row(
+ modifier = Modifier.height(IntrinsicSize.Min)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(top = 16.dp)
+ .fillMaxHeight()
+ .weight(2f),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Text(
+ text = title ?: "",
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = artist ?: "",
+ modifier = Modifier.padding(vertical = 2.dp),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = album ?: "",
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
}
- )
- .conditional(
- albumArt == null,
- Modifier.background(
- MaterialTheme.colorScheme.primaryContainer,
- )
- ),
- contentAlignment = Alignment.Center
- ) {
- if (albumArt != null) {
- albumArt?.let {
- Image(
- bitmap = it.asImageBitmap(),
+ Row(
modifier = Modifier
- .fillMaxSize(),
- contentDescription = null
- )
+ .fillMaxWidth()
+ .padding(bottom = 4.dp, end = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ IconButton(
+ onClick = {
+ viewModel.skipPrevious()
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.SkipPrevious,
+ null
+ )
+ }
+ val playPauseIcon =
+ AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
+ IconButton(onClick = { viewModel.togglePause() }) {
+ Icon(
+ painter = rememberAnimatedVectorPainter(
+ playPauseIcon,
+ atEnd = playbackState == PlaybackState.Playing
+ ),
+ contentDescription = ""
+ )
+ }
+ IconButton(onClick = {
+ viewModel.skipNext()
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.SkipNext,
+ null
+ )
+ }
+ }
+ }
+ Box(
+ modifier = Modifier
+ .size(144.dp)
+ .combinedClickable(
+ onClick = {
+ viewModel.openPlayer()
+ },
+ onLongClick = {
+ viewModel.openPlayerSelector(context)
+ }
+ )
+ .conditional(
+ albumArt == null,
+ Modifier.background(
+ MaterialTheme.colorScheme.primaryContainer,
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ if (albumArt != null) {
+ albumArt?.let {
+ Image(
+ bitmap = it.asImageBitmap(),
+ modifier = Modifier
+ .fillMaxSize(),
+ contentDescription = null
+ )
+ }
+ } else {
+ Icon(
+ imageVector = Icons.Rounded.MusicNote,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onPrimaryContainer,
+ modifier = Modifier.size(56.dp)
+ )
+ }
}
- } else {
- Icon(
- imageVector = Icons.Rounded.MusicNote,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.onPrimaryContainer,
- modifier = Modifier.size(56.dp)
- )
}
}
}
+}
+
+@Composable
+fun NoData() {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Audiotrack,
+ contentDescription = "",
+ modifier = Modifier
+ .padding(24.dp)
+ .size(32.dp),
+ tint = MaterialTheme.colorScheme.secondary
+ )
+ Text(
+ text = stringResource(id = R.string.music_widget_no_data),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt
index 629f32d4..9ea6ceac 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidgetVM.kt
@@ -3,17 +3,21 @@ package de.mm20.launcher2.ui.launcher.widgets.music
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
+import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.music.MusicRepository
import de.mm20.launcher2.music.PlaybackState
+import de.mm20.launcher2.permissions.PermissionGroup
+import de.mm20.launcher2.permissions.PermissionsManager
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MusicWidgetVM: ViewModel(), KoinComponent {
private val musicRepository: MusicRepository by inject()
+ private val permissionsManager: PermissionsManager by inject()
val title: LiveData = musicRepository.title.asLiveData()
val artist: LiveData = musicRepository.artist.asLiveData()
@@ -21,6 +25,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
val albumArt: LiveData = musicRepository.albumArt.asLiveData()
val playbackState: LiveData = musicRepository.playbackState.asLiveData()
+ val hasPermission = permissionsManager.hasPermission(PermissionGroup.Notifications).asLiveData()
+
fun skipPrevious() {
musicRepository.previous()
}
@@ -35,7 +41,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
fun openPlayer() {
try {
- musicRepository.getLaunchIntent().send()
+ musicRepository.
+ openPlayer()?.send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
@@ -44,4 +51,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
fun openPlayerSelector(context: Context) {
musicRepository.openPlayerChooser(context)
}
+
+ fun requestPermission(context: AppCompatActivity) {
+ permissionsManager.requestPermission(context, PermissionGroup.Notifications)
+ }
}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
index 9ed562f1..ccb27044 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt
@@ -10,7 +10,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R
-import de.mm20.launcher2.ui.widget.MusicWidget
+import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
class MusicWidget : LauncherWidget {
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
index b720ca40..e23ad411 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
@@ -26,6 +26,7 @@ import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
+import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
@@ -89,6 +90,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/widgets/weather") {
WeatherWidgetSettingsScreen()
}
+ composable("settings/widgets/music") {
+ MusicWidgetSettingsScreen()
+ }
composable("settings/about") {
AboutSettingsScreen()
}
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreen.kt
new file mode 100644
index 00000000..5b4e1074
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreen.kt
@@ -0,0 +1,66 @@
+package de.mm20.launcher2.ui.settings.musicwidget
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import de.mm20.launcher2.ui.BuildConfig
+import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.component.MissingPermissionBanner
+import de.mm20.launcher2.ui.component.preferences.Preference
+import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
+import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
+import de.mm20.launcher2.ui.component.preferences.SwitchPreference
+
+@Composable
+fun MusicWidgetSettingsScreen() {
+ val context = LocalContext.current
+ val viewModel: MusicWidgetSettingsScreenVM = viewModel()
+ val hasPermission by viewModel.hasPermission.observeAsState()
+ PreferenceScreen(
+ stringResource(R.string.preference_screen_musicwidget)
+ ) {
+ item {
+ PreferenceCategory {
+ AnimatedVisibility(hasPermission == false) {
+ MissingPermissionBanner(
+ text = stringResource(R.string.missing_permission_music_widget),
+ onClick = {
+ viewModel.requestNotificationPermission(context as AppCompatActivity)
+ },
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ val filterSources by viewModel.filterSources.observeAsState(false)
+ SwitchPreference(
+ title = stringResource(R.string.preference_music_filter_sources),
+ summary = stringResource(R.string.preference_music_filter_sources_summary),
+ value = filterSources,
+ onValueChanged = {
+ viewModel.setFilterSources(it)
+ }
+ )
+ }
+ }
+ if (BuildConfig.DEBUG) {
+ item {
+ PreferenceCategory(stringResource(R.string.preference_category_debug)) {
+ Preference(
+ title = "Reset widget",
+ summary = "Clear all music data",
+ onClick = {
+ viewModel.resetWidget()
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreenVM.kt
new file mode 100644
index 00000000..3e1c0483
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/musicwidget/MusicWidgetSettingsScreenVM.kt
@@ -0,0 +1,46 @@
+package de.mm20.launcher2.ui.settings.musicwidget
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import de.mm20.launcher2.music.MusicRepository
+import de.mm20.launcher2.permissions.PermissionGroup
+import de.mm20.launcher2.permissions.PermissionsManager
+import de.mm20.launcher2.preferences.LauncherDataStore
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class MusicWidgetSettingsScreenVM : ViewModel(), KoinComponent {
+ private val permissionsManager: PermissionsManager by inject()
+ private val musicRepository: MusicRepository by inject()
+ private val dataStore: LauncherDataStore by inject()
+ val hasPermission =
+ permissionsManager.hasPermission(PermissionGroup.Notifications).asLiveData()
+
+
+ fun requestNotificationPermission(activity: AppCompatActivity) {
+ permissionsManager.requestPermission(activity, PermissionGroup.Notifications)
+ }
+
+ val filterSources = dataStore.data.map { it.musicWidget.filterSources }.asLiveData()
+ fun setFilterSources(filterSources: Boolean) {
+ viewModelScope.launch {
+ dataStore.updateData {
+ it.toBuilder()
+ .setMusicWidget(
+ it.musicWidget.toBuilder()
+ .setFilterSources(filterSources)
+ )
+ .build()
+ }
+ }
+ }
+
+ fun resetWidget() {
+ musicRepository.resetPlayer()
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetsSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetsSettingsScreen.kt
index adcfc723..bcc42009 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetsSettingsScreen.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetsSettingsScreen.kt
@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.settings.widgets
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Audiotrack
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Today
import androidx.compose.runtime.Composable
@@ -22,6 +23,13 @@ fun WidgetsSettingsScreen() {
navController?.navigate("settings/widgets/weather")
}
)
+ Preference(
+ title = stringResource(R.string.preference_screen_musicwidget),
+ icon = Icons.Rounded.Audiotrack,
+ onClick = {
+ navController?.navigate("settings/widgets/music")
+ }
+ )
Preference(
title = stringResource(R.string.preference_screen_calendarwidget),
icon = Icons.Rounded.Today
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/widget/WidgetCard.kt b/ui/src/main/java/de/mm20/launcher2/ui/widget/WidgetCard.kt
index 12a8f337..e800364a 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/widget/WidgetCard.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/widget/WidgetCard.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
+import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType