Refactor music widget

This commit is contained in:
MM20 2021-12-19 18:29:52 +01:00
parent 71652ffc71
commit 8be19a1309
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 88 additions and 95 deletions

View File

@ -74,7 +74,7 @@ fun buildTime(): String {
} }
fun versionCodeDate(): Int { fun versionCodeDate(): Int {
val df = SimpleDateFormat("yyyyMMdd00") val df = SimpleDateFormat("yyyyMMdd01")
return df.format(Date()).toInt() return df.format(Date()).toInt()
} }

View File

@ -6,5 +6,4 @@ import org.koin.dsl.module
val musicModule = module { val musicModule = module {
single { MusicRepository(androidContext()) } single { MusicRepository(androidContext()) }
viewModel { MusicViewModel(get()) }
} }

View File

@ -6,33 +6,30 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.graphics.scale import androidx.core.graphics.scale
import androidx.lifecycle.MutableLiveData
import androidx.media2.common.MediaItem import androidx.media2.common.MediaItem
import androidx.media2.common.MediaMetadata import androidx.media2.common.MediaMetadata
import androidx.media2.common.SessionPlayer import androidx.media2.common.SessionPlayer
import androidx.media2.session.MediaController import androidx.media2.session.MediaController
import androidx.media2.session.SessionCommandGroup import androidx.media2.session.SessionCommandGroup
import de.mm20.launcher2.ktx.asBitmap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.util.concurrent.Executors import java.util.concurrent.Executors
class MusicRepository(val context: Context) { class MusicRepository(val context: Context) {
private val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
val playbackState = MutableLiveData<PlaybackState>() val playbackState = MutableStateFlow(PlaybackState.Stopped)
val title = MutableLiveData<String?>() val title = MutableStateFlow<String?>(null)
val artist = MutableLiveData<String?>() val artist = MutableStateFlow<String?>(null)
val album = MutableLiveData<String?>() val album = MutableStateFlow<String?>(null)
val albumArt = MutableLiveData<Bitmap?>() val albumArt = MutableStateFlow<Bitmap?>(null)
private var lastPlayer: String? = null private var lastPlayer: String? = null
set(value) { set(value) {
@ -57,7 +54,7 @@ class MusicRepository(val context: Context) {
private var mediaController: MediaController? = null private var mediaController: MediaController? = null
set(value) { set(value) {
if (value == null) { if (value == null) {
playbackState.postValue(PlaybackState.Stopped) playbackState.value = PlaybackState.Stopped
} }
field = value field = value
} }
@ -120,9 +117,9 @@ class MusicRepository(val context: Context) {
val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM) val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM)
lastPlayer = mediaController?.connectedToken?.packageName ?: lastPlayer lastPlayer = mediaController?.connectedToken?.packageName ?: lastPlayer
this@MusicRepository.title.postValue(title) this@MusicRepository.title.value = title
this@MusicRepository.artist.postValue(artist) this@MusicRepository.artist.value = artist
this@MusicRepository.album.postValue(album) this@MusicRepository.album.value = album
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -131,7 +128,7 @@ class MusicRepository(val context: Context) {
putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull") putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull")
} }
if (albumArt == null) { if (albumArt == null) {
this@MusicRepository.albumArt.postValue(null) this@MusicRepository.albumArt.value = null
return@withContext return@withContext
} }
val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size) val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size)
@ -140,7 +137,7 @@ class MusicRepository(val context: Context) {
val outStream = file.outputStream() val outStream = file.outputStream()
scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream) scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream)
outStream.close() outStream.close()
this@MusicRepository.albumArt.postValue(scaledBitmap) this@MusicRepository.albumArt.value = scaledBitmap
} }
} }
@ -157,7 +154,7 @@ class MusicRepository(val context: Context) {
SessionPlayer.PLAYER_STATE_PAUSED -> PlaybackState.Paused SessionPlayer.PLAYER_STATE_PAUSED -> PlaybackState.Paused
else -> PlaybackState.Stopped else -> PlaybackState.Stopped
} }
this@MusicRepository.playbackState.postValue(playbackState) this.playbackState.value = playbackState
} }
val hasActiveSession: Boolean = mediaController?.isConnected != null val hasActiveSession: Boolean = mediaController?.isConnected != null
@ -228,7 +225,7 @@ class MusicRepository(val context: Context) {
if (playbackState.value != PlaybackState.Playing) play() else pause() if (playbackState.value != PlaybackState.Playing) play() else pause()
} }
fun getLaunchIntent(context: Context): PendingIntent { fun getLaunchIntent(): PendingIntent {
mediaController?.sessionActivity?.let { mediaController?.sessionActivity?.let {
return it return it
} }

View File

@ -1,48 +0,0 @@
package de.mm20.launcher2.music
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
class MusicViewModel(
val musicRepository: MusicRepository
) : ViewModel() {
val title: LiveData<String?> = musicRepository.title
val artist: LiveData<String?> = musicRepository.artist
val album: LiveData<String?> = musicRepository.album
val albumArt: LiveData<Bitmap?> = musicRepository.albumArt
val playbackState: LiveData<PlaybackState> = musicRepository.playbackState
val hasActiveSession : Boolean = musicRepository.hasActiveSession
fun previous() {
musicRepository.previous()
}
fun next() {
musicRepository.next()
}
fun play() {
musicRepository.play()
}
fun pause() {
musicRepository.pause()
}
fun togglePause() {
musicRepository.togglePause()
}
fun getLaunchIntent(context: Context): PendingIntent {
return musicRepository.getLaunchIntent(context)
}
fun openPlayerChooser(context: Context) {
musicRepository.openPlayerChooser(context)
}
}

View File

@ -0,0 +1,17 @@
package de.mm20.launcher2.ui.launcher.search
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.search.data.File
import org.koin.core.component.KoinComponent
class SearchViewModel: ViewModel(), KoinComponent {
fun search(query: String) {
}
private val _files = MutableLiveData<List<File>>()
val files: LiveData<List<File>> = _files
}

View File

@ -21,26 +21,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.music.MusicViewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.music.PlaybackState import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.conditional import de.mm20.launcher2.ui.ktx.conditional
import org.koin.androidx.compose.getViewModel import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidgetVM
@OptIn( @OptIn(
ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class,
ExperimentalAnimationGraphicsApi::class ExperimentalAnimationGraphicsApi::class
) )
@Composable @Composable
fun MusicWidget() { fun MusicWidget() {
val viewModel: MusicViewModel = getViewModel() val viewModel: MusicWidgetVM = viewModel()
val albumArt by viewModel.albumArt.observeAsState() val albumArt by viewModel.albumArt.observeAsState()
val title by viewModel.title.observeAsState() val title by viewModel.title.observeAsState()
@ -91,7 +90,7 @@ fun MusicWidget() {
) { ) {
IconButton( IconButton(
onClick = { onClick = {
viewModel.previous() viewModel.onPreviousClick()
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.SkipPrevious, imageVector = Icons.Rounded.SkipPrevious,
@ -99,14 +98,14 @@ fun MusicWidget() {
) )
} }
val playPauseIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause) val playPauseIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_play_pause)
IconButton(onClick = { viewModel.togglePause() }) { IconButton(onClick = { viewModel.onPlayClick() }) {
Icon( Icon(
painter = rememberAnimatedVectorPainter(playPauseIcon, atEnd = playbackState == PlaybackState.Playing), painter = rememberAnimatedVectorPainter(playPauseIcon, atEnd = playbackState == PlaybackState.Playing),
contentDescription = "" contentDescription = ""
) )
} }
IconButton(onClick = { IconButton(onClick = {
viewModel.next() viewModel.onNextClick()
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.SkipNext, imageVector = Icons.Rounded.SkipNext,
@ -120,12 +119,10 @@ fun MusicWidget() {
.size(144.dp) .size(144.dp)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
viewModel viewModel.onAlbumArtClick()
.getLaunchIntent(context)
.send()
}, },
onLongClick = { onLongClick = {
viewModel.openPlayerChooser(context) viewModel.onAlbumArtLongClick(context)
} }
) )
.conditional( .conditional(

View File

@ -0,0 +1,47 @@
package de.mm20.launcher2.ui.launcher.widgets.music
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
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 org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class MusicWidgetVM: ViewModel(), KoinComponent {
private val musicRepository: MusicRepository by inject()
val title: LiveData<String?> = musicRepository.title.asLiveData()
val artist: LiveData<String?> = musicRepository.artist.asLiveData()
val album: LiveData<String?> = musicRepository.album.asLiveData()
val albumArt: LiveData<Bitmap?> = musicRepository.albumArt.asLiveData()
val playbackState: LiveData<PlaybackState> = musicRepository.playbackState.asLiveData()
fun onPreviousClick() {
musicRepository.previous()
}
fun onNextClick() {
musicRepository.next()
}
fun onPlayClick() {
musicRepository.togglePause()
}
fun onAlbumArtClick() {
try {
musicRepository.getLaunchIntent().send()
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}
fun onAlbumArtLongClick(context: Context) {
musicRepository.openPlayerChooser(context)
}
}

View File

@ -1,30 +1,16 @@
package de.mm20.launcher2.ui.legacy.widget package de.mm20.launcher2.ui.legacy.widget
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.AnimatedVectorDrawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.core.content.ContextCompat
import androidx.core.graphics.alpha
import androidx.lifecycle.Observer
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.legacy.helper.ActivityStarter
import de.mm20.launcher2.music.MusicViewModel
import de.mm20.launcher2.music.PlaybackState
import de.mm20.launcher2.ui.LegacyLauncherTheme import de.mm20.launcher2.ui.LegacyLauncherTheme
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.CompactMusicBinding
import de.mm20.launcher2.ui.widget.MusicWidget import de.mm20.launcher2.ui.widget.MusicWidget
import org.koin.androidx.viewmodel.ext.android.viewModel
class MusicWidget : LauncherWidget { class MusicWidget : LauncherWidget {
@ -33,8 +19,6 @@ class MusicWidget : LauncherWidget {
override val name: String override val name: String
get() = context.getString(R.string.widget_name_music) get() = context.getString(R.string.widget_name_music)
private val viewModel: MusicViewModel by (context as AppCompatActivity).viewModel()
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes)