Add option to disable music widget filtering

This commit is contained in:
MM20 2022-01-06 23:43:50 +01:00
parent 51e9370dd5
commit d3358fa7b5
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
16 changed files with 477 additions and 198 deletions

View File

@ -419,7 +419,8 @@
<string name="preference_grid_column_count">Spaltenanzahl</string>
<string name="grant_permission">Gewähren</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt, um den Standort automatisch zu ermitteln</string>
<string name="missing_permission_auto_location">Standortzugriff wird benötigt um den Standort automatisch zu ermitteln</string>
<string name="missing_permission_music_widget">Benachrichtigungszugriff wird benötigt um Medienwiedergabe zu steuern</string>
<string name="weather_widget_set_location">Standort festlegen</string>
<string name="preference_screen_debug">Debug</string>
@ -429,7 +430,14 @@
<string name="preference_screen_widgets_summary">Widgets konfigurieren</string>
<string name="preference_screen_weatherwidget">Wetter</string>
<string name="preference_screen_musicwidget">Musik</string>
<string name="preference_crash_reporter">Crash-Reporter</string>
<string name="preference_crash_reporter_summary">Fehler- und Absturzberichte</string>
<string name="preference_music_filter_sources">Auf Musik-Apps begrenzen</string>
<string name="preference_music_filter_sources_summary">Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind</string>
<string name="music_widget_default_title">%1$s spielt Medien</string>
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
</resources>

View File

@ -458,6 +458,7 @@
<string name="grant_permission">Grant</string>
<string name="missing_permission_auto_location">Location access is required to determine the location automatically</string>
<string name="missing_permission_music_widget">Notification access is required to control media playback</string>
<string name="weather_widget_set_location">Set location</string>
<string name="preference_screen_debug">Debug</string>
@ -467,7 +468,15 @@
<string name="preference_screen_widgets_summary">Configure widgets</string>
<string name="preference_screen_weatherwidget">Weather</string>
<string name="preference_screen_musicwidget">Music</string>
<string name="preference_crash_reporter">Crash reporter</string>
<string name="preference_crash_reporter_summary">Error and crash reports</string>
<string name="preference_music_filter_sources">Restrict to music apps</string>
<string name="preference_music_filter_sources_summary">Ignore media sessions of apps that are not music apps</string>
<string name="music_widget_default_title">%1$s is playing media</string>
<string name="music_widget_no_data">No media has been played yet</string>
</resources>

View File

@ -43,5 +43,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":ktx"))
implementation(project(":preferences"))
}

View File

@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val musicModule = module {
single { MusicRepository(androidContext()) }
single<MusicRepository> { MusicRepositoryImpl(androidContext()) }
}

View File

@ -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<PlaybackState>
val title: Flow<String?>
val artist: Flow<String?>
val album: Flow<String?>
val albumArt: Flow<Bitmap?>
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<String?>(null)
val artist = MutableStateFlow<String?>(null)
val album = MutableStateFlow<String?>(null)
val albumArt = MutableStateFlow<Bitmap?>(null)
override val playbackState = MutableStateFlow(PlaybackState.Stopped)
override val title = MutableStateFlow<String?>(null)
override val artist = MutableStateFlow<String?>(null)
override val album = MutableStateFlow<String?>(null)
override val albumArt = MutableStateFlow<Bitmap?>(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"

View File

@ -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

View File

@ -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()
}

View File

@ -32,4 +32,9 @@ message Settings {
}
WeatherSettings weather = 5;
message MusicWidgetSettings {
bool filter_sources = 1;
}
MusicWidgetSettings music_widget = 6;
}

View File

@ -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
)
}
}

View File

@ -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<String?> = musicRepository.title.asLiveData()
val artist: LiveData<String?> = musicRepository.artist.asLiveData()
@ -21,6 +25,8 @@ class MusicWidgetVM: ViewModel(), KoinComponent {
val albumArt: LiveData<Bitmap?> = musicRepository.albumArt.asLiveData()
val playbackState: LiveData<PlaybackState> = 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)
}
}

View File

@ -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 {

View File

@ -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()
}

View File

@ -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()
}
)
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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