From c477d2295ad5dc4e7a562e6d38f17ad5588bbf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20=C4=90=E1=BB=A9c=20Tu=E1=BA=A5n=20Minh?= Date: Wed, 11 Oct 2023 14:01:16 +0700 Subject: [PATCH] Better architecture. Hot fix crash --- .idea/kotlinc.xml | 2 +- app/build.gradle.kts | 6 +- app/src/main/AndroidManifest.xml | 4 +- .../maxrave/simpmusic/SimpMusicApplication.kt | 12 + .../moodandgenre/genre/GenreItemAdapter.kt | 3 +- .../moodandgenre/mood/MoodItemAdapter.kt | 3 +- .../adapter/playlist/PlaylistItemAdapter.kt | 4 +- .../data/dataStore/DataStoreManager.kt | 5 +- .../simpmusic/data/model/home/HomeResponse.kt | 3 +- .../simpmusic/data/parser/ChartParser.kt | 2 +- .../simpmusic/data/parser/HomeParser.kt | 8 +- .../data/parser/MoodsGenresParser.kt | 14 +- .../data/repository/MainRepository.kt | 4 +- .../simpmusic/di/LocalServiceModule.kt | 4 +- .../simpmusic/di/MusicServiceModule.kt | 180 +----- .../simpmusic/service/SimpleMediaService.kt | 264 +++++++-- .../service/SimpleMediaServiceHandler.kt | 406 +++++++++++-- .../service/test/download/DownloadUtils.kt | 3 +- .../service/test/source/MusicSource.kt | 548 +++++++++--------- .../com/maxrave/simpmusic/ui/MainActivity.kt | 272 +++++---- .../simpmusic/ui/fragment/SearchFragment.kt | 3 - .../ui/fragment/home/SettingsFragment.kt | 10 +- .../ui/fragment/other/AlbumFragment.kt | 2 +- .../ui/fragment/player/InfoFragment.kt | 33 +- .../ui/fragment/player/NowPlayingFragment.kt | 504 ++++++++-------- .../ui/fragment/player/QueueFragment.kt | 90 +-- .../simpmusic/viewModel/SearchViewModel.kt | 4 - .../simpmusic/viewModel/SettingsViewModel.kt | 4 - .../simpmusic/viewModel/SharedViewModel.kt | 369 ++++++------ .../res/navigation/nav_bottom_navigation.xml | 14 - build.gradle.kts | 2 +- kotlinYtmusicScraper/build.gradle.kts | 2 +- .../maxrave/kotlinytmusicscraper/Ytmusic.kt | 6 - .../models/response/CreatePlaylistResponse.kt | 4 +- .../test/CustomRedirectConfig.kt | 14 +- 35 files changed, 1580 insertions(+), 1228 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d994..f8467b45 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 564882d8..dee511d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") //material design3 - implementation("com.google.android.material:material:1.9.0") + implementation("com.google.android.material:material:1.10.0") //runtime implementation("androidx.startup:startup-runtime:1.1.1") implementation(project(mapOf("path" to ":kotlinYtmusicScraper"))) @@ -93,8 +93,8 @@ dependencies { //Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") //Navigation - implementation("androidx.navigation:navigation-fragment-ktx:2.7.3") - implementation("androidx.navigation:navigation-ui-ktx:2.7.3") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") + implementation("androidx.navigation:navigation-ui-ktx:2.7.4") implementation("com.google.code.gson:gson:2.10.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 727481b1..1a6c9eb5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + - - - diff --git a/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt b/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt index 78517e5d..47cc4ace 100644 --- a/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt +++ b/app/src/main/java/com/maxrave/simpmusic/SimpMusicApplication.kt @@ -1,6 +1,7 @@ package com.maxrave.simpmusic import android.app.Application +import android.util.Log import androidx.appcompat.app.AppCompatDelegate import androidx.media3.common.util.UnstableApi import cat.ereza.customactivityoncrash.config.CaocConfig @@ -26,4 +27,15 @@ class SimpMusicApplication: Application(){ .restartActivity(MainActivity::class.java) //default: null (your app's launch activity) .apply() } + + override fun onLowMemory() { + super.onLowMemory() + Log.w("Low Memory", "Checking") + } + + override fun onTerminate() { + super.onTerminate() + + Log.w("Terminate", "Checking") + } } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/genre/GenreItemAdapter.kt b/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/genre/GenreItemAdapter.kt index 84847b39..c990b9ec 100644 --- a/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/genre/GenreItemAdapter.kt +++ b/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/genre/GenreItemAdapter.kt @@ -13,9 +13,8 @@ import com.maxrave.simpmusic.data.model.explore.mood.genre.ItemsPlaylist import com.maxrave.simpmusic.databinding.ItemMoodMomentPlaylistBinding class GenreItemAdapter(private var genreList: ArrayList, val context: Context, val navController: NavController): RecyclerView.Adapter() { - inner class ViewHolder(val binding: ItemMoodMomentPlaylistBinding): RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemMoodMomentPlaylistBinding): RecyclerView.ViewHolder(binding.root) - } fun updateData(newList: ArrayList){ genreList.clear() genreList.addAll(newList) diff --git a/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/mood/MoodItemAdapter.kt b/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/mood/MoodItemAdapter.kt index a87177b1..b349f997 100644 --- a/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/mood/MoodItemAdapter.kt +++ b/app/src/main/java/com/maxrave/simpmusic/adapter/moodandgenre/mood/MoodItemAdapter.kt @@ -13,9 +13,8 @@ import com.maxrave.simpmusic.data.model.explore.mood.moodmoments.Item import com.maxrave.simpmusic.databinding.ItemMoodMomentPlaylistBinding class MoodItemAdapter(private var itemList: ArrayList, val context: Context, val navController: NavController): RecyclerView.Adapter() { - inner class ViewHolder(val binding: ItemMoodMomentPlaylistBinding): RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemMoodMomentPlaylistBinding): RecyclerView.ViewHolder(binding.root) - } fun updateData(newList: ArrayList){ itemList.clear() itemList.addAll(newList) diff --git a/app/src/main/java/com/maxrave/simpmusic/adapter/playlist/PlaylistItemAdapter.kt b/app/src/main/java/com/maxrave/simpmusic/adapter/playlist/PlaylistItemAdapter.kt index 84fc174d..5697c4be 100644 --- a/app/src/main/java/com/maxrave/simpmusic/adapter/playlist/PlaylistItemAdapter.kt +++ b/app/src/main/java/com/maxrave/simpmusic/adapter/playlist/PlaylistItemAdapter.kt @@ -65,12 +65,12 @@ class PlaylistItemAdapter(private var playlistItemList: ArrayList): Recycle } } fun updateList(newList: ArrayList){ - Log.d("PlaylistItemAdapter", "updateList: ${newList.toString()}") + Log.d("PlaylistItemAdapter", "updateList: $newList") playlistItemList.clear() newList.forEach { playlistItemList.add(it) } - Log.d("PlaylistItemAdapter", "updateList: ${playlistItemList.toString()}") + Log.d("PlaylistItemAdapter", "updateList: $playlistItemList") notifyDataSetChanged() } diff --git a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt index 9281defb..8d3592ef 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/dataStore/DataStoreManager.kt @@ -1,6 +1,5 @@ package com.maxrave.simpmusic.data.dataStore -import android.content.Context import android.util.Log import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -10,7 +9,6 @@ import androidx.media3.common.Player import com.maxrave.simpmusic.common.SELECTED_LANGUAGE import com.maxrave.simpmusic.common.SPONSOR_BLOCK import com.maxrave.simpmusic.common.SUPPORTED_LANGUAGE -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -19,7 +17,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject import com.maxrave.simpmusic.common.QUALITY as COMMON_QUALITY -class DataStoreManager @Inject constructor(@ApplicationContext appContext: Context, private val settingsDataStore: DataStore) { +class DataStoreManager @Inject constructor(private val settingsDataStore: DataStore) { val location: Flow = settingsDataStore.data.map { preferences -> preferences[LOCATION] ?: "VN" @@ -234,6 +232,7 @@ class DataStoreManager @Inject constructor(@ApplicationContext appContext: Conte preferences[RECENT_SONG_POSITION_KEY] ?: "0" } suspend fun saveRecentSong (mediaId: String, position: Long) { + Log.w("saveRecentSong", "$mediaId $position") withContext(Dispatchers.IO) { settingsDataStore.edit { settings -> settings[RECENT_SONG_MEDIA_ID_KEY] = mediaId diff --git a/app/src/main/java/com/maxrave/simpmusic/data/model/home/HomeResponse.kt b/app/src/main/java/com/maxrave/simpmusic/data/model/home/HomeResponse.kt index 3a384b50..7af78da7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/model/home/HomeResponse.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/model/home/HomeResponse.kt @@ -9,5 +9,4 @@ data class HomeResponse( val homeItem: Resource>, val exploreMood: Resource, val exploreChart: Resource -) { -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/data/parser/ChartParser.kt b/app/src/main/java/com/maxrave/simpmusic/data/parser/ChartParser.kt index 4e1fd445..8401a853 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/parser/ChartParser.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/parser/ChartParser.kt @@ -50,7 +50,7 @@ fun parseSongChart(contents: List): ArrayLis var view = "" val artists: ArrayList = arrayListOf() val albums: ArrayList = arrayListOf() - if (runs != null) { + if (runs != null) { for (i in runs.indices) { if (i.rem(2) == 0) { if (i == runs.size -1) { diff --git a/app/src/main/java/com/maxrave/simpmusic/data/parser/HomeParser.kt b/app/src/main/java/com/maxrave/simpmusic/data/parser/HomeParser.kt index b2eeef8f..3c305dfa 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/parser/HomeParser.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/parser/HomeParser.kt @@ -181,7 +181,7 @@ fun parseMixedContent(data: List?): List if (ytItem != null) { listContent.add( Content( - album = Album( id = musicTwoRowItemRenderer.navigationEndpoint.browseEndpoint?.browseId ?: "", name = title ?: ""), + album = Album( id = musicTwoRowItemRenderer.navigationEndpoint.browseEndpoint?.browseId ?: "", name = title), artists = listOf(), description = null, isExplicit = false, @@ -214,7 +214,7 @@ fun parseMixedContent(data: List?): List if (ytItemAlbum != null) { listContent.add( Content( - album = Album( id = musicTwoRowItemRenderer.navigationEndpoint.browseEndpoint?.browseId ?: "", name = title ?: ""), + album = Album( id = musicTwoRowItemRenderer.navigationEndpoint.browseEndpoint?.browseId ?: "", name = title), artists = listOf(), description = null, isExplicit = false, @@ -264,8 +264,8 @@ fun parseMixedContent(data: List?): List playlistId = null, browseId = null, thumbnails = result1.musicResponsiveListItemRenderer!!.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.toListThumbnail() ?: listOf(), - title = ytItem.title ?: "", - videoId = ytItem.id ?: "", + title = ytItem.title, + videoId = ytItem.id, views = "", radio = null ) diff --git a/app/src/main/java/com/maxrave/simpmusic/data/parser/MoodsGenresParser.kt b/app/src/main/java/com/maxrave/simpmusic/data/parser/MoodsGenresParser.kt index a17032d4..c6f5baa5 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/parser/MoodsGenresParser.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/parser/MoodsGenresParser.kt @@ -77,14 +77,14 @@ fun parseMoodsMomentObject(data: BrowseResponse?): MoodsMomentObject? { val contentTitle = content.musicTwoRowItemRenderer?.title?.runs?.get(0)?.text val playlistBrowseId = content.musicTwoRowItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId - listContent.add( - Content( - playlistBrowseId = playlistBrowseId ?: "", - subtitle = subtitle, - thumbnails = thumbnails ?: listOf(), - title = contentTitle ?: "" - ) + listContent.add( + Content( + playlistBrowseId = playlistBrowseId ?: "", + subtitle = subtitle, + thumbnails = thumbnails ?: listOf(), + title = contentTitle ?: "" ) + ) } } } diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index 57cb4343..5df27a80 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -273,7 +273,7 @@ class MainRepository @Inject constructor(private val localDataSource: LocalDataS withContext(Dispatchers.IO) { localDataSource.insertFormat(format) } suspend fun getFormat(videoId: String): Flow = - flow { emit(localDataSource.getFormat(videoId)) }.flowOn(Dispatchers.IO) + flow { emit(localDataSource.getFormat(videoId)) }.flowOn(Dispatchers.Main) suspend fun recoverQueue(temp: List) { @@ -1166,7 +1166,7 @@ class MainRepository @Inject constructor(private val localDataSource: LocalDataS suspend fun removeYouTubePlaylistItem(youtubePlaylistId: String, videoId: String) = flow { runCatching { - getSetVideoId(videoId).collect() { setVideoId -> + getSetVideoId(videoId).collect { setVideoId -> if (setVideoId?.setVideoId != null) { YouTube.removeItemYouTubePlaylist(youtubePlaylistId, videoId, setVideoId.setVideoId).onSuccess { emit(it) diff --git a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt index b7ec83bf..f0bf35f4 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/LocalServiceModule.kt @@ -37,5 +37,7 @@ object LocalServiceModule { @Provides @Singleton - fun provideDatastoreManager(@ApplicationContext context: Context, settingsDataStore: DataStore): DataStoreManager = DataStoreManager(context, settingsDataStore) + fun provideDatastoreManager(settingsDataStore: DataStore): DataStoreManager = DataStoreManager( + settingsDataStore + ) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt b/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt index bbb6e5b3..e5b99358 100644 --- a/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt +++ b/app/src/main/java/com/maxrave/simpmusic/di/MusicServiceModule.kt @@ -1,42 +1,17 @@ package com.maxrave.simpmusic.di -import android.app.PendingIntent import android.content.Context -import android.content.Intent -import androidx.core.net.toUri -import androidx.media3.common.AudioAttributes -import androidx.media3.common.C import androidx.media3.common.util.UnstableApi import androidx.media3.database.DatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DataSpec -import androidx.media3.datasource.DefaultHttpDataSource -import androidx.media3.datasource.ResolvingDataSource -import androidx.media3.datasource.cache.CacheDataSource -import androidx.media3.datasource.cache.CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR import androidx.media3.datasource.cache.NoOpCacheEvictor import androidx.media3.datasource.cache.SimpleCache -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import androidx.media3.extractor.mkv.MatroskaExtractor -import androidx.media3.extractor.mp4.FragmentedMp4Extractor -import androidx.media3.session.MediaSession -import com.maxrave.simpmusic.common.QUALITY -import com.maxrave.simpmusic.data.dataStore.DataStoreManager -import com.maxrave.simpmusic.data.repository.MainRepository -import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.service.SimpleMediaSessionCallback -import com.maxrave.simpmusic.service.test.source.MusicSource -import com.maxrave.simpmusic.ui.MainActivity import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import javax.inject.Qualifier import javax.inject.Singleton @@ -60,7 +35,10 @@ object MusicServiceModule { @Singleton @Provides @PlayerCache - fun providePlayerCache(@ApplicationContext context: Context, databaseProvider: DatabaseProvider): SimpleCache = + fun providePlayerCache( + @ApplicationContext context: Context, + databaseProvider: DatabaseProvider + ): SimpleCache = SimpleCache( context.filesDir.resolve("exoplayer"), NoOpCacheEvictor(), @@ -70,156 +48,14 @@ object MusicServiceModule { @Singleton @Provides @DownloadCache - fun provideDownloadCache(@ApplicationContext context: Context, databaseProvider: DatabaseProvider): SimpleCache = + fun provideDownloadCache( + @ApplicationContext context: Context, + databaseProvider: DatabaseProvider + ): SimpleCache = SimpleCache(context.filesDir.resolve("download"), NoOpCacheEvictor(), databaseProvider) - @Provides - @Singleton - fun provideAudioAttributes(): AudioAttributes = - AudioAttributes.Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .setUsage(C.USAGE_MEDIA) - .build() - -// @Provides -// @Singleton -// @UnstableApi -// fun provideDataSource(@ApplicationContext context: Context): DefaultMediaSourceFactory = DefaultMediaSourceFactory(context).setDataSourceFactory( -// DefaultHttpDataSource.Factory() -// .setAllowCrossProtocolRedirects(true) -// .setUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") -// .setConnectTimeoutMs(5000) -// ) - - @Provides - @Singleton - @UnstableApi - fun provideCacheDataSource(@DownloadCache downloadCache: SimpleCache, @PlayerCache playerCache: SimpleCache): CacheDataSource.Factory { - return CacheDataSource.Factory() - .setCache(downloadCache) - .setUpstreamDataSourceFactory( - CacheDataSource.Factory() - .setCache(playerCache) - .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory() - .setAllowCrossProtocolRedirects(true) - .setUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") - .setConnectTimeoutMs(5000)) - ) - .setCacheWriteDataSinkFactory(null) - .setFlags(FLAG_IGNORE_CACHE_ON_ERROR) - } - -// @Provides -// @Singleton -// @UnstableApi -// fun provideMediaSourceFactory( -// @ApplicationContext context: Context, -// cacheDataSourceFactory: CacheDataSource.Factory -// ): DefaultMediaSourceFactory = -// DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory) - - @Provides - @Singleton - @UnstableApi - fun provideResolvingDataSourceFactory( - cacheDataSourceFactory: CacheDataSource.Factory, - @DownloadCache downloadCache: SimpleCache, @PlayerCache playerCache: SimpleCache, mainRepository: MainRepository, dataStoreManager: DataStoreManager - ): DataSource.Factory { - return ResolvingDataSource.Factory(cacheDataSourceFactory) { dataSpec -> - val mediaId = dataSpec.key ?: error("No media id") - val CHUNK_LENGTH = 512 * 1024L - if (downloadCache.isCached(mediaId, dataSpec.position, if (dataSpec.length >= 0) dataSpec.length else 1) || playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH)) { - return@Factory dataSpec - } - var dataSpecReturn: DataSpec? = null - runBlocking { - val itag = dataStoreManager.quality.first() - - mainRepository.getStream(mediaId, if (itag == QUALITY.items[0].toString()) QUALITY.itags[0] else QUALITY.itags[1]).collect { - if (it != null) { - dataSpecReturn = dataSpec.withUri(it.toUri()) - } - } - } - return@Factory dataSpecReturn!! - } - } - - @Provides - @Singleton - @UnstableApi - fun provideMediaSourceFactory( - dataSourceFactory: DataSource.Factory - ): DefaultMediaSourceFactory = - DefaultMediaSourceFactory( - dataSourceFactory - ) { - arrayOf(MatroskaExtractor(), FragmentedMp4Extractor()) - } - - @Provides - @Singleton - @UnstableApi - fun providePlayer( - @ApplicationContext context: Context, - audioAttributes: AudioAttributes, - mediaSourceFactory: DefaultMediaSourceFactory - ): ExoPlayer = - ExoPlayer.Builder(context) - .setAudioAttributes(audioAttributes, true) - .setWakeMode(C.WAKE_MODE_NETWORK) - .setHandleAudioBecomingNoisy(true) - .setSeekForwardIncrementMs(5000) - .setSeekBackIncrementMs(5000) - .setTrackSelector(DefaultTrackSelector(context)) - .setMediaSourceFactory(mediaSourceFactory) - .build() @Provides @Singleton fun provideMediaSessionCallback() : SimpleMediaSessionCallback = SimpleMediaSessionCallback() - - @Provides - @Singleton - fun provideMediaSession( - @ApplicationContext context: Context, - player: ExoPlayer, - callback: SimpleMediaSessionCallback - ): MediaSession = - MediaSession.Builder(context, player) - .setCallback(callback) - .setSessionActivity( - PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE - ) - ) - .build() - - - @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - @Provides - @Singleton - fun provideServiceHandler( - player: ExoPlayer, - dataStoreManager: DataStoreManager, - mainRepository: MainRepository, - @ApplicationContext context: Context, - mediaSession: MediaSession, - mediaSessionCallback: SimpleMediaSessionCallback - ): SimpleMediaServiceHandler = - SimpleMediaServiceHandler( - player = player, - dataStoreManager = dataStoreManager, - mainRepository = mainRepository, - context = context, - mediaSession = mediaSession, - mediaSessionCallback = mediaSessionCallback - ) - - @Provides - @Singleton - fun provideMusicSource( - simpleMediaServiceHandler: SimpleMediaServiceHandler, dataStoreManager: DataStoreManager, mainRepository: MainRepository - ): MusicSource = - MusicSource(simpleMediaServiceHandler, dataStoreManager, mainRepository) } \ No newline at end of file diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt index b787af3b..2701e129 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaService.kt @@ -1,13 +1,34 @@ package com.maxrave.simpmusic.service -import android.app.Application +import android.app.PendingIntent import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.os.Binder +import android.os.IBinder import android.util.Log +import androidx.core.net.toUri +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C import androidx.media3.common.Player +import androidx.media3.common.audio.SonicAudioProcessor import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.audio.AudioSink +import androidx.media3.exoplayer.audio.DefaultAudioSink +import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.extractor.ExtractorsFactory +import androidx.media3.extractor.mkv.MatroskaExtractor +import androidx.media3.extractor.mp4.FragmentedMp4Extractor import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaController import androidx.media3.session.MediaSession @@ -15,85 +36,258 @@ import androidx.media3.session.MediaSessionService import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors import com.maxrave.simpmusic.R -import com.maxrave.simpmusic.common.MEDIA_NOTIFICATION.NOTIFICATION_CHANNEL_ID -import com.maxrave.simpmusic.common.MEDIA_NOTIFICATION.NOTIFICATION_ID +import com.maxrave.simpmusic.common.MEDIA_NOTIFICATION +import com.maxrave.simpmusic.common.QUALITY +import com.maxrave.simpmusic.data.dataStore.DataStoreManager +import com.maxrave.simpmusic.data.repository.MainRepository +import com.maxrave.simpmusic.di.DownloadCache +import com.maxrave.simpmusic.di.PlayerCache +import com.maxrave.simpmusic.ui.MainActivity import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import javax.inject.Inject @AndroidEntryPoint class SimpleMediaService : MediaSessionService() { - @Inject lateinit var player: ExoPlayer - @Inject lateinit var mediaSession: MediaSession -// @Inject -// lateinit var notificationManager: SimpleMediaNotificationManager + @Inject + lateinit var dataStoreManager: DataStoreManager @Inject - lateinit var context: Application + lateinit var mainRepository: MainRepository @Inject - lateinit var simpleMediaServiceHandler: SimpleMediaServiceHandler + @PlayerCache + lateinit var playerCache: SimpleCache - @UnstableApi - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + @Inject + @DownloadCache + lateinit var downloadCache: SimpleCache + + @Inject + lateinit var simpleMediaSessionCallback: SimpleMediaSessionCallback + + private val binder = MusicBinder() + @UnstableApi + override fun onCreate() { + super.onCreate() + Log.w("Service", "Simple Media Service Created") setMediaNotificationProvider( - DefaultMediaNotificationProvider(this, { NOTIFICATION_ID }, NOTIFICATION_CHANNEL_ID, R.string.notification_channel_name) + DefaultMediaNotificationProvider(this, { MEDIA_NOTIFICATION.NOTIFICATION_ID }, MEDIA_NOTIFICATION.NOTIFICATION_CHANNEL_ID, R.string.notification_channel_name) .apply { setSmallIcon(R.drawable.logo_simpmusic_01_removebg_preview) } ) + player = ExoPlayer.Builder(this) + .setAudioAttributes(provideAudioAttributes(), true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .setHandleAudioBecomingNoisy(true) + .setSeekForwardIncrementMs(5000) + .setSeekBackIncrementMs(5000) + .setMediaSourceFactory(provideMediaSourceFactory()) + .setRenderersFactory(provideRendererFactory(this)) + .build() + + mediaSession = provideMediaSession( + context = this, + player = player, + callback = simpleMediaSessionCallback + + ) val sessionToken = SessionToken(this, ComponentName(this, SimpleMediaService::class.java)) val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() controllerFuture.addListener({ controllerFuture.get() }, MoreExecutors.directExecutor()) + } + + @UnstableApi + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return super.onStartCommand(intent, flags, startId) } + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = + mediaSession + @UnstableApi - override fun onDestroy() { - super.onDestroy() - simpleMediaServiceHandler.mayBeSaveRecentSong() - simpleMediaServiceHandler.mayBeSavePlaybackState() + override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) { + super.onUpdateNotification(session, startInForegroundRequired) + + } + + @UnstableApi + private fun release() { mediaSession.run { release() if (player.playbackState != Player.STATE_IDLE) { - player.seekTo(0) - player.playWhenReady = false - player.stop() + player.release() } } - simpleMediaServiceHandler.release() + } + + @UnstableApi + override fun onDestroy() { + super.onDestroy() + release() Log.d("SimpleMediaService", "onDestroy: ") } @UnstableApi override fun onTaskRemoved(rootIntent: Intent?) { - simpleMediaServiceHandler.mayBeSaveRecentSong() - simpleMediaServiceHandler.mayBeSavePlaybackState() - mediaSession.run { - release() - if (player.playbackState != Player.STATE_IDLE) { - player.seekTo(0) - player.playWhenReady = false - player.stop() - } - } - simpleMediaServiceHandler.release() + super.onTaskRemoved(rootIntent) + Log.d("SimpleMediaService", "onTaskRemoved: ") + release() stopSelf() + } - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = - mediaSession + inner class MusicBinder : Binder() { + val service: SimpleMediaService + get() = this@SimpleMediaService + } + + override fun onBind(intent: Intent?): IBinder = + super.onBind(intent) ?: binder + + fun provideAudioAttributes(): AudioAttributes = + AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build() + +// @Provides +// @Singleton +// @UnstableApi +// fun provideDataSource( context: Context): DefaultMediaSourceFactory = DefaultMediaSourceFactory(context).setDataSourceFactory( +// DefaultHttpDataSource.Factory() +// .setAllowCrossProtocolRedirects(true) +// .setUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") +// .setConnectTimeoutMs(5000) +// ) @UnstableApi - override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) { - super.onUpdateNotification(session, startInForegroundRequired) + fun provideCacheDataSource( + @DownloadCache downloadCache: SimpleCache, + @PlayerCache playerCache: SimpleCache + ): CacheDataSource.Factory { + return CacheDataSource.Factory() + .setCache(downloadCache) + .setUpstreamDataSourceFactory( + CacheDataSource.Factory() + .setCache(playerCache) + .setUpstreamDataSourceFactory( + DefaultHttpDataSource.Factory() + .setAllowCrossProtocolRedirects(true) + .setUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") + .setConnectTimeoutMs(5000) + ) + ) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) + } + +// @Provides +// @Singleton +// @UnstableApi +// fun provideMediaSourceFactory( +// context: Context, +// cacheDataSourceFactory: CacheDataSource.Factory +// ): DefaultMediaSourceFactory = +// DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory) + + + @UnstableApi + fun provideResolvingDataSourceFactory( + cacheDataSourceFactory: CacheDataSource.Factory, + @DownloadCache downloadCache: SimpleCache, + @PlayerCache playerCache: SimpleCache, + mainRepository: MainRepository, + dataStoreManager: DataStoreManager + ): DataSource.Factory { + return ResolvingDataSource.Factory(cacheDataSourceFactory) { dataSpec -> + val mediaId = dataSpec.key ?: error("No media id") + val CHUNK_LENGTH = 512 * 1024L + if (downloadCache.isCached( + mediaId, + dataSpec.position, + if (dataSpec.length >= 0) dataSpec.length else 1 + ) || playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH) + ) { + return@Factory dataSpec + } + var dataSpecReturn: DataSpec = dataSpec + runBlocking(Dispatchers.IO) { + val itag = dataStoreManager.quality.first() + mainRepository.getStream( + mediaId, + if (itag == QUALITY.items[0].toString()) QUALITY.itags[0] else QUALITY.itags[1] + ).cancellable().collect { + if (it != null) { + dataSpecReturn = dataSpec.withUri(it.toUri()) + } + } + } + return@Factory dataSpecReturn + } } + @UnstableApi + fun provideExtractorFactory(): ExtractorsFactory = ExtractorsFactory { + arrayOf(MatroskaExtractor(), FragmentedMp4Extractor()) + } + + @UnstableApi + fun provideMediaSourceFactory(): DefaultMediaSourceFactory = + DefaultMediaSourceFactory( + provideResolvingDataSourceFactory(provideCacheDataSource(downloadCache, playerCache), downloadCache, playerCache, mainRepository, dataStoreManager), + provideExtractorFactory() + ) + + @UnstableApi + fun provideRendererFactory(context: Context): DefaultRenderersFactory = + object : DefaultRenderersFactory(context) { + override fun buildAudioSink( + context: Context, + enableFloatOutput: Boolean, + enableAudioTrackPlaybackParams: Boolean, + enableOffload: Boolean + ): AudioSink { + return DefaultAudioSink.Builder(context) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setOffloadMode(if (enableOffload) DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED else DefaultAudioSink.OFFLOAD_MODE_DISABLED) + .setAudioProcessorChain( + DefaultAudioSink.DefaultAudioProcessorChain( + emptyArray(), + SilenceSkippingAudioProcessor(2_000_000, 20_000, 256), + SonicAudioProcessor() + ) + ) + .build() + } + } + + @UnstableApi + fun provideMediaSession( + context: Context, + player: ExoPlayer, + callback: SimpleMediaSessionCallback + ): MediaSession = + MediaSession.Builder(context, player) + .setCallback(callback) + .setSessionActivity( + PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + } diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt index 9e8e0969..161333c3 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt @@ -8,7 +8,10 @@ import android.media.audiofx.LoudnessEnhancer import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.core.net.toUri +import androidx.lifecycle.LifecycleCoroutineScope import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.Tracks @@ -21,27 +24,27 @@ import com.maxrave.simpmusic.R import com.maxrave.simpmusic.common.MEDIA_CUSTOM_COMMAND import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.model.browse.album.Track +import com.maxrave.simpmusic.data.model.searchResult.songs.Artist import com.maxrave.simpmusic.data.queue.Queue import com.maxrave.simpmusic.data.repository.MainRepository -import dagger.hilt.android.qualifiers.ApplicationContext +import com.maxrave.simpmusic.extension.connectArtists +import com.maxrave.simpmusic.extension.toListName import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first -import javax.inject.Inject -@OptIn(DelicateCoroutinesApi::class) @UnstableApi -class SimpleMediaServiceHandler @Inject constructor( - private val player: ExoPlayer, +class SimpleMediaServiceHandler constructor( + val player: ExoPlayer, private val mediaSession: MediaSession, mediaSessionCallback: SimpleMediaSessionCallback, private val dataStoreManager: DataStoreManager, private val mainRepository: MainRepository, - @ApplicationContext private val context: Context + private val coroutineScope: LifecycleCoroutineScope, + private val context: Context ) : Player.Listener { private var loudnessEnhancer: LoudnessEnhancer? = null @@ -53,22 +56,20 @@ class SimpleMediaServiceHandler @Inject constructor( private val _simpleMediaState = MutableStateFlow(SimpleMediaState.Initial) val simpleMediaState = _simpleMediaState.asStateFlow() - private val _changeTrack = MutableStateFlow(false) - val changeTrack = _changeTrack.asStateFlow() - - var nowPlaying = MutableStateFlow(player.currentMediaItem) + private var _nowPlaying = MutableStateFlow(player.currentMediaItem) + val nowPlaying = _nowPlaying.asSharedFlow() private val _nextTrackAvailable = MutableStateFlow(false) - val nextTrackAvailable = _nextTrackAvailable.asStateFlow() + val nextTrackAvailable = _nextTrackAvailable.asSharedFlow() private val _previousTrackAvailable = MutableStateFlow(false) - val previousTrackAvailable = _previousTrackAvailable.asStateFlow() + val previousTrackAvailable = _previousTrackAvailable.asSharedFlow() private val _shuffle = MutableStateFlow(false) - val shuffle = _shuffle.asStateFlow() + val shuffle = _shuffle.asSharedFlow() private val _repeat = MutableStateFlow(RepeatState.None) - val repeat = _repeat.asStateFlow() + val repeat = _repeat.asSharedFlow() private val _sleepMinutes = MutableStateFlow(0) val sleepMinutes = _sleepMinutes.asSharedFlow() @@ -79,17 +80,36 @@ class SimpleMediaServiceHandler @Inject constructor( private val _liked = MutableStateFlow(false) val liked = _liked.asSharedFlow() - private var updateNotificationJob: Job? = null - private var skipSilent = false private var normalizeVolume = false private var job: Job? = null + private var updateNotificationJob: Job? = null + + private var toggleLikeJob: Job? = null + + private var loadJob : Job? = null + + //Add MusicSource to this + var catalogMetadata: ArrayList = (arrayListOf()) + + var added: MutableStateFlow = MutableStateFlow(false) + + private var _stateFlow = MutableStateFlow(StateSource.STATE_CREATED) + val stateFlow = _stateFlow.asStateFlow() + private var _currentSongIndex = MutableStateFlow(0) + val currentSongIndex = _currentSongIndex.asSharedFlow() + init { player.addListener(this) job = Job() + sleepTimerJob = Job() + volumeNormalizationJob = Job() + updateNotificationJob = Job() + toggleLikeJob = Job() + loadJob = Job() skipSilent = runBlocking { dataStoreManager.skipSilent.first() == DataStoreManager.TRUE } normalizeVolume = runBlocking { dataStoreManager.normalizeVolume.first() == DataStoreManager.TRUE } if (runBlocking{ dataStoreManager.saveStateOfPlayback.first() } == DataStoreManager.TRUE ) { @@ -113,14 +133,14 @@ class SimpleMediaServiceHandler @Inject constructor( Player.REPEAT_MODE_OFF -> RepeatState.None else -> {RepeatState.None} } - nowPlaying.value = player.currentMediaItem + _nowPlaying.value = player.currentMediaItem mediaSessionCallback.apply { toggleLike = ::toggleLike } } private fun toggleLike() { - updateNotificationJob?.cancel() - updateNotificationJob = GlobalScope.launch(Dispatchers.Main) { + toggleLikeJob?.cancel() + toggleLikeJob = coroutineScope.launch { mainRepository.updateLikeStatus(player.currentMediaItem?.mediaId ?: "", if (!(_liked.value)) 1 else 0) } _liked.value = !(_liked.value) @@ -132,10 +152,11 @@ class SimpleMediaServiceHandler @Inject constructor( updateNotification() } //Set sleep timer + @OptIn(DelicateCoroutinesApi::class) fun sleepStart(minutes: Int) { _sleepDone.value = false sleepTimerJob?.cancel() - sleepTimerJob = GlobalScope.launch(Dispatchers.Main) { + sleepTimerJob = coroutineScope.launch(Dispatchers.Main) { _sleepMinutes.value = minutes var count = minutes while (count > 0) { @@ -165,6 +186,8 @@ class SimpleMediaServiceHandler @Inject constructor( fun removeMediaItem(position: Int) { player.removeMediaItem(position) + catalogMetadata.removeAt(position) + _currentSongIndex.value = currentIndex() } fun addMediaItem(mediaItem: MediaItem, playWhenReady: Boolean = true) { @@ -311,7 +334,7 @@ class SimpleMediaServiceHandler @Inject constructor( mayBeNormalizeVolume() Log.w("REASON", "onMediaItemTransition: $reason") Log.d("Media Item Transition", "Media Item: ${mediaItem?.mediaMetadata?.title}") - nowPlaying.value = mediaItem + _nowPlaying.value = mediaItem updateNextPreviousTrackAvailability() updateNotification() } @@ -355,7 +378,7 @@ class SimpleMediaServiceHandler @Inject constructor( override fun onIsPlayingChanged(isPlaying: Boolean) { _simpleMediaState.value = SimpleMediaState.Playing(isPlaying = isPlaying) if (isPlaying) { - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { startProgressUpdate() } } else { @@ -403,7 +426,7 @@ class SimpleMediaServiceHandler @Inject constructor( super.onIsLoadingChanged(isLoading) _simpleMediaState.value = SimpleMediaState.Loading(player.bufferedPercentage, player.duration) if (isLoading) { - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { startBufferedUpdate() } } else { @@ -411,8 +434,8 @@ class SimpleMediaServiceHandler @Inject constructor( } } - @OptIn(DelicateCoroutinesApi::class) private fun mayBeNormalizeVolume() { + runBlocking { normalizeVolume = dataStoreManager.normalizeVolume.first() == DataStoreManager.TRUE } if (!normalizeVolume) { loudnessEnhancer?.enabled = false loudnessEnhancer?.release() @@ -428,26 +451,29 @@ class SimpleMediaServiceHandler @Inject constructor( player.currentMediaItem?.mediaId?.let { songId -> volumeNormalizationJob?.cancel() - volumeNormalizationJob = GlobalScope.launch(Dispatchers.Main) { - mainRepository.getFormat(songId).cancellable().collectLatest { format -> - if (format != null){ + volumeNormalizationJob = coroutineScope.launch(Dispatchers.Main) { + mainRepository.getFormat(songId).cancellable().first().let { format -> + if (format != null) { try { loudnessEnhancer?.setTargetGain(-((format.loudnessDb ?: 0f) * 100).toInt() + 500) Log.w("Loudness", "mayBeNormalizeVolume: ${loudnessEnhancer?.targetGain}") loudnessEnhancer?.enabled = true - } catch (_: Exception) { } + } catch (e: Exception) { + e.printStackTrace() + } } } } } } private fun maybeSkipSilent() { + skipSilent = runBlocking { dataStoreManager.skipSilent.first() } == DataStoreManager.TRUE player.skipSilenceEnabled = skipSilent } fun mayBeSaveRecentSong() { runBlocking { if (dataStoreManager.saveRecentSongAndQueue.first() == DataStoreManager.TRUE) { - dataStoreManager.saveRecentSong(player.currentMediaItem?.mediaId ?: "", player.currentPosition) + dataStoreManager.saveRecentSong(player.currentMediaItem?.mediaId ?: "", player.contentPosition) Log.d("Check saved", player.currentMediaItem?.mediaMetadata?.title.toString()) val temp: ArrayList = ArrayList() temp.clear() @@ -482,8 +508,7 @@ class SimpleMediaServiceHandler @Inject constructor( fun seekTo(position: String) { player.seekTo(position.toLong()) - player.playWhenReady = false - Log.d("Check seek", "seekTo: ${player.duration}") + Log.d("Check seek", "seekTo: ${player.currentPosition}") } fun skipSegment(position: Long) { if (position in 0..player.duration) { @@ -510,19 +535,41 @@ class SimpleMediaServiceHandler @Inject constructor( ) } fun release() { - stopBufferedUpdate() - stopProgressUpdate() - sendCloseEqualizerIntent() + player.stop() + player.playWhenReady = false player.removeListener(this) - job?.cancel() - sleepTimerJob?.cancel() - volumeNormalizationJob?.cancel() - updateNotificationJob?.cancel() + sendCloseEqualizerIntent() + if (job?.isActive == true) { + job?.cancel() + job = null + } + if (sleepTimerJob?.isActive == true) { + sleepTimerJob?.cancel() + sleepTimerJob = null + } + if (volumeNormalizationJob?.isActive == true) { + volumeNormalizationJob?.cancel() + volumeNormalizationJob = null + } + if (toggleLikeJob?.isActive == true) { + toggleLikeJob?.cancel() + toggleLikeJob = null + } + if (updateNotificationJob?.isActive == true) { + updateNotificationJob?.cancel() + updateNotificationJob = null + } + if (loadJob?.isActive == true) { + loadJob?.cancel() + loadJob = null + } + Log.w("Service", "Check job: ${job?.isActive}") + Log.w("Service", "scope is active: ${coroutineScope?.isActive}") } private fun updateNotification() { updateNotificationJob?.cancel() - updateNotificationJob = GlobalScope.launch(Dispatchers.Main) { + updateNotificationJob = coroutineScope.launch { val liked = mainRepository.getSongById(player.currentMediaItem?.mediaId ?: "").first()?.liked if (liked != null) { _liked.value = liked @@ -555,32 +602,285 @@ class SimpleMediaServiceHandler @Inject constructor( ) } } + + fun getPlayerDuration(): Long { + return player.duration + } + + fun getProgress(): Long { + return player.currentPosition + } + + fun changeAddedState() { + added.value = false + } + + fun addFirstMetadata(it: Track) { + added.value = true + catalogMetadata.add(0, it) + Log.d("MusicSource", "addFirstMetadata: ${it.title}, ${catalogMetadata.size}") + } + + @UnstableApi + fun moveItemUp(position: Int) { + moveMediaItem(position, position - 1) + val temp = catalogMetadata[position] + catalogMetadata[position] = catalogMetadata[position - 1] + catalogMetadata[position - 1] = temp + _currentSongIndex.value = currentIndex() + } + + @UnstableApi + fun moveItemDown(position: Int) { + moveMediaItem(position, position + 1) + val temp = catalogMetadata[position] + catalogMetadata[position] = catalogMetadata[position + 1] + catalogMetadata[position + 1] = temp + _currentSongIndex.value = currentIndex() + } + + @UnstableApi + fun addFirstMediaItemToIndex(mediaItem: MediaItem?, index: Int) { + if (mediaItem != null){ + Log.d("MusicSource", "addFirstMediaItem: ${mediaItem.mediaId}") + moveMediaItem(0, index) + } + } + fun reset() { + _currentSongIndex.value = 0 + catalogMetadata.clear() + _stateFlow.value = StateSource.STATE_CREATED + } + + @UnstableApi + suspend fun load(downloaded: Int = 0, index: Int? = null) { + updateCatalog(downloaded).let { + _stateFlow.value = StateSource.STATE_INITIALIZED + if (index != null) { + when (index) { + -1 -> { + + } + else -> { + addFirstMediaItemToIndex(getMediaItemWithIndex(0), index) + Queue.getNowPlaying().let { song -> + if (song != null) { + catalogMetadata.removeAt(0) + catalogMetadata.add(index, song) + } + } + } + } + } + } + } + + @UnstableApi + suspend fun updateCatalog(downloaded: Int = 0): Boolean { + _stateFlow.value = StateSource.STATE_INITIALIZING + val tempQueue: ArrayList = arrayListOf() + tempQueue.addAll(Queue.getQueue()) + for (i in 0 until tempQueue.size){ + val track = tempQueue[i] + var thumbUrl = track.thumbnails?.last()?.url ?: "http://i.ytimg.com/vi/${track.videoId}/maxresdefault.jpg" + if (thumbUrl.contains("w120")){ + thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") + } + if (downloaded == 1) { + if (track.artists.isNullOrEmpty()) + { + mainRepository.getFormat(track.videoId).cancellable().first().let { format -> + if (format != null) { + val mediaItem = MediaItem.Builder() + .setMediaId(track.videoId) + .setUri(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .setTitle(track.title) + .setArtist(format.uploader) + .build() + ) + .build() + addMediaItemNotSet(mediaItem) + catalogMetadata.add(track.copy( + artists = listOf(Artist(format.uploaderId, format.uploader?: "" )) + )) + } + else { + val mediaItem = MediaItem.Builder() + .setMediaId(track.videoId) + .setUri(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .setTitle(track.title) + .setArtist("Various Artists") + .build() + ) + .build() + addMediaItemNotSet(mediaItem) + catalogMetadata.add(track.copy( + artists = listOf(Artist("", "Various Artists" )) + )) + } + } + } + else { + val mediaItem = MediaItem.Builder() + .setMediaId(track.videoId) + .setUri(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .setTitle(track.title) + .setArtist(track.artists.toListName().connectArtists()) + .build() + ) + .build() + addMediaItemNotSet(mediaItem) + catalogMetadata.add(track) + } + Log.d("MusicSource", "updateCatalog: ${track.title}, ${catalogMetadata.size}") + added.value = true + } + else { + val artistName: String = track.artists.toListName().connectArtists() + if (!catalogMetadata.contains(track)) { + if (track.artists.isNullOrEmpty()) + { + mainRepository.getFormat(track.videoId).cancellable().first().let { format -> + if (format != null) { + catalogMetadata.add( + track.copy( + artists = listOf( + Artist( + format.uploaderId, + format.uploader ?: "" + ) + ) + ) + ) + addMediaItemNotSet( + MediaItem.Builder().setUri(track.videoId) + .setMediaId(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(track.title) + .setArtist(format.uploader) + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .build() + ) + .build() + ) + } + else { + val mediaItem = MediaItem.Builder() + .setMediaId(track.videoId) + .setUri(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .setTitle(track.title) + .setArtist("Various Artists") + .build() + ) + .build() + addMediaItemNotSet(mediaItem) + catalogMetadata.add(track.copy( + artists = listOf(Artist("", "Various Artists" )) + )) + } + } + } + else { + addMediaItemNotSet( + MediaItem.Builder().setUri(track.videoId) + .setMediaId(track.videoId) + .setCustomCacheKey(track.videoId) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(track.title) + .setArtist(artistName) + .setArtworkUri(thumbUrl.toUri()) + .setAlbumTitle(track.album?.name) + .build() + ) + .build() + ) + catalogMetadata.add(track) + } + Log.d( + "MusicSource", + "updateCatalog: ${track.title}, ${catalogMetadata.size}" + ) + added.value = true + Log.d("MusicSource", "updateCatalog: ${track.title}") + } + } + } + return true + } + fun addQueueToPlayer() { + loadJob?.cancel() + loadJob = coroutineScope.launch { + load() + } + } + + fun loadPlaylistOrAlbum(index: Int? = null) { + loadJob?.cancel() + loadJob = coroutineScope.launch { + load(index = index) + } + } + + fun setCurrentSongIndex(index: Int) { + _currentSongIndex.value = index + } + } sealed class RepeatState { - object None : RepeatState() - object All : RepeatState() - object One : RepeatState() + data object None : RepeatState() + data object All : RepeatState() + data object One : RepeatState() } sealed class PlayerEvent { - object PlayPause : PlayerEvent() - object Backward : PlayerEvent() - object Forward : PlayerEvent() - object Stop : PlayerEvent() - object Next : PlayerEvent() - object Previous : PlayerEvent() - object Shuffle : PlayerEvent() - object Repeat : PlayerEvent() + data object PlayPause : PlayerEvent() + data object Backward : PlayerEvent() + data object Forward : PlayerEvent() + data object Stop : PlayerEvent() + data object Next : PlayerEvent() + data object Previous : PlayerEvent() + data object Shuffle : PlayerEvent() + data object Repeat : PlayerEvent() data class UpdateProgress(val newProgress: Float) : PlayerEvent() } sealed class SimpleMediaState { - object Initial : SimpleMediaState() - object Ended : SimpleMediaState() + data object Initial : SimpleMediaState() + data object Ended : SimpleMediaState() data class Ready(val duration: Long) : SimpleMediaState() data class Loading(val bufferedPercentage: Int, val duration: Long): SimpleMediaState() data class Progress(val progress: Long) : SimpleMediaState() data class Buffering(val position: Long) : SimpleMediaState() data class Playing(val isPlaying: Boolean) : SimpleMediaState() } +enum class StateSource { + STATE_CREATED, + STATE_INITIALIZING, + STATE_INITIALIZED, + STATE_ERROR +} diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/download/DownloadUtils.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/download/DownloadUtils.kt index f0292c16..a334319d 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/test/download/DownloadUtils.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/download/DownloadUtils.kt @@ -21,6 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking @@ -61,7 +62,7 @@ class DownloadUtils @Inject constructor( runBlocking(Dispatchers.IO) { Log.w("DownloadUtils", "Not cached: $mediaId") var extract: DataSpec? = null - mainRepository.getStream(mediaId, 251).collect { values -> + mainRepository.getStream(mediaId, 251).cancellable().collect { values -> if (values != null){ extract = dataSpec.withUri((values).toUri()) } diff --git a/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt b/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt index 0c8cba90..71bee4b7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/test/source/MusicSource.kt @@ -1,272 +1,276 @@ -package com.maxrave.simpmusic.service.test.source - -import android.util.Log -import androidx.core.net.toUri -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.util.UnstableApi -import com.maxrave.simpmusic.data.dataStore.DataStoreManager -import com.maxrave.simpmusic.data.model.browse.album.Track -import com.maxrave.simpmusic.data.model.searchResult.songs.Artist -import com.maxrave.simpmusic.data.queue.Queue -import com.maxrave.simpmusic.data.repository.MainRepository -import com.maxrave.simpmusic.extension.connectArtists -import com.maxrave.simpmusic.extension.toListName -import com.maxrave.simpmusic.service.SimpleMediaServiceHandler -import com.maxrave.simpmusic.service.test.source.StateSource.STATE_CREATED -import com.maxrave.simpmusic.service.test.source.StateSource.STATE_ERROR -import com.maxrave.simpmusic.service.test.source.StateSource.STATE_INITIALIZED -import com.maxrave.simpmusic.service.test.source.StateSource.STATE_INITIALIZING -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject - -class MusicSource @Inject constructor(private val simpleMediaServiceHandler: SimpleMediaServiceHandler, private val dataStoreManager: DataStoreManager, private val mainRepository: MainRepository) { - - var catalogMetadata: ArrayList = (arrayListOf()) - - var added: MutableStateFlow = MutableStateFlow(false) - - private val onReadyListeners = mutableListOf<(Boolean) -> Unit>() - - private var _stateFlow = MutableStateFlow(STATE_CREATED) - val stateFlow = _stateFlow.asStateFlow() - private var _currentSongIndex = MutableStateFlow(0) - val currentSongIndex = _currentSongIndex.asStateFlow() - - var state: StateSource = STATE_CREATED - set(value) { - if(value == STATE_INITIALIZED || value == STATE_ERROR) { - synchronized(onReadyListeners) { - field = value - _stateFlow.value = value - onReadyListeners.forEach { listener -> - listener(state == STATE_INITIALIZED) - } - } - } else { - _stateFlow.value = value - field = value - } - } - - fun addFirstMediaItem(mediaItem: MediaItem?) { - if (mediaItem != null){ - Log.d("MusicSource", "addFirstMediaItem: ${mediaItem.mediaId}") - } - } - @UnstableApi - fun addFirstMediaItemToIndex(mediaItem: MediaItem?, index: Int) { - if (mediaItem != null){ - Log.d("MusicSource", "addFirstMediaItem: ${mediaItem.mediaId}") - simpleMediaServiceHandler.moveMediaItem(0, index) - } - } - fun reset() { - _currentSongIndex.value = 0 - catalogMetadata.clear() - state = STATE_CREATED - } - fun setCurrentSongIndex(index: Int) { - _currentSongIndex.value = index - } - - @UnstableApi - suspend fun load(downloaded: Int = 0) { - updateCatalog(downloaded).let { - state = STATE_INITIALIZED - } - } - - @UnstableApi - private suspend fun updateCatalog(downloaded: Int = 0): Boolean { - state = STATE_INITIALIZING - val tempQueue: ArrayList = arrayListOf() - tempQueue.addAll(Queue.getQueue()) - for (i in 0 until tempQueue.size){ - val track = tempQueue[i] - var thumbUrl = track.thumbnails?.last()?.url ?: "http://i.ytimg.com/vi/${track.videoId}/maxresdefault.jpg" - if (thumbUrl.contains("w120")){ - thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") - } - if (downloaded == 1) { - if (track.artists.isNullOrEmpty()) - { - mainRepository.getFormat(track.videoId).collect { format -> - if (format != null) { - val mediaItem = MediaItem.Builder() - .setMediaId(track.videoId) - .setUri(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .setTitle(track.title) - .setArtist(format.uploader) - .build() - ) - .build() - simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) - catalogMetadata.add(track.copy( - artists = listOf(Artist(format.uploaderId, format.uploader?: "" )) - )) - } - else { - val mediaItem = MediaItem.Builder() - .setMediaId(track.videoId) - .setUri(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .setTitle(track.title) - .setArtist("Various Artists") - .build() - ) - .build() - simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) - catalogMetadata.add(track.copy( - artists = listOf(Artist("", "Various Artists" )) - )) - } - } - } - else { - val mediaItem = MediaItem.Builder() - .setMediaId(track.videoId) - .setUri(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .setTitle(track.title) - .setArtist(track.artists.toListName().connectArtists()) - .build() - ) - .build() - simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) - catalogMetadata.add(track) - } - Log.d("MusicSource", "updateCatalog: ${track.title}, ${catalogMetadata.size}") - added.value = true - } - else { - val artistName: String = track.artists.toListName().connectArtists() - if (!catalogMetadata.contains(track)) { - if (track.artists.isNullOrEmpty()) - { - mainRepository.getFormat(track.videoId).collect { format -> - if (format != null) { - catalogMetadata.add(track.copy( - artists = listOf(Artist(format.uploaderId, format.uploader?: "" )) - )) - simpleMediaServiceHandler.addMediaItemNotSet( - MediaItem.Builder().setUri(track.videoId) - .setMediaId(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(track.title) - .setArtist(format.uploader) - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .build() - ) - .build() - ) - } - else { - val mediaItem = MediaItem.Builder() - .setMediaId(track.videoId) - .setUri(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .setTitle(track.title) - .setArtist("Various Artists") - .build() - ) - .build() - simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) - catalogMetadata.add(track.copy( - artists = listOf(Artist("", "Various Artists" )) - )) - } - } - } - else { - simpleMediaServiceHandler.addMediaItemNotSet( - MediaItem.Builder().setUri(track.videoId) - .setMediaId(track.videoId) - .setCustomCacheKey(track.videoId) - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(track.title) - .setArtist(artistName) - .setArtworkUri(thumbUrl.toUri()) - .setAlbumTitle(track.album?.name) - .build() - ) - .build() - ) - catalogMetadata.add(track) - } - Log.d( - "MusicSource", - "updateCatalog: ${track.title}, ${catalogMetadata.size}" - ) - added.value = true - Log.d("MusicSource", "updateCatalog: ${track.title}") - } - } - } - return true - } - fun changeAddedState() { - added.value = false - } - - fun addFirstMetadata(it: Track) { - added.value = true - catalogMetadata.add(0, it) - Log.d("MusicSource", "addFirstMetadata: ${it.title}, ${catalogMetadata.size}") - } - - @UnstableApi - fun moveItemUp(position: Int) { - simpleMediaServiceHandler.moveMediaItem(position, position - 1) - val temp = catalogMetadata[position] - catalogMetadata[position] = catalogMetadata[position - 1] - catalogMetadata[position - 1] = temp - _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() - } - - @UnstableApi - fun moveItemDown(position: Int) { - simpleMediaServiceHandler.moveMediaItem(position, position + 1) - val temp = catalogMetadata[position] - catalogMetadata[position] = catalogMetadata[position + 1] - catalogMetadata[position + 1] = temp - _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() - } - - @UnstableApi - fun removeMediaItem(position: Int) { - simpleMediaServiceHandler.removeMediaItem(position) - catalogMetadata.removeAt(position) - _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() - } -} - -enum class StateSource { - STATE_CREATED, - STATE_INITIALIZING, - STATE_INITIALIZED, - STATE_ERROR -} - +//package com.maxrave.simpmusic.service.test.source +// +//import android.util.Log +//import androidx.core.net.toUri +//import androidx.media3.common.MediaItem +//import androidx.media3.common.MediaMetadata +//import androidx.media3.common.util.UnstableApi +//import com.maxrave.simpmusic.data.model.browse.album.Track +//import com.maxrave.simpmusic.data.model.searchResult.songs.Artist +//import com.maxrave.simpmusic.data.queue.Queue +//import com.maxrave.simpmusic.data.repository.MainRepository +//import com.maxrave.simpmusic.extension.connectArtists +//import com.maxrave.simpmusic.extension.toListName +//import com.maxrave.simpmusic.service.SimpleMediaServiceHandler +//import com.maxrave.simpmusic.service.test.source.StateSource.STATE_CREATED +//import com.maxrave.simpmusic.service.test.source.StateSource.STATE_ERROR +//import com.maxrave.simpmusic.service.test.source.StateSource.STATE_INITIALIZED +//import com.maxrave.simpmusic.service.test.source.StateSource.STATE_INITIALIZING +//import kotlinx.coroutines.flow.MutableStateFlow +//import kotlinx.coroutines.flow.asStateFlow +//import javax.inject.Inject +//import javax.inject.Singleton +// +//@Singleton +//class MusicSource @Inject constructor( +// private val simpleMediaServiceHandler: SimpleMediaServiceHandler, +// private val mainRepository: MainRepository +//) { +// +// var catalogMetadata: ArrayList = (arrayListOf()) +// +// var added: MutableStateFlow = MutableStateFlow(false) +// +// private val onReadyListeners = mutableListOf<(Boolean) -> Unit>() +// +// private var _stateFlow = MutableStateFlow(STATE_CREATED) +// val stateFlow = _stateFlow.asStateFlow() +// private var _currentSongIndex = MutableStateFlow(0) +// val currentSongIndex = _currentSongIndex.asStateFlow() +// +// var state: StateSource = STATE_CREATED +// set(value) { +// if(value == STATE_INITIALIZED || value == STATE_ERROR) { +// synchronized(onReadyListeners) { +// field = value +// _stateFlow.value = value +// onReadyListeners.forEach { listener -> +// listener(state == STATE_INITIALIZED) +// } +// } +// } else { +// _stateFlow.value = value +// field = value +// } +// } +// +// fun addFirstMediaItem(mediaItem: MediaItem?) { +// if (mediaItem != null){ +// Log.d("MusicSource", "addFirstMediaItem: ${mediaItem.mediaId}") +// } +// } +// @UnstableApi +// fun addFirstMediaItemToIndex(mediaItem: MediaItem?, index: Int) { +// if (mediaItem != null){ +// Log.d("MusicSource", "addFirstMediaItem: ${mediaItem.mediaId}") +// simpleMediaServiceHandler.moveMediaItem(0, index) +// } +// } +// fun reset() { +// _currentSongIndex.value = 0 +// catalogMetadata.clear() +// state = STATE_CREATED +// } +// fun setCurrentSongIndex(index: Int) { +// _currentSongIndex.value = index +// } +// +// @UnstableApi +// suspend fun load(downloaded: Int = 0) { +// updateCatalog(downloaded).let { +// state = STATE_INITIALIZED +// } +// } +// +// @UnstableApi +// private suspend fun updateCatalog(downloaded: Int = 0): Boolean { +// state = STATE_INITIALIZING +// val tempQueue: ArrayList = arrayListOf() +// tempQueue.addAll(Queue.getQueue()) +// for (i in 0 until tempQueue.size){ +// val track = tempQueue[i] +// var thumbUrl = track.thumbnails?.last()?.url ?: "http://i.ytimg.com/vi/${track.videoId}/maxresdefault.jpg" +// if (thumbUrl.contains("w120")){ +// thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") +// } +// if (downloaded == 1) { +// if (track.artists.isNullOrEmpty()) +// { +// mainRepository.getFormat(track.videoId).collect { format -> +// if (format != null) { +// val mediaItem = MediaItem.Builder() +// .setMediaId(track.videoId) +// .setUri(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .setTitle(track.title) +// .setArtist(format.uploader) +// .build() +// ) +// .build() +// simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) +// catalogMetadata.add(track.copy( +// artists = listOf(Artist(format.uploaderId, format.uploader?: "" )) +// )) +// } +// else { +// val mediaItem = MediaItem.Builder() +// .setMediaId(track.videoId) +// .setUri(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .setTitle(track.title) +// .setArtist("Various Artists") +// .build() +// ) +// .build() +// simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) +// catalogMetadata.add(track.copy( +// artists = listOf(Artist("", "Various Artists" )) +// )) +// } +// } +// } +// else { +// val mediaItem = MediaItem.Builder() +// .setMediaId(track.videoId) +// .setUri(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .setTitle(track.title) +// .setArtist(track.artists.toListName().connectArtists()) +// .build() +// ) +// .build() +// simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) +// catalogMetadata.add(track) +// } +// Log.d("MusicSource", "updateCatalog: ${track.title}, ${catalogMetadata.size}") +// added.value = true +// } +// else { +// val artistName: String = track.artists.toListName().connectArtists() +// if (!catalogMetadata.contains(track)) { +// if (track.artists.isNullOrEmpty()) +// { +// mainRepository.getFormat(track.videoId).collect { format -> +// if (format != null) { +// catalogMetadata.add(track.copy( +// artists = listOf(Artist(format.uploaderId, format.uploader?: "" )) +// )) +// simpleMediaServiceHandler.addMediaItemNotSet( +// MediaItem.Builder().setUri(track.videoId) +// .setMediaId(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setTitle(track.title) +// .setArtist(format.uploader) +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .build() +// ) +// .build() +// ) +// } +// else { +// val mediaItem = MediaItem.Builder() +// .setMediaId(track.videoId) +// .setUri(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .setTitle(track.title) +// .setArtist("Various Artists") +// .build() +// ) +// .build() +// simpleMediaServiceHandler.addMediaItemNotSet(mediaItem) +// catalogMetadata.add(track.copy( +// artists = listOf(Artist("", "Various Artists" )) +// )) +// } +// } +// } +// else { +// simpleMediaServiceHandler.addMediaItemNotSet( +// MediaItem.Builder().setUri(track.videoId) +// .setMediaId(track.videoId) +// .setCustomCacheKey(track.videoId) +// .setMediaMetadata( +// MediaMetadata.Builder() +// .setTitle(track.title) +// .setArtist(artistName) +// .setArtworkUri(thumbUrl.toUri()) +// .setAlbumTitle(track.album?.name) +// .build() +// ) +// .build() +// ) +// catalogMetadata.add(track) +// } +// Log.d( +// "MusicSource", +// "updateCatalog: ${track.title}, ${catalogMetadata.size}" +// ) +// added.value = true +// Log.d("MusicSource", "updateCatalog: ${track.title}") +// } +// } +// } +// return true +// } +// fun changeAddedState() { +// added.value = false +// } +// +// fun addFirstMetadata(it: Track) { +// added.value = true +// catalogMetadata.add(0, it) +// Log.d("MusicSource", "addFirstMetadata: ${it.title}, ${catalogMetadata.size}") +// } +// +// @UnstableApi +// fun moveItemUp(position: Int) { +// simpleMediaServiceHandler.moveMediaItem(position, position - 1) +// val temp = catalogMetadata[position] +// catalogMetadata[position] = catalogMetadata[position - 1] +// catalogMetadata[position - 1] = temp +// _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() +// } +// +// @UnstableApi +// fun moveItemDown(position: Int) { +// simpleMediaServiceHandler.moveMediaItem(position, position + 1) +// val temp = catalogMetadata[position] +// catalogMetadata[position] = catalogMetadata[position + 1] +// catalogMetadata[position + 1] = temp +// _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() +// } +// +// @UnstableApi +// fun removeMediaItem(position: Int) { +// simpleMediaServiceHandler.removeMediaItem(position) +// catalogMetadata.removeAt(position) +// _currentSongIndex.value = simpleMediaServiceHandler.currentIndex() +// } +//} +// +//enum class StateSource { +// STATE_CREATED, +// STATE_INITIALIZING, +// STATE_INITIALIZED, +// STATE_ERROR +//} +// diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt index 877c716b..cf445e22 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/MainActivity.kt @@ -1,13 +1,16 @@ package com.maxrave.simpmusic.ui import android.Manifest +import android.content.ComponentName import android.content.Intent +import android.content.ServiceConnection import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.IBinder import android.util.Log import android.view.View import android.view.animation.AnimationUtils @@ -50,10 +53,11 @@ import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.RESTORE_LAST_PLAYED_TRACK_AND_QUEUE_DONE import com.maxrave.simpmusic.data.model.browse.album.Track import com.maxrave.simpmusic.data.queue.Queue +import com.maxrave.simpmusic.data.repository.MainRepository import com.maxrave.simpmusic.databinding.ActivityMainBinding import com.maxrave.simpmusic.extension.isMyServiceRunning import com.maxrave.simpmusic.service.SimpleMediaService -import com.maxrave.simpmusic.service.test.source.MusicSource +import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.viewModel.SharedViewModel import com.maxrave.simpmusic.viewModel.UIEvent import dagger.hilt.android.AndroidEntryPoint @@ -71,14 +75,152 @@ import javax.inject.Inject @UnstableApi @AndroidEntryPoint class MainActivity : AppCompatActivity() { - @Inject - lateinit var musicSource: MusicSource private lateinit var binding: ActivityMainBinding val viewModel by viewModels() private var action: String? = null private var data: Uri? = null + @Inject + lateinit var dataStoreManager: DataStoreManager + + @Inject + lateinit var mainRepository: MainRepository + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service is SimpleMediaService.MusicBinder) { + Log.w("MainActivity", "onServiceConnected: ") + + viewModel.simpleMediaServiceHandler = SimpleMediaServiceHandler( + player = service.service.player, + mediaSession = service.service.mediaSession, + mediaSessionCallback = service.service.simpleMediaSessionCallback, + dataStoreManager = dataStoreManager, + mainRepository = mainRepository, + context = service.service, + coroutineScope = lifecycleScope + ) + + viewModel.init() + mayBeRestoreLastPlayedTrackAndQueue() + runCollect() + Log.w("TEST", viewModel.simpleMediaServiceHandler?.player.toString()) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + Log.w("MainActivity", "onServiceDisconnected: ") + viewModel.simpleMediaServiceHandler = null + } + } + + private fun runCollect() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + val job5 = launch { + viewModel.simpleMediaServiceHandler?.nowPlaying?.collect { + if (it != null){ + Log.w("Test service", viewModel.simpleMediaServiceHandler?.getCurrentMediaItem()?.mediaMetadata?.title.toString()) + binding.songTitle.text = it.mediaMetadata.title + binding.songTitle.isSelected = true + binding.songArtist.text = it.mediaMetadata.artist + binding.songArtist.isSelected = true + val request = ImageRequest.Builder(this@MainActivity) + .data(it.mediaMetadata.artworkUri) + .target( + onSuccess = { result -> + binding.ivArt.setImageDrawable(result) + }, + ) + .transformations(object : Transformation { + override val cacheKey: String + get() = it.mediaMetadata.artworkUri.toString() + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val p = Palette.from(input).generate() + val defaultColor = 0x000000 + var startColor = p.getDarkVibrantColor(defaultColor) + Log.d("Check Start Color", "transform: $startColor") + if (startColor == defaultColor){ + startColor = p.getDarkMutedColor(defaultColor) + if (startColor == defaultColor){ + startColor = p.getVibrantColor(defaultColor) + if (startColor == defaultColor){ + startColor = p.getMutedColor(defaultColor) + if (startColor == defaultColor){ + startColor = p.getLightVibrantColor(defaultColor) + if (startColor == defaultColor){ + startColor = p.getLightMutedColor(defaultColor) + } + } + } + } + Log.d("Check Start Color", "transform: $startColor") + } + val endColor = 0x1b1a1f + val gd = GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf(startColor, endColor) + ) + gd.cornerRadius = 0f + gd.gradientType = GradientDrawable.LINEAR_GRADIENT + gd.gradientRadius = 0.5f + gd.alpha = 150 + val bg = ColorUtils.setAlphaComponent(startColor, 255) + binding.card.setCardBackgroundColor(bg) + binding.cardBottom.setCardBackgroundColor(bg) + return input + } + + }) + .build() + ImageLoader(this@MainActivity).execute(request) + } + } + } + val job2 = launch { + viewModel.progress.collect{ + binding.progressBar.progress = (it * 100).toInt() + } + } + + val job6 = launch { + viewModel.simpleMediaServiceHandler?.liked?.collect{ liked -> + binding.cbFavorite.isChecked = liked + } + } + val job3 = launch { + viewModel.isPlaying.collect { + if (it){ + binding.btPlayPause.setImageResource(R.drawable.baseline_pause_24) + }else{ + binding.btPlayPause.setImageResource(R.drawable.baseline_play_arrow_24) + } + } + } + val job4 = launch { + viewModel.simpleMediaServiceHandler?.sleepDone?.collect { done -> + if (done) { + MaterialAlertDialogBuilder(this@MainActivity) + .setTitle(getString(R.string.sleep_timer_off)) + .setMessage(getString(R.string.good_night)) + .setPositiveButton(getString(R.string.yes)) { d, _ -> + d.dismiss() + } + .show() + } + } + } + job2.join() + job3.join() + job5.join() + job6.join() + job4.join() + } + } + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) action = intent?.action @@ -101,6 +243,11 @@ class MainActivity : AppCompatActivity() { Log.d("MainActivity", "onResume: ") } + override fun onStart() { + super.onStart() + startMusicService() + } + @UnstableApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -340,94 +487,7 @@ class MainActivity : AppCompatActivity() { } } } - val job5 = launch { - viewModel.nowPLaying.collect { - if (it != null){ - if (viewModel.isServiceRunning.value == false){ - startMusicService() - } - binding.songTitle.text = it.mediaMetadata.title - binding.songTitle.isSelected = true - binding.songArtist.text = it.mediaMetadata.artist - binding.songArtist.isSelected = true - val request = ImageRequest.Builder(this@MainActivity) - .data(it.mediaMetadata.artworkUri) - .target( - onSuccess = { result -> - binding.ivArt.setImageDrawable(result) - }, - ) - .transformations(object : Transformation { - override val cacheKey: String - get() = it.mediaMetadata.artworkUri.toString() - - override suspend fun transform(input: Bitmap, size: Size): Bitmap { - val p = Palette.from(input).generate() - val defaultColor = 0x000000 - var startColor = p.getDarkVibrantColor(defaultColor) - Log.d("Check Start Color", "transform: $startColor") - if (startColor == defaultColor){ - startColor = p.getDarkMutedColor(defaultColor) - if (startColor == defaultColor){ - startColor = p.getVibrantColor(defaultColor) - if (startColor == defaultColor){ - startColor = p.getMutedColor(defaultColor) - if (startColor == defaultColor){ - startColor = p.getLightVibrantColor(defaultColor) - if (startColor == defaultColor){ - startColor = p.getLightMutedColor(defaultColor) - } - } - } - } - Log.d("Check Start Color", "transform: $startColor") - } - val endColor = 0x1b1a1f - val gd = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, - intArrayOf(startColor, endColor) - ) - gd.cornerRadius = 0f - gd.gradientType = GradientDrawable.LINEAR_GRADIENT - gd.gradientRadius = 0.5f - gd.alpha = 150 - val bg = ColorUtils.setAlphaComponent(startColor, 255) - binding.card.setCardBackgroundColor(bg) - binding.cardBottom.setCardBackgroundColor(bg) - return input - } - - }) - .build() - ImageLoader(this@MainActivity).execute(request) - } - } - } - val job2 = launch { - viewModel.progress.collect{ - binding.progressBar.progress = (it * 100).toInt() - } - } - - val job6 = launch { - viewModel.liked.collect{ liked -> - binding.cbFavorite.isChecked = liked - } - } - val job3 = launch { - viewModel.isPlaying.observe(this@MainActivity){ - if (it){ - binding.btPlayPause.setImageResource(R.drawable.baseline_pause_24) - }else{ - binding.btPlayPause.setImageResource(R.drawable.baseline_play_arrow_24) - } - } - } job1.join() - job2.join() - job3.join() - job5.join() - job6.join() } } lifecycleScope.launch { @@ -453,21 +513,8 @@ class MainActivity : AppCompatActivity() { } } - val job2 = launch { - viewModel.sleepTimerDone.collect { done -> - if (done) { - MaterialAlertDialogBuilder(this@MainActivity) - .setTitle(getString(R.string.sleep_timer_off)) - .setMessage(getString(R.string.good_night)) - .setPositiveButton(getString(R.string.yes)) { d, _ -> - d.dismiss() - } - .show() - } - } - } + job1.join() - job2.join() } binding.card.animation = AnimationUtils.loadAnimation(this, R.anim.bottom_to_top) binding.cbFavorite.setOnCheckedChangeListener{ _, isChecked -> @@ -486,7 +533,6 @@ class MainActivity : AppCompatActivity() { } } } - mayBeRestoreLastPlayedTrackAndQueue() } private fun mayBeRestoreLastPlayedTrackAndQueue() { @@ -496,7 +542,7 @@ class MainActivity : AppCompatActivity() { val queue = viewModel.saveLastPlayedSong.switchMap { saved: Boolean -> if (saved) { viewModel.from.postValue(viewModel.from_backup) - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.getSavedSongAndQueue() return@switchMap viewModel.savedQueue } else { @@ -534,12 +580,17 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() stopService() + runBlocking { + delay(1000) + Log.e("service running ?", this@MainActivity.isMyServiceRunning(SimpleMediaService::class.java).toString()) + } + Log.w("MainActivity", "onDestroy: ") } private fun startMusicService() { if (viewModel.isServiceRunning.value == false) { if (!isMyServiceRunning(SimpleMediaService::class.java)) { val intent = Intent(this, SimpleMediaService::class.java) - startForegroundService(intent) + bindService(intent, serviceConnection, BIND_AUTO_CREATE) viewModel.isServiceRunning.postValue(true) Log.d("Service", "Service started") } @@ -547,10 +598,13 @@ class MainActivity : AppCompatActivity() { } private fun stopService(){ if (viewModel.isServiceRunning.value == true){ - stopService(Intent(this, SimpleMediaService::class.java)) + viewModel.simpleMediaServiceHandler?.mayBeSaveRecentSong() + viewModel.simpleMediaServiceHandler?.mayBeSavePlaybackState() + viewModel.simpleMediaServiceHandler?.release() + unbindService(serviceConnection) Log.d("Service", "Service stopped") if (this.isMyServiceRunning(DownloadService:: class.java)){ - this.stopService(Intent(this, DownloadService::class.java)) + stopService(Intent(this, DownloadService::class.java)) viewModel.changeAllDownloadingToError() Log.d("Service", "DownloadService stopped") } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt index 5a13172c..65779344 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/SearchFragment.kt @@ -56,7 +56,6 @@ import com.maxrave.simpmusic.extension.setEnabledAll import com.maxrave.simpmusic.extension.toListName import com.maxrave.simpmusic.extension.toTrack import com.maxrave.simpmusic.service.test.download.MusicDownloadService -import com.maxrave.simpmusic.service.test.source.MusicSource import com.maxrave.simpmusic.utils.Resource import com.maxrave.simpmusic.viewModel.SearchViewModel import dagger.hilt.android.AndroidEntryPoint @@ -66,8 +65,6 @@ import javax.inject.Inject @AndroidEntryPoint class SearchFragment : Fragment() { - @Inject - lateinit var musicSource: MusicSource @Inject lateinit var application: Application diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt index bf82e2e9..17f9046c 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/home/SettingsFragment.kt @@ -13,9 +13,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import androidx.navigation.fragment.findNavController import coil.annotation.ExperimentalCoilApi import coil.imageLoader @@ -29,6 +29,7 @@ import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.databinding.FragmentSettingsBinding import com.maxrave.simpmusic.extension.setEnabledAll import com.maxrave.simpmusic.viewModel.SettingsViewModel +import com.maxrave.simpmusic.viewModel.SharedViewModel import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter import java.text.SimpleDateFormat @@ -37,18 +38,15 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale -import javax.inject.Inject @UnstableApi @AndroidEntryPoint class SettingsFragment : Fragment() { - @Inject - lateinit var player: ExoPlayer - private var _binding: FragmentSettingsBinding? = null private val binding get() = _binding!! private val viewModel by viewModels() + private val sharedViewModel by activityViewModels() private val backupLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri -> if (uri != null) { @@ -231,7 +229,7 @@ class SettingsFragment : Fragment() { binding.btEqualizer.setOnClickListener { val eqIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) eqIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, requireContext().packageName) - eqIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.audioSessionId) + eqIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sharedViewModel.simpleMediaServiceHandler?.player?.audioSessionId) eqIntent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) val packageManager = requireContext().packageManager val resolveInfo: List<*> = packageManager.queryIntentActivities(eqIntent, 0) diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/AlbumFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/AlbumFragment.kt index 385b85cc..b60ee8c5 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/AlbumFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/other/AlbumFragment.kt @@ -375,7 +375,7 @@ class AlbumFragment: Fragment() { binding.loadingLayout.visibility = View.GONE val albumEntity = viewModel.albumEntity.value if (albumEntity != null) { - viewModel.checkAllSongDownloaded(it?.tracks as ArrayList) + viewModel.checkAllSongDownloaded(it.tracks as ArrayList) viewModel.albumEntity.observe(viewLifecycleOwner){albumEntity2 -> when (albumEntity2.downloadState) { DownloadState.STATE_DOWNLOADED -> { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/InfoFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/InfoFragment.kt index 92ed5a9f..6738f098 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/InfoFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/InfoFragment.kt @@ -15,10 +15,8 @@ import com.maxrave.simpmusic.R import com.maxrave.simpmusic.databinding.InfoFragmentBinding import com.maxrave.simpmusic.extension.connectArtists import com.maxrave.simpmusic.extension.toListName -import com.maxrave.simpmusic.service.test.source.MusicSource import com.maxrave.simpmusic.viewModel.SharedViewModel import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject @AndroidEntryPoint class InfoFragment: BottomSheetDialogFragment(){ @@ -26,9 +24,6 @@ class InfoFragment: BottomSheetDialogFragment(){ private val binding get() = _binding!! private val viewModel by activityViewModels() - @Inject - lateinit var musicSource: MusicSource - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.FullScreenDialogTheme) @@ -74,19 +69,21 @@ class InfoFragment: BottomSheetDialogFragment(){ @UnstableApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (viewModel.nowPlayingMediaItem.value != null) { - val data = musicSource.catalogMetadata[viewModel.getCurrentMediaItemIndex()] - with(binding){ - toolbar.title = data.title - artistsName.text = data.artists.toListName().connectArtists() - "https://www.youtube.com/watch?v=${data.videoId}".also { youtubeUrl.text = it } - title.text = data.title - albumName.text = data.album?.name - if (viewModel.format.value != null){ - val format = viewModel.format.value - itag.text = format?.itag.toString() - mimeType.text = format?.mimeType ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown) - bitrate.text = (format?.bitrate ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown)).toString() - description.text = format?.description ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown) + if (viewModel.simpleMediaServiceHandler != null) { + val data = viewModel.simpleMediaServiceHandler!!.catalogMetadata[viewModel.getCurrentMediaItemIndex()] + with(binding){ + toolbar.title = data.title + artistsName.text = data.artists.toListName().connectArtists() + "https://www.youtube.com/watch?v=${data.videoId}".also { youtubeUrl.text = it } + title.text = data.title + albumName.text = data.album?.name + if (viewModel.format.value != null){ + val format = viewModel.format.value + itag.text = format?.itag.toString() + mimeType.text = format?.mimeType ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown) + bitrate.text = (format?.bitrate ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown)).toString() + description.text = format?.description ?: context?.getString(androidx.media3.ui.R.string.exo_track_unknown) + } } } } diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt index 1f129190..07d1fe84 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/NowPlayingFragment.kt @@ -64,9 +64,7 @@ import com.maxrave.simpmusic.extension.setEnabledAll import com.maxrave.simpmusic.extension.toListName import com.maxrave.simpmusic.extension.toTrack import com.maxrave.simpmusic.service.RepeatState -import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.service.test.download.MusicDownloadService -import com.maxrave.simpmusic.service.test.source.MusicSource import com.maxrave.simpmusic.utils.Resource import com.maxrave.simpmusic.viewModel.SharedViewModel import com.maxrave.simpmusic.viewModel.UIEvent @@ -76,20 +74,12 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import javax.inject.Inject @UnstableApi @AndroidEntryPoint class NowPlayingFragment : Fragment() { - @Inject - lateinit var musicSource: MusicSource - - @Inject - lateinit var simpleMediaServiceHandler: SimpleMediaServiceHandler - - val viewModel by activityViewModels() private var _binding: FragmentNowPlayingBinding? = null private val binding get() = _binding!! @@ -180,7 +170,7 @@ class NowPlayingFragment : Fragment() { // } // } // } - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.resetRelated() // if (requireContext().isMyServiceRunning(FetchQueue::class.java)) { // requireActivity().stopService( @@ -215,7 +205,7 @@ class NowPlayingFragment : Fragment() { val track = it.toTrack() Queue.clear() Queue.setNowPlaying(track) - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.resetRelated() // if (requireContext().isMyServiceRunning(FetchQueue::class.java)) { // requireActivity().stopService( @@ -263,7 +253,7 @@ class NowPlayingFragment : Fragment() { binding.tvSongTitle.visibility = View.GONE binding.tvSongArtist.visibility = View.GONE Queue.getNowPlaying()?.let { - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.resetRelated() // if (requireContext().isMyServiceRunning(FetchQueue::class.java)) { // requireActivity().stopService( @@ -310,7 +300,7 @@ class NowPlayingFragment : Fragment() { binding.tvSongTitle.visibility = View.GONE binding.tvSongArtist.visibility = View.GONE Queue.getNowPlaying()?.let { - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.resetRelated() // if (requireContext().isMyServiceRunning(FetchQueue::class.java)) { // requireActivity().stopService( @@ -374,7 +364,7 @@ class NowPlayingFragment : Fragment() { binding.tvSongTitle.visibility = View.GONE binding.tvSongArtist.visibility = View.GONE Queue.getNowPlaying()?.let { - musicSource.reset() + viewModel.simpleMediaServiceHandler?.reset() viewModel.resetRelated() // if (requireContext().isMyServiceRunning(FetchQueue::class.java)) { // requireActivity().stopService( @@ -453,7 +443,7 @@ class NowPlayingFragment : Fragment() { // binding.loadingArt.visibility = View.VISIBLE // Log.d("Check Lyrics", viewModel._lyrics.value?.data.toString()) // updateUIfromCurrentMediaItem(song) -// musicSource.setCurrentSongIndex(viewModel.getCurrentMediaItemIndex()) +// simpleMediaServiceHandler.setCurrentSongIndex(viewModel.getCurrentMediaItemIndex()) // viewModel.changeSongTransitionToFalse() // } // } @@ -463,7 +453,7 @@ class NowPlayingFragment : Fragment() { val job7 = launch { - viewModel.nowPLaying.collectLatest { song -> + viewModel.simpleMediaServiceHandler?.nowPlaying?.collectLatest { song -> if (song != null) { viewModel.getFormat(song.mediaId) Log.i("Now Playing Fragment", "song ${song.mediaMetadata.title}") @@ -472,7 +462,7 @@ class NowPlayingFragment : Fragment() { binding.loadingArt.visibility = View.VISIBLE Log.d("Check Lyrics", viewModel._lyrics.value?.data.toString()) updateUIfromCurrentMediaItem(song) - musicSource.setCurrentSongIndex(viewModel.getCurrentMediaItemIndex()) + viewModel.simpleMediaServiceHandler?.setCurrentSongIndex(viewModel.getCurrentMediaItemIndex()) viewModel.changeSongTransitionToFalse() } } @@ -501,7 +491,7 @@ class NowPlayingFragment : Fragment() { } } val job2 = launch { - viewModel.isPlaying.observe(viewLifecycleOwner) { + viewModel.isPlaying.collect { Log.d("Check Song Transistion", "${viewModel.songTransitions.value}") if (it) { binding.btPlayPause.setImageResource(R.drawable.baseline_pause_circle_24) @@ -622,12 +612,12 @@ class NowPlayingFragment : Fragment() { } } val job11 = launch { - viewModel.previousTrackAvailable.collect { available -> + viewModel.simpleMediaServiceHandler?.previousTrackAvailable?.collect { available -> setEnabledAll(binding.btPrevious, available) } } val job12 = launch { - viewModel.nextTrackAvailable.collect { available -> + viewModel.simpleMediaServiceHandler?.nextTrackAvailable?.collect { available -> setEnabledAll(binding.btNext, available) } } @@ -832,18 +822,18 @@ class NowPlayingFragment : Fragment() { binding.topAppBar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.now_playing_dialog_menu_item_more -> { - if (musicSource.catalogMetadata.isNotEmpty()) { + if (!viewModel.simpleMediaServiceHandler?.catalogMetadata.isNullOrEmpty()) { viewModel.refreshSongDB() val dialog = BottomSheetDialog(requireContext()) val bottomSheetView = BottomSheetNowPlayingBinding.inflate(layoutInflater) with(bottomSheetView) { lifecycleScope.launch { - viewModel.sleepTimerMinutes.collect { min -> + viewModel.simpleMediaServiceHandler?.sleepMinutes?.collect { min -> if (min > 0) { - tvSleepTimer.text = getString(R.string.sleep_timer, min.toString()) + tvSleepTimer.text = + getString(R.string.sleep_timer, min.toString()) ivSleepTimer.setImageResource(R.drawable.alarm_enable) - } - else { + } else { tvSleepTimer.text = getString(R.string.sleep_timer_off) ivSleepTimer.setImageResource(R.drawable.baseline_access_alarm_24) } @@ -881,239 +871,281 @@ class NowPlayingFragment : Fragment() { setEnabledAll(btDownload, true) } } - val song = - musicSource.catalogMetadata[viewModel.getCurrentMediaItemIndex()] - tvSongTitle.text = song.title - tvSongTitle.isSelected = true - tvSongArtist.text = song.artists.toListName().connectArtists() - tvSongArtist.isSelected = true - ivThumbnail.load(song.thumbnails?.last()?.url) + if (!viewModel.simpleMediaServiceHandler?.catalogMetadata.isNullOrEmpty()) { + val song = + viewModel.simpleMediaServiceHandler!!.catalogMetadata[viewModel.getCurrentMediaItemIndex()] + tvSongTitle.text = song.title + tvSongTitle.isSelected = true + tvSongArtist.text = song.artists.toListName().connectArtists() + tvSongArtist.isSelected = true + ivThumbnail.load(song.thumbnails?.last()?.url) - btLike.setOnClickListener { - if (cbFavorite.isChecked) { - cbFavorite.isChecked = false - tvFavorite.text = getString(R.string.like) - viewModel.updateLikeStatus(song.videoId, false) - } else { - cbFavorite.isChecked = true - tvFavorite.text = getString(R.string.liked) - viewModel.updateLikeStatus(song.videoId, true) - } - } - btRadio.setOnClickListener { - val args = Bundle() - args.putString("radioId", "RDAMVM${song.videoId}") - args.putString("title", "${song.title} ${context?.getString(R.string.radio)}") - args.putString("thumbnails", song.thumbnails?.lastOrNull()?.url) - dialog.dismiss() - findNavController().navigate(R.id.action_global_playlistFragment, args) - } - btSleepTimer.setOnClickListener { - Log.w("Sleep Timer", "onClick") - if(viewModel.sleepTimerRunning.value == true) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.warning)) - .setMessage(getString(R.string.sleep_timer_warning)) - .setPositiveButton(getString(R.string.yes)) { d, _ -> - viewModel.stopSleepTimer() - Toast.makeText(requireContext(), getString(R.string.sleep_timer_off_done), Toast.LENGTH_SHORT).show() - d.dismiss() - } - .setNegativeButton(getString(R.string.cancel)) { d, _ -> - d.dismiss() - } - .show() - } - else { - val d = BottomSheetDialog(requireContext()) - val v = BottomSheetSleepTimerBinding.inflate(layoutInflater) - v.btSet.setOnClickListener { - val min = v.etTime.editText?.text.toString() - if (min.isNotBlank() && min.toInt() > 0){ - viewModel.setSleepTimer(min.toInt()) - d.dismiss() - } - else { - Toast.makeText(requireContext(), getString(R.string.sleep_timer_set_error), Toast.LENGTH_SHORT).show() - } + btLike.setOnClickListener { + if (cbFavorite.isChecked) { + cbFavorite.isChecked = false + tvFavorite.text = getString(R.string.like) + viewModel.updateLikeStatus(song.videoId, false) + } else { + cbFavorite.isChecked = true + tvFavorite.text = getString(R.string.liked) + viewModel.updateLikeStatus(song.videoId, true) } - d.setContentView(v.root) - d.setCancelable(true) - d.show() - } - } - btAddPlaylist.setOnClickListener { - viewModel.getAllLocalPlaylist() - val listLocalPlaylist: ArrayList = arrayListOf() - val addPlaylistDialog = BottomSheetDialog(requireContext()) - val viewAddPlaylist = BottomSheetAddToAPlaylistBinding.inflate(layoutInflater) - val addToAPlaylistAdapter = AddToAPlaylistAdapter(arrayListOf()) - viewAddPlaylist.rvLocalPlaylists.apply { - adapter = addToAPlaylistAdapter - layoutManager = LinearLayoutManager(requireContext()) } - viewModel.localPlaylist.observe(viewLifecycleOwner) {list -> - Log.d("Check Local Playlist", list.toString()) - listLocalPlaylist.clear() - listLocalPlaylist.addAll(list) - addToAPlaylistAdapter.updateList(listLocalPlaylist) + btRadio.setOnClickListener { + val args = Bundle() + args.putString("radioId", "RDAMVM${song.videoId}") + args.putString( + "title", + "${song.title} ${context?.getString(R.string.radio)}" + ) + args.putString("thumbnails", song.thumbnails?.lastOrNull()?.url) + dialog.dismiss() + findNavController().navigate( + R.id.action_global_playlistFragment, + args + ) } - addToAPlaylistAdapter.setOnItemClickListener(object : AddToAPlaylistAdapter.OnItemClickListener{ - override fun onItemClick(position: Int) { - val playlist = listLocalPlaylist[position] - val tempTrack = ArrayList() - if (playlist.tracks != null) { - tempTrack.addAll(playlist.tracks) - } - if (!tempTrack.contains(song.videoId) && playlist.syncedWithYouTubePlaylist == 1 && playlist.youtubePlaylistId != null) { - viewModel.addToYouTubePlaylist(playlist.id, playlist.youtubePlaylistId, song.videoId) + btSleepTimer.setOnClickListener { + Log.w("Sleep Timer", "onClick") + if (viewModel.sleepTimerRunning.value == true) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.warning)) + .setMessage(getString(R.string.sleep_timer_warning)) + .setPositiveButton(getString(R.string.yes)) { d, _ -> + viewModel.stopSleepTimer() + Toast.makeText( + requireContext(), + getString(R.string.sleep_timer_off_done), + Toast.LENGTH_SHORT + ).show() + d.dismiss() + } + .setNegativeButton(getString(R.string.cancel)) { d, _ -> + d.dismiss() + } + .show() + } else { + val d = BottomSheetDialog(requireContext()) + val v = BottomSheetSleepTimerBinding.inflate(layoutInflater) + v.btSet.setOnClickListener { + val min = v.etTime.editText?.text.toString() + if (min.isNotBlank() && min.toInt() > 0) { + viewModel.setSleepTimer(min.toInt()) + d.dismiss() + } else { + Toast.makeText( + requireContext(), + getString(R.string.sleep_timer_set_error), + Toast.LENGTH_SHORT + ).show() + } } - tempTrack.add(song.videoId) - tempTrack.removeConflicts() - viewModel.updateLocalPlaylistTracks(tempTrack, playlist.id) - addPlaylistDialog.dismiss() - dialog.dismiss() + d.setContentView(v.root) + d.setCancelable(true) + d.show() } - }) - addPlaylistDialog.setContentView(viewAddPlaylist.root) - addPlaylistDialog.setCancelable(true) - addPlaylistDialog.show() - } - - btSeeArtists.setOnClickListener { - val subDialog = BottomSheetDialog(requireContext()) - val subBottomSheetView = - BottomSheetSeeArtistOfNowPlayingBinding.inflate(layoutInflater) - if (song.artists != null) { - val artistAdapter = SeeArtistOfNowPlayingAdapter(song.artists) - subBottomSheetView.rvArtists.apply { - adapter = artistAdapter + } + btAddPlaylist.setOnClickListener { + viewModel.getAllLocalPlaylist() + val listLocalPlaylist: ArrayList = + arrayListOf() + val addPlaylistDialog = BottomSheetDialog(requireContext()) + val viewAddPlaylist = + BottomSheetAddToAPlaylistBinding.inflate(layoutInflater) + val addToAPlaylistAdapter = AddToAPlaylistAdapter(arrayListOf()) + viewAddPlaylist.rvLocalPlaylists.apply { + adapter = addToAPlaylistAdapter layoutManager = LinearLayoutManager(requireContext()) } - artistAdapter.setOnClickListener(object : - SeeArtistOfNowPlayingAdapter.OnItemClickListener { + viewModel.localPlaylist.observe(viewLifecycleOwner) { list -> + Log.d("Check Local Playlist", list.toString()) + listLocalPlaylist.clear() + listLocalPlaylist.addAll(list) + addToAPlaylistAdapter.updateList(listLocalPlaylist) + } + addToAPlaylistAdapter.setOnItemClickListener(object : + AddToAPlaylistAdapter.OnItemClickListener { override fun onItemClick(position: Int) { - val artist = song.artists[position] - if (artist.id != null) { - findNavController().navigate( - R.id.action_global_artistFragment, - Bundle().apply { - putString("channelId", artist.id) - }) - subDialog.dismiss() - dialog.dismiss() + val playlist = listLocalPlaylist[position] + val tempTrack = ArrayList() + if (playlist.tracks != null) { + tempTrack.addAll(playlist.tracks) } + if (!tempTrack.contains(song.videoId) && playlist.syncedWithYouTubePlaylist == 1 && playlist.youtubePlaylistId != null) { + viewModel.addToYouTubePlaylist( + playlist.id, + playlist.youtubePlaylistId, + song.videoId + ) + } + tempTrack.add(song.videoId) + tempTrack.removeConflicts() + viewModel.updateLocalPlaylistTracks( + tempTrack, + playlist.id + ) + addPlaylistDialog.dismiss() + dialog.dismiss() } - }) + addPlaylistDialog.setContentView(viewAddPlaylist.root) + addPlaylistDialog.setCancelable(true) + addPlaylistDialog.show() } - subDialog.setCancelable(true) - subDialog.setContentView(subBottomSheetView.root) - subDialog.show() - } - btShare.setOnClickListener { - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.type = "text/plain" - val url = "https://youtube.com/watch?v=${song.videoId}" - shareIntent.putExtra(Intent.EXTRA_TEXT, url) - val chooserIntent = Intent.createChooser(shareIntent, getString(R.string.share_url)) - startActivity(chooserIntent) - } - btDownload.setOnClickListener { - if (tvDownload.text == getString(R.string.download)) { - Log.d("Download", "onClick: ${song.videoId}") - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_PREPARING - ) - val downloadRequest = - DownloadRequest.Builder(song.videoId, song.videoId.toUri()) - .setData(song.title.toByteArray()) - .setCustomCacheKey(song.videoId) - .build() - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_DOWNLOADING - ) - viewModel.getDownloadStateFromService(song.videoId) - DownloadService.sendAddDownload( - requireContext(), - MusicDownloadService::class.java, - downloadRequest, - false + btSeeArtists.setOnClickListener { + val subDialog = BottomSheetDialog(requireContext()) + val subBottomSheetView = + BottomSheetSeeArtistOfNowPlayingBinding.inflate( + layoutInflater + ) + if (song.artists != null) { + val artistAdapter = + SeeArtistOfNowPlayingAdapter(song.artists) + subBottomSheetView.rvArtists.apply { + adapter = artistAdapter + layoutManager = LinearLayoutManager(requireContext()) + } + artistAdapter.setOnClickListener(object : + SeeArtistOfNowPlayingAdapter.OnItemClickListener { + override fun onItemClick(position: Int) { + val artist = song.artists[position] + if (artist.id != null) { + findNavController().navigate( + R.id.action_global_artistFragment, + Bundle().apply { + putString("channelId", artist.id) + }) + subDialog.dismiss() + dialog.dismiss() + } + } + + }) + } + + subDialog.setCancelable(true) + subDialog.setContentView(subBottomSheetView.root) + subDialog.show() + } + btShare.setOnClickListener { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "text/plain" + val url = "https://youtube.com/watch?v=${song.videoId}" + shareIntent.putExtra(Intent.EXTRA_TEXT, url) + val chooserIntent = Intent.createChooser( + shareIntent, + getString(R.string.share_url) ) - lifecycleScope.launch { - viewModel.downloadState.collect { download -> - if (download != null) { - when (download.state) { - Download.STATE_DOWNLOADING -> { - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_DOWNLOADING - ) - tvDownload.text = getString(R.string.downloading) - ivDownload.setImageResource(R.drawable.baseline_downloading_white) - setEnabledAll(btDownload, true) - } + startActivity(chooserIntent) + } + btDownload.setOnClickListener { + if (tvDownload.text == getString(R.string.download)) { + Log.d("Download", "onClick: ${song.videoId}") + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_PREPARING + ) + val downloadRequest = + DownloadRequest.Builder( + song.videoId, + song.videoId.toUri() + ) + .setData(song.title.toByteArray()) + .setCustomCacheKey(song.videoId) + .build() + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_DOWNLOADING + ) + viewModel.getDownloadStateFromService(song.videoId) + DownloadService.sendAddDownload( + requireContext(), + MusicDownloadService::class.java, + downloadRequest, + false + ) + lifecycleScope.launch { + viewModel.downloadState.collect { download -> + if (download != null) { + when (download.state) { + Download.STATE_DOWNLOADING -> { + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_DOWNLOADING + ) + tvDownload.text = + getString(R.string.downloading) + ivDownload.setImageResource(R.drawable.baseline_downloading_white) + setEnabledAll(btDownload, true) + } - Download.STATE_FAILED -> { - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_NOT_DOWNLOADED - ) - tvDownload.text = getString(R.string.download) - ivDownload.setImageResource(R.drawable.outline_download_for_offline_24) - setEnabledAll(btDownload, true) - Toast.makeText( - requireContext(), - getString(androidx.media3.exoplayer.R.string.exo_download_failed), - Toast.LENGTH_SHORT - ).show() - } + Download.STATE_FAILED -> { + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) + tvDownload.text = + getString(R.string.download) + ivDownload.setImageResource(R.drawable.outline_download_for_offline_24) + setEnabledAll(btDownload, true) + Toast.makeText( + requireContext(), + getString(androidx.media3.exoplayer.R.string.exo_download_failed), + Toast.LENGTH_SHORT + ).show() + } - Download.STATE_COMPLETED -> { - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_DOWNLOADED - ) - Toast.makeText( - requireContext(), - getString(androidx.media3.exoplayer.R.string.exo_download_completed), - Toast.LENGTH_SHORT - ).show() - tvDownload.text = getString(R.string.downloaded) - ivDownload.setImageResource(R.drawable.baseline_downloaded) - setEnabledAll(btDownload, true) - } - else -> { - Log.d("Download", "onCreate: ${download.state}") + Download.STATE_COMPLETED -> { + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_DOWNLOADED + ) + Toast.makeText( + requireContext(), + getString(androidx.media3.exoplayer.R.string.exo_download_completed), + Toast.LENGTH_SHORT + ).show() + tvDownload.text = + getString(R.string.downloaded) + ivDownload.setImageResource(R.drawable.baseline_downloaded) + setEnabledAll(btDownload, true) + } + + else -> { + Log.d( + "Download", + "onCreate: ${download.state}" + ) + } } } } } + } else if (tvDownload.text == getString(R.string.downloaded) || tvDownload.text == getString( + R.string.downloading + ) + ) { + DownloadService.sendRemoveDownload( + requireContext(), + MusicDownloadService::class.java, + song.videoId, + false + ) + viewModel.updateDownloadState( + song.videoId, + DownloadState.STATE_NOT_DOWNLOADED + ) + tvDownload.text = getString(R.string.download) + ivDownload.setImageResource(R.drawable.outline_download_for_offline_24) + setEnabledAll(btDownload, true) + Toast.makeText( + requireContext(), + getString(R.string.removed_download), + Toast.LENGTH_SHORT + ).show() } } - else if (tvDownload.text == getString(R.string.downloaded) || tvDownload.text == getString(R.string.downloading)) { - DownloadService.sendRemoveDownload( - requireContext(), - MusicDownloadService::class.java, - song.videoId, - false - ) - viewModel.updateDownloadState( - song.videoId, - DownloadState.STATE_NOT_DOWNLOADED - ) - tvDownload.text = getString(R.string.download) - ivDownload.setImageResource(R.drawable.outline_download_for_offline_24) - setEnabledAll(btDownload, true) - Toast.makeText( - requireContext(), - getString(R.string.removed_download), - Toast.LENGTH_SHORT - ).show() - } } } dialog.setCancelable(true) diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt index ce6e545d..7030c808 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/fragment/player/QueueFragment.kt @@ -20,19 +20,15 @@ import com.maxrave.simpmusic.adapter.queue.QueueAdapter import com.maxrave.simpmusic.databinding.BottomSheetQueueTrackOptionBinding import com.maxrave.simpmusic.databinding.QueueBottomSheetBinding import com.maxrave.simpmusic.extension.setEnabledAll -import com.maxrave.simpmusic.service.test.source.MusicSource -import com.maxrave.simpmusic.service.test.source.StateSource +import com.maxrave.simpmusic.service.StateSource import com.maxrave.simpmusic.viewModel.SharedViewModel import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class QueueFragment: BottomSheetDialogFragment() { - @Inject - lateinit var musicSource: MusicSource - private val viewModel by activityViewModels() private var _binding: QueueBottomSheetBinding? = null private val binding get() = _binding!! @@ -94,13 +90,13 @@ class QueueFragment: BottomSheetDialogFragment() { adapter = queueAdapter layoutManager = LinearLayoutManager(requireContext()) } - if (musicSource.catalogMetadata.isNotEmpty()) { - queueAdapter.updateList(musicSource.catalogMetadata) + if (!viewModel.simpleMediaServiceHandler?.catalogMetadata.isNullOrEmpty()) { + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) } lifecycleScope.launch { val job1 = launch { - musicSource.stateFlow.collect{ state -> + viewModel.simpleMediaServiceHandler?.stateFlow?.collect{ state -> when(state) { StateSource.STATE_INITIALIZING -> { binding.loadingQueue.visibility = View.VISIBLE @@ -113,7 +109,7 @@ class QueueFragment: BottomSheetDialogFragment() { StateSource.STATE_INITIALIZED -> { binding.loadingQueue.visibility = View.GONE binding.rvQueue.visibility = View.VISIBLE - queueAdapter.updateList(musicSource.catalogMetadata) + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) } else -> { binding.loadingQueue.visibility = View.VISIBLE @@ -126,21 +122,20 @@ class QueueFragment: BottomSheetDialogFragment() { updateNowPlaying() } val job3 = launch { - musicSource.currentSongIndex.collect{ index -> + viewModel.simpleMediaServiceHandler?.currentSongIndex?.collect{ index -> Log.d("QueueFragment", "onViewCreated: $index") - if (musicSource.stateFlow.value == StateSource.STATE_INITIALIZED || musicSource.stateFlow.value == StateSource.STATE_INITIALIZING){ + if (viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZED || viewModel.simpleMediaServiceHandler?.stateFlow?.first() == StateSource.STATE_INITIALIZING){ binding.rvQueue.smoothScrollToPosition(index) queueAdapter.setCurrentPlaying(index) } } } val job4 = launch { - musicSource.added.collect{ isAdded -> + viewModel.simpleMediaServiceHandler?.added?.collect{ isAdded -> Log.d("Check Added in Queue", "$isAdded") if (isAdded){ - Log.d("Check Queue", "${musicSource.catalogMetadata}") - queueAdapter.updateList(musicSource.catalogMetadata) - musicSource.changeAddedState() + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) + viewModel.simpleMediaServiceHandler?.changeAddedState() } } } @@ -163,37 +158,46 @@ class QueueFragment: BottomSheetDialogFragment() { override fun onOptionClick(position: Int) { val dialog = BottomSheetDialog(requireContext()) val dialogView = BottomSheetQueueTrackOptionBinding.inflate(layoutInflater) - with(dialogView) { - btMoveUp.setOnClickListener { musicSource.moveItemUp(position) - queueAdapter.updateList(musicSource.catalogMetadata) - dialog.dismiss() } - btMoveDown.setOnClickListener { musicSource.moveItemDown(position) - queueAdapter.updateList(musicSource.catalogMetadata) - dialog.dismiss() } - btDelete.setOnClickListener { musicSource.removeMediaItem(position) - queueAdapter.updateList(musicSource.catalogMetadata) - dialog.dismiss() } - } - if (musicSource.catalogMetadata.size > 1) { - when (position) { - 0 -> { - setEnabledAll(dialogView.btMoveUp, false) - setEnabledAll(dialogView.btMoveDown, true) + if (viewModel.simpleMediaServiceHandler != null) { + with(dialogView) { + btMoveUp.setOnClickListener { + viewModel.simpleMediaServiceHandler?.moveItemUp(position) + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) + dialog.dismiss() } - musicSource.catalogMetadata.size - 1 -> { - setEnabledAll(dialogView.btMoveUp, true) - setEnabledAll(dialogView.btMoveDown, false) + btMoveDown.setOnClickListener { + viewModel.simpleMediaServiceHandler?.moveItemDown(position) + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) + dialog.dismiss() } - else -> { - setEnabledAll(dialogView.btMoveUp, true) - setEnabledAll(dialogView.btMoveDown, true) + btDelete.setOnClickListener { + viewModel.simpleMediaServiceHandler?.removeMediaItem(position) + queueAdapter.updateList(viewModel.simpleMediaServiceHandler!!.catalogMetadata) + dialog.dismiss() } } - } - else { - setEnabledAll(dialogView.btMoveUp, false) - setEnabledAll(dialogView.btMoveDown, false) - setEnabledAll(dialogView.btDelete, false) + if (viewModel.simpleMediaServiceHandler!!.catalogMetadata.size > 1) { + when (position) { + 0 -> { + setEnabledAll(dialogView.btMoveUp, false) + setEnabledAll(dialogView.btMoveDown, true) + } + + viewModel.simpleMediaServiceHandler!!.catalogMetadata.size - 1 -> { + setEnabledAll(dialogView.btMoveUp, true) + setEnabledAll(dialogView.btMoveDown, false) + } + + else -> { + setEnabledAll(dialogView.btMoveUp, true) + setEnabledAll(dialogView.btMoveDown, true) + } + } + } else { + setEnabledAll(dialogView.btMoveUp, false) + setEnabledAll(dialogView.btMoveDown, false) + setEnabledAll(dialogView.btDelete, false) + } } dialog.setCancelable(true) dialog.setContentView(dialogView.root) diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SearchViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SearchViewModel.kt index e577661d..c4a19d7b 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SearchViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SearchViewModel.kt @@ -183,10 +183,6 @@ class SearchViewModel @Inject constructor(private val mainRepository: MainReposi } - override fun onCleared() { - super.onCleared() - } - fun searchAlbums(query: String) { if (loading.value == false){ loading.value = true diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt index b6e62478..795f10e7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SettingsViewModel.kt @@ -30,7 +30,6 @@ import com.maxrave.simpmusic.extension.div import com.maxrave.simpmusic.extension.zipInputStream import com.maxrave.simpmusic.extension.zipOutputStream import com.maxrave.simpmusic.service.SimpleMediaService -import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.ui.MainActivity import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -52,7 +51,6 @@ class SettingsViewModel @Inject constructor( private var databaseDao: DatabaseDao, @PlayerCache private val playerCache: SimpleCache, @DownloadCache private val downloadCache: SimpleCache, - private val simpleMediaServiceHandler: SimpleMediaServiceHandler ) : AndroidViewModel(application) { @Inject @@ -369,7 +367,6 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { withContext((Dispatchers.Main)) { dataStoreManager.setNormalizeVolume(normalizeVolume) - simpleMediaServiceHandler.editNormalizeVolume(normalizeVolume) getNormalizeVolume() } } @@ -407,7 +404,6 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { withContext((Dispatchers.Main)) { dataStoreManager.setSkipSilent(skip) - simpleMediaServiceHandler.editSkipSilent(skip) getSkipSilent() } } diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt index 2cba2c09..d7116aca 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/SharedViewModel.kt @@ -55,12 +55,9 @@ import com.maxrave.simpmusic.service.RepeatState import com.maxrave.simpmusic.service.SimpleMediaServiceHandler import com.maxrave.simpmusic.service.SimpleMediaState import com.maxrave.simpmusic.service.test.download.DownloadUtils -import com.maxrave.simpmusic.service.test.source.MusicSource -import com.maxrave.simpmusic.service.test.source.StateSource import com.maxrave.simpmusic.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -78,22 +75,19 @@ import javax.inject.Inject @HiltViewModel @UnstableApi -class SharedViewModel @Inject constructor(private var dataStoreManager: DataStoreManager, @DownloadCache private val downloadedCache: SimpleCache, private val musicSource: MusicSource, private val mainRepository: MainRepository, private val simpleMediaServiceHandler: SimpleMediaServiceHandler, private val application: Application) : AndroidViewModel(application){ +class SharedViewModel @Inject constructor(private var dataStoreManager: DataStoreManager, @DownloadCache private val downloadedCache: SimpleCache, private val mainRepository: MainRepository, private val application: Application) : AndroidViewModel(application){ @Inject lateinit var downloadUtils: DownloadUtils private var restoreLastPlayedTrackDone: Boolean = false - private var _allSongsDB: MutableLiveData> = MutableLiveData() - val allSongsDB: LiveData> = _allSongsDB + var simpleMediaServiceHandler: SimpleMediaServiceHandler? = null private var _songDB: MutableLiveData = MutableLiveData() val songDB: LiveData = _songDB private var _liked: MutableStateFlow = MutableStateFlow(false) val liked: SharedFlow = _liked.asSharedFlow() - private var loadMusicSourceJob: Job? = null - protected val context get() = getApplication() @@ -124,7 +118,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor private val _uiState = MutableStateFlow(UIState.Initial) val uiState = _uiState.asStateFlow() - var isPlaying = MutableLiveData(false) + var isPlaying = MutableStateFlow(false) var notReady = MutableLiveData(true) var _lyrics = MutableLiveData>() @@ -135,18 +129,9 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor private var _nowPlayingMediaItem = MutableLiveData() val nowPlayingMediaItem: LiveData = _nowPlayingMediaItem - private var _nowPlaying = simpleMediaServiceHandler.nowPlaying - val nowPLaying = _nowPlaying.asSharedFlow() - private var _songTransitions = MutableStateFlow(false) val songTransitions: StateFlow = _songTransitions - private var _nextTrackAvailable = simpleMediaServiceHandler.nextTrackAvailable - val nextTrackAvailable: StateFlow = _nextTrackAvailable - - private var _previousTrackAvailable = simpleMediaServiceHandler.previousTrackAvailable - val previousTrackAvailable: StateFlow = _previousTrackAvailable - private var _shuffleModeEnabled = MutableStateFlow(false) val shuffleModeEnabled: StateFlow = _shuffleModeEnabled @@ -157,13 +142,6 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor private var _skipSegments: MutableStateFlow?> = MutableStateFlow(null) val skipSegments: StateFlow?> = _skipSegments - //Sleep Timer - private var _sleepTimerMinutes = simpleMediaServiceHandler.sleepMinutes - val sleepTimerMinutes: SharedFlow = _sleepTimerMinutes - - private var _sleepTimerDone = simpleMediaServiceHandler.sleepDone - val sleepTimerDone: SharedFlow = _sleepTimerDone - private var _sleepTimerRunning: MutableLiveData = MutableLiveData(false) val sleepTimerRunning: LiveData = _sleepTimerRunning @@ -184,154 +162,152 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor val intent: MutableStateFlow = MutableStateFlow(null) - init { -// regionCode = runBlocking { dataStoreManager.location.first() } -// quality = runBlocking { dataStoreManager.quality.first() } -// language = runBlocking { dataStoreManager.getString(SELECTED_LANGUAGE).first() } -// val from_backup = runBlocking { dataStoreManager.playlistFromSaved.first() } -// if (runBlocking { dataStoreManager.saveRecentSongAndQueue.first() == TRUE }) { -// if (from_backup != null) { -// from.postValue(from_backup) -// } -// recentPosition = runBlocking { (dataStoreManager.recentPosition.first()) } -// } - loadMusicSourceJob = Job() - viewModelScope.launch { - val job1 = launch { - simpleMediaServiceHandler.simpleMediaState.collect { mediaState -> - when (mediaState) { - is SimpleMediaState.Buffering -> { - notReady.value = true - } - SimpleMediaState.Initial -> _uiState.value = UIState.Initial - SimpleMediaState.Ended -> { - _uiState.value = UIState.Ended - Log.d("Check lại videoId", videoId.value.toString()) - } - is SimpleMediaState.Playing -> isPlaying.value = mediaState.isPlaying - is SimpleMediaState.Progress -> { - if (_duration.value > 0){ - calculateProgressValues(mediaState.progress) - _progressMillis.value = mediaState.progress +// init { +// Log.w("Check SharedViewModel init", (simpleMediaServiceHandler != null).toString()) +//// regionCode = runBlocking { dataStoreManager.location.first() } +//// quality = runBlocking { dataStoreManager.quality.first() } +//// language = runBlocking { dataStoreManager.getString(SELECTED_LANGUAGE).first() } +//// val from_backup = runBlocking { dataStoreManager.playlistFromSaved.first() } +//// if (runBlocking { dataStoreManager.saveRecentSongAndQueue.first() == TRUE }) { +//// if (from_backup != null) { +//// from.postValue(from_backup) +//// } +//// recentPosition = runBlocking { (dataStoreManager.recentPosition.first()) } +//// } +// +// } + fun init() { + if (simpleMediaServiceHandler != null) { + viewModelScope.launch { + val job1 = launch { + simpleMediaServiceHandler!!.simpleMediaState.collect { mediaState -> + when (mediaState) { + is SimpleMediaState.Buffering -> { + notReady.value = true + } + SimpleMediaState.Initial -> _uiState.value = UIState.Initial + SimpleMediaState.Ended -> { + _uiState.value = UIState.Ended + Log.d("Check lại videoId", videoId.value.toString()) + } + is SimpleMediaState.Playing -> isPlaying.value = mediaState.isPlaying + is SimpleMediaState.Progress -> { + if (_duration.value > 0){ + calculateProgressValues(mediaState.progress) + _progressMillis.value = mediaState.progress + } + } + is SimpleMediaState.Loading -> { + _bufferedPercentage.value = mediaState.bufferedPercentage + _duration.value = mediaState.duration + } + is SimpleMediaState.Ready -> { + notReady.value = false + _duration.value = mediaState.duration + calculateProgressValues(simpleMediaServiceHandler!!.getProgress()) + _uiState.value = UIState.Ready + getFormat(simpleMediaServiceHandler!!.nowPlaying.first()?.mediaId) } - } - is SimpleMediaState.Loading -> { - _bufferedPercentage.value = mediaState.bufferedPercentage - _duration.value = mediaState.duration - } - is SimpleMediaState.Ready -> { - notReady.value = false - _duration.value = mediaState.duration - _uiState.value = UIState.Ready - getFormat(nowPLaying.first()?.mediaId) } } } - } - val job2 = launch { - simpleMediaServiceHandler.nowPlaying.collectLatest { nowPlaying -> - nowPlaying?.let { now -> - getSkipSegments(now.mediaId) - if (dataStoreManager.sendBackToGoogle.first() == TRUE) { - mainRepository.getFormat(now.mediaId).collect { formatTemp -> - if (formatTemp != null) { - initPlayback(formatTemp.playbackTrackingVideostatsPlaybackUrl, formatTemp.playbackTrackingAtrUrl, formatTemp.playbackTrackingVideostatsWatchtimeUrl) + val job2 = launch { + simpleMediaServiceHandler!!.nowPlaying.collectLatest { nowPlaying -> + nowPlaying?.let { now -> + getSkipSegments(now.mediaId) + if (dataStoreManager.sendBackToGoogle.first() == TRUE) { + mainRepository.getFormat(now.mediaId).collect { formatTemp -> + if (formatTemp != null) { + initPlayback(formatTemp.playbackTrackingVideostatsPlaybackUrl, formatTemp.playbackTrackingAtrUrl, formatTemp.playbackTrackingVideostatsWatchtimeUrl) + } } } } - } - if (nowPlaying != null && getCurrentMediaItemIndex() > 0) { - _nowPlayingMediaItem.postValue(nowPlaying) - var downloaded = false - val tempSong = musicSource.catalogMetadata[getCurrentMediaItemIndex()] - Log.d("Check tempSong", tempSong.toString()) - mainRepository.insertSong(tempSong.toSongEntity()) - mainRepository.getSongById(tempSong.videoId) - .collectLatest { songEntity -> - _songDB.value = songEntity - if (songEntity != null) { - _liked.value = songEntity.liked - simpleMediaServiceHandler.like(songEntity.liked) - downloaded = - songEntity.downloadState == DownloadState.STATE_DOWNLOADED - Log.d("Check like", songEntity.toString()) + if (nowPlaying != null && getCurrentMediaItemIndex() > 0) { + _nowPlayingMediaItem.postValue(nowPlaying) + var downloaded = false + val tempSong = simpleMediaServiceHandler!!.catalogMetadata.get(getCurrentMediaItemIndex()) + Log.d("Check tempSong", tempSong.toString()) + mainRepository.insertSong(tempSong.toSongEntity()) + mainRepository.getSongById(tempSong.videoId) + .collectLatest { songEntity -> + _songDB.value = songEntity + if (songEntity != null) { + _liked.value = songEntity.liked + simpleMediaServiceHandler!!.like(songEntity.liked) + downloaded = + songEntity.downloadState == DownloadState.STATE_DOWNLOADED + Log.d("Check like", songEntity.toString()) + } } - } - mainRepository.updateSongInLibrary(LocalDateTime.now(), tempSong.videoId) - mainRepository.updateListenCount(tempSong.videoId) - videoId.postValue(tempSong.videoId) - _nowPlayingMediaItem.value = nowPlaying - resetLyrics() - if (!downloaded) { - mainRepository.getLyricsData("${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}") - .collect { response -> - _lyrics.value = response - when (_lyrics.value) { - is Resource.Success -> { - if (_lyrics.value?.data != null) { - insertLyrics( - _lyrics.value?.data!!.toLyricsEntity( - nowPlaying.mediaId + mainRepository.updateSongInLibrary(LocalDateTime.now(), tempSong.videoId) + mainRepository.updateListenCount(tempSong.videoId) + videoId.postValue(tempSong.videoId) + _nowPlayingMediaItem.value = nowPlaying + resetLyrics() + if (!downloaded) { + mainRepository.getLyricsData("${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}") + .collect { response -> + _lyrics.value = response + when (_lyrics.value) { + is Resource.Success -> { + if (_lyrics.value?.data != null) { + insertLyrics( + _lyrics.value?.data!!.toLyricsEntity( + nowPlaying.mediaId + ) ) - ) - parseLyrics(_lyrics.value?.data) + parseLyrics(_lyrics.value?.data) + } } - } - is Resource.Error -> { - if (_lyrics.value?.message != "reset") { - getSavedLyrics( - tempSong.videoId, - "${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}" - ) + is Resource.Error -> { + if (_lyrics.value?.message != "reset") { + getSavedLyrics( + tempSong.videoId, + "${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}" + ) + } } - } - else -> { - Log.d("Check lyrics", "Loading") + else -> { + Log.d("Check lyrics", "Loading") + } } } - } - } else { - getSavedLyrics( - tempSong.videoId, - "${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}" - ) + } else { + getSavedLyrics( + tempSong.videoId, + "${tempSong.title} ${tempSong.artists?.firstOrNull()?.name}" + ) + } } } } - } - val job3 = launch { - simpleMediaServiceHandler.shuffle.collect { shuffle -> - _shuffleModeEnabled.value = shuffle - } - } - val job4 = launch { - simpleMediaServiceHandler.repeat.collect { repeat -> - _repeatMode.value = repeat + val job3 = launch { + simpleMediaServiceHandler!!.shuffle.collect { shuffle -> + _shuffleModeEnabled.value = shuffle + } } - } - val job5 = launch { - sleepTimerDone.collect { done -> - if (done) { - _sleepTimerRunning.value = false + val job4 = launch { + simpleMediaServiceHandler!!.repeat.collect { repeat -> + _repeatMode.value = repeat } } - } - val job6 = launch { - simpleMediaServiceHandler.liked.collect { liked -> - if (liked != _liked.value) { - videoId.value?.let { updateLikeStatus(it, liked) } + val job6 = launch { + simpleMediaServiceHandler!!.liked.collect { liked -> + if (liked != _liked.value) { + videoId.value?.let { updateLikeStatus(it, liked) } + } } } - } - job1.join() - job2.join() - job3.join() - job4.join() - job5.join() - job6.join() + job1.join() + job2.join() + job3.join() + job4.join() + job6.join() + } } } @@ -355,15 +331,15 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor fun setSleepTimer(minutes: Int) { _sleepTimerRunning.value = true - simpleMediaServiceHandler.sleepStart(minutes) + simpleMediaServiceHandler!!.sleepStart(minutes) } fun stopSleepTimer() { _sleepTimerRunning.value = false - simpleMediaServiceHandler.sleepStop() + simpleMediaServiceHandler!!.sleepStop() } fun updateLikeInNotification(liked: Boolean) { - simpleMediaServiceHandler.like(liked) + simpleMediaServiceHandler!!.like(liked) } private var _downloadState: MutableStateFlow = MutableStateFlow(null) @@ -412,7 +388,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor viewModelScope.launch { dataStoreManager.isRestoringDatabase.first().let { restoring -> isRestoring.value = restoring == TRUE - isRestoring.collect() { it -> + isRestoring.collect { it -> if (it) { Toast.makeText(context, context.getString(R.string.restore_success), Toast.LENGTH_SHORT).show() mainRepository.getDownloadedSongs().collect { songs -> @@ -493,22 +469,22 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } fun getCurrentMediaItem(): MediaItem? { - _nowPlayingMediaItem.value = simpleMediaServiceHandler.getCurrentMediaItem() - return simpleMediaServiceHandler.getCurrentMediaItem() + _nowPlayingMediaItem.value = simpleMediaServiceHandler?.getCurrentMediaItem() + return simpleMediaServiceHandler?.getCurrentMediaItem() } fun getCurrentMediaItemIndex(): Int { - return simpleMediaServiceHandler.currentIndex() + return simpleMediaServiceHandler?.currentIndex() ?: 0 } @UnstableApi fun playMediaItemInMediaSource(index: Int){ - simpleMediaServiceHandler.playMediaItemInMediaSource(index) + simpleMediaServiceHandler?.playMediaItemInMediaSource(index) } @UnstableApi fun loadMediaItemFromTrack(track: Track, type: String, index: Int? = null) { quality = runBlocking { dataStoreManager.quality.first() } viewModelScope.launch { - simpleMediaServiceHandler.clearMediaItems() + simpleMediaServiceHandler?.clearMediaItems() var uri = "" mainRepository.insertSong(track.toSongEntity()) mainRepository.getSongById(track.videoId) @@ -526,7 +502,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor if (thumbUrl.contains("w120")) { thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") } - simpleMediaServiceHandler.addMediaItem( + simpleMediaServiceHandler?.addMediaItem( MediaItem.Builder() .setUri(track.videoId) .setMediaId(track.videoId) @@ -547,7 +523,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor "Check MediaItem Thumbnail", getCurrentMediaItem()?.mediaMetadata?.artworkUri.toString() ) - musicSource.addFirstMetadata(track) + simpleMediaServiceHandler?.addFirstMetadata(track) getSavedLyrics(track.videoId, "${track.title} ${track.artists?.firstOrNull()?.name}") } else { // var itag = 0 @@ -570,7 +546,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor thumbUrl = Regex("([wh])120").replace(thumbUrl, "$1544") } Log.d("Check URI", uri) - simpleMediaServiceHandler.addMediaItem( + simpleMediaServiceHandler?.addMediaItem( MediaItem.Builder() .setUri(track.videoId) .setMediaId(track.videoId) @@ -591,7 +567,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor "Check MediaItem Thumbnail", getCurrentMediaItem()?.mediaMetadata?.artworkUri.toString() ) - musicSource.addFirstMetadata(track) + simpleMediaServiceHandler?.addFirstMetadata(track) resetLyrics() mainRepository.getLyricsData("${track.title} ${track.artists?.firstOrNull()?.name}").collect { response -> _lyrics.value = response @@ -646,19 +622,26 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } RECOVER_TRACK_QUEUE -> { if (getString(RESTORE_LAST_PLAYED_TRACK_AND_QUEUE_DONE) == DataStoreManager.FALSE) { + recentPosition = runBlocking { dataStoreManager.recentPosition.first() } restoreLastPLayedTrackDone() from.postValue(from_backup) - simpleMediaServiceHandler.seekTo(recentPosition) + simpleMediaServiceHandler?.seekTo(recentPosition) Log.d("Check recentPosition", recentPosition) - songDB.value?.duration?.let { - if (it != "" && it.contains(":")) { - it.split(":").let { split -> + if (songDB.value?.duration != null) { + if (songDB.value?.duration != "" && songDB.value?.duration?.contains(":") == true) { + songDB.value?.duration?.split(":")?.let { split -> _duration.emit(((split[0].toInt() * 60) + split[1].toInt())*1000.toLong()) Log.d("Check Duration", _duration.value.toString()) calculateProgressValues(recentPosition.toLong()) } } } + else { + simpleMediaServiceHandler?.getPlayerDuration()?.let { + _duration.emit(it) + calculateProgressValues(recentPosition.toLong()) + } + } getSaveQueue() } } @@ -670,22 +653,22 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor @UnstableApi fun onUIEvent(uiEvent: UIEvent) = viewModelScope.launch { when (uiEvent) { - UIEvent.Backward -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Backward) - UIEvent.Forward -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Forward) - UIEvent.PlayPause -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.PlayPause) - UIEvent.Next -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Next) - UIEvent.Previous -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Previous) - UIEvent.Stop -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Stop) + UIEvent.Backward -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Backward) + UIEvent.Forward -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Forward) + UIEvent.PlayPause -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.PlayPause) + UIEvent.Next -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Next) + UIEvent.Previous -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Previous) + UIEvent.Stop -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Stop) is UIEvent.UpdateProgress -> { _progress.value = uiEvent.newProgress - simpleMediaServiceHandler.onPlayerEvent( + simpleMediaServiceHandler?.onPlayerEvent( PlayerEvent.UpdateProgress( uiEvent.newProgress ) ) } - UIEvent.Repeat -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Repeat) - UIEvent.Shuffle -> simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Shuffle) + UIEvent.Repeat -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Repeat) + UIEvent.Shuffle -> simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Shuffle) } } fun formatDuration(duration: Long): String { @@ -827,8 +810,9 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor Log.d("Check from", from.value!!) dataStoreManager.setPlaylistFromSaved(from.value!!) } - simpleMediaServiceHandler.onPlayerEvent(PlayerEvent.Stop) + simpleMediaServiceHandler?.onPlayerEvent(PlayerEvent.Stop) } + Log.w("Check onCleared", "onCleared") } fun changeSongTransitionToFalse() { @@ -1023,7 +1007,7 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } } fun skipSegment(position: Long) { - simpleMediaServiceHandler.skipSegment(position) + simpleMediaServiceHandler?.skipSegment(position) } fun sponsorBlockEnabled() = runBlocking { dataStoreManager.sponsorBlockEnabled.first() } fun sponsorBlockCategories() = runBlocking { dataStoreManager.getSponsorBlockCategories() } @@ -1048,38 +1032,11 @@ class SharedViewModel @Inject constructor(private var dataStoreManager: DataStor } fun addQueueToPlayer() { - loadMusicSourceJob?.cancel() - loadMusicSourceJob = viewModelScope.launch { - musicSource.load() - } + simpleMediaServiceHandler?.addQueueToPlayer() } private fun loadPlaylistOrAlbum(index: Int? = null) { - loadMusicSourceJob?.cancel() - loadMusicSourceJob = viewModelScope.launch { - musicSource.load() - musicSource.stateFlow.collect{ state -> - if (state == StateSource.STATE_INITIALIZED) { - when (index) { - null -> { - musicSource.addFirstMediaItem(simpleMediaServiceHandler.getMediaItemWithIndex(0)) - } - -1 -> { - - } - else -> { - musicSource.addFirstMediaItemToIndex(simpleMediaServiceHandler.getMediaItemWithIndex(0), index) - Queue.getNowPlaying().let { song -> - if (song != null) { - musicSource.catalogMetadata.removeAt(0) - musicSource.catalogMetadata.add(index, song) - } - } - } - } - } - } - } + simpleMediaServiceHandler?.loadPlaylistOrAlbum(index) } fun resetRelated() { diff --git a/app/src/main/res/navigation/nav_bottom_navigation.xml b/app/src/main/res/navigation/nav_bottom_navigation.xml index 161663ff..e3fa825b 100644 --- a/app/src/main/res/navigation/nav_bottom_navigation.xml +++ b/app/src/main/res/navigation/nav_bottom_navigation.xml @@ -17,9 +17,6 @@ app:exitAnim="@anim/top_to_bottom" app:popEnterAnim="@anim/bottom_to_top" app:popExitAnim="@anim/top_to_bottom"> - - - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 3720e36b..14253b58 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.application") version "8.1.2" apply false id("com.android.library") version "8.1.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.10" apply false id("androidx.navigation.safeargs.kotlin") version "2.5.3" apply false id("com.google.dagger.hilt.android") version "2.48" apply false id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0" apply false diff --git a/kotlinYtmusicScraper/build.gradle.kts b/kotlinYtmusicScraper/build.gradle.kts index 2d65338f..3bf9bed8 100644 --- a/kotlinYtmusicScraper/build.gradle.kts +++ b/kotlinYtmusicScraper/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.9.0") + implementation("com.google.android.material:material:1.10.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt index d46002f8..69f3631d 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt @@ -52,13 +52,7 @@ class Ytmusic { private var cookieMap = emptyMap() var spotifyCookie: String? = null - set(value) { - field = value - } var musixmatchUserToken: String? = null - set(value) { - field = value - } var proxy: Proxy? = null set(value) { diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/CreatePlaylistResponse.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/CreatePlaylistResponse.kt index 224a1ed7..fe2eeca7 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/CreatePlaylistResponse.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/CreatePlaylistResponse.kt @@ -5,6 +5,4 @@ import kotlinx.serialization.Serializable @Serializable data class CreatePlaylistResponse( val playlistId: String, -) { - -} \ No newline at end of file +) \ No newline at end of file diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/CustomRedirectConfig.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/CustomRedirectConfig.kt index f96ac5c5..83154b10 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/CustomRedirectConfig.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/CustomRedirectConfig.kt @@ -25,14 +25,14 @@ private val LOGGER = KtorSimpleLogger("io.ktor.client.plugins.HttpRedirect") /** * An [HttpClient] plugin that handles HTTP redirects */ -public class CustomRedirectConfig private constructor( +class CustomRedirectConfig private constructor( private val checkHttpMethod: Boolean, private val allowHttpsDowngrade: Boolean, private val defaultHostUrl : String? = null ) { @KtorDsl - public class Config { + class Config { /** * Checks whether the HTTP method is allowed for the redirect. @@ -40,23 +40,23 @@ public class CustomRedirectConfig private constructor( * * Please note: changing this flag could lead to security issues, consider changing the request URL instead. */ - public var checkHttpMethod: Boolean = true + var checkHttpMethod: Boolean = true /** * `true` allows a client to make a redirect with downgrading from HTTPS to plain HTTP. */ - public var allowHttpsDowngrade: Boolean = false + var allowHttpsDowngrade: Boolean = false - public var defaultHostUrl: String? = null + var defaultHostUrl: String? = null } - public companion object Plugin : HttpClientPlugin { + companion object Plugin : HttpClientPlugin { override val key: AttributeKey = AttributeKey("HttpRedirect") /** * Occurs when receiving a response with a redirect message. */ - public val HttpResponseRedirect: EventDefinition = EventDefinition() + val HttpResponseRedirect: EventDefinition = EventDefinition() override fun prepare(block: Config.() -> Unit): CustomRedirectConfig { val config = Config().apply(block)