diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a9647b1b..42ce60803 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -121,7 +121,9 @@ android:name=".appshortcuts.AppShortcutLauncherActivity" android:launchMode="singleInstance" android:theme="@android:style/Theme.Translucent.NoTitleBar" /> - + - + android:enabled="true" + android:label="@string/app_name"> - + + + + diff --git a/app/src/main/java/code/name/monkey/retromusic/Constants.kt b/app/src/main/java/code/name/monkey/retromusic/Constants.kt index 2ff0d03fe..36805ba24 100644 --- a/app/src/main/java/code/name/monkey/retromusic/Constants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/Constants.kt @@ -19,49 +19,6 @@ import android.provider.MediaStore object Constants { - @JvmField - val RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic" - @JvmField - val MUSIC_PACKAGE_NAME = "com.android.music" - @JvmField - val ACTION_TOGGLE_PAUSE = "$RETRO_MUSIC_PACKAGE_NAME.togglepause" - @JvmField - val ACTION_PLAY = "$RETRO_MUSIC_PACKAGE_NAME.play" - @JvmField - val ACTION_PLAY_PLAYLIST = "$RETRO_MUSIC_PACKAGE_NAME.play.playlist" - @JvmField - val ACTION_PAUSE = "$RETRO_MUSIC_PACKAGE_NAME.pause" - @JvmField - val ACTION_STOP = "$RETRO_MUSIC_PACKAGE_NAME.stop" - @JvmField - val ACTION_SKIP = "$RETRO_MUSIC_PACKAGE_NAME.skip" - @JvmField - val ACTION_REWIND = "$RETRO_MUSIC_PACKAGE_NAME.rewind" - @JvmField - val ACTION_QUIT = "$RETRO_MUSIC_PACKAGE_NAME.quitservice" - @JvmField - val ACTION_PENDING_QUIT = "$RETRO_MUSIC_PACKAGE_NAME.pendingquitservice" - @JvmField - val INTENT_EXTRA_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist" - @JvmField - val INTENT_EXTRA_SHUFFLE_MODE = "$RETRO_MUSIC_PACKAGE_NAME.intentextra.shufflemode" - @JvmField - val APP_WIDGET_UPDATE = "$RETRO_MUSIC_PACKAGE_NAME.appwidgetupdate" - @JvmField - val EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name" - - @JvmField - val META_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.metachanged" - @JvmField - val QUEUE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.queuechanged" - @JvmField - val PLAY_STATE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.playstatechanged" - @JvmField - val REPEAT_MODE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.repeatmodechanged" - @JvmField - val SHUFFLE_MODE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.shufflemodechanged" - @JvmField - val MEDIA_STORE_CHANGED = "$RETRO_MUSIC_PACKAGE_NAME.mediastorechanged" const val RATE_ON_GOOGLE_PLAY = "https://play.google.com/store/apps/details?id=code.name.monkey.retromusic" const val TRANSLATE = "http://monkeycodeapp.oneskyapp.com/collaboration/project?id=238534" const val GITHUB_PROJECT = "https://github.com/h4h13/RetroMusicPlayer" @@ -76,7 +33,6 @@ object Constants { const val BASE_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1" + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''" - @JvmField val BASE_PROJECTION = arrayOf(BaseColumns._ID, // 0 MediaStore.Audio.AudioColumns.TITLE, // 1 MediaStore.Audio.AudioColumns.TRACK, // 2 diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt index 85d5e379d..6b276273b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/AlbumDetailsActivity.kt @@ -77,6 +77,9 @@ class AlbumDetailsActivity : AbsSlidingMusicPanelActivity(), AlbumDetailsContrac ActivityCompat.postponeEnterTransition(this) + + artistImage = findViewById(R.id.artistImage) + val albumId = intent.getIntExtra(EXTRA_ALBUM_ID, -1) albumDetailsPresenter = AlbumDetailsPresenter(this, albumId) albumDetailsPresenter.subscribe() @@ -84,7 +87,6 @@ class AlbumDetailsActivity : AbsSlidingMusicPanelActivity(), AlbumDetailsContrac setupRecyclerView() setupToolbarMarginHeight() - artistImage = findViewById(R.id.artistImage) artistImage.setOnClickListener { val artistPairs = arrayOf>(Pair.create(image, resources.getString(R.string.transition_artist_image))) NavigationUtil.goToArtist(this, album.artistId, *artistPairs) @@ -189,9 +191,7 @@ class AlbumDetailsActivity : AbsSlidingMusicPanelActivity(), AlbumDetailsContrac private lateinit var artistImage: ImageView private fun loadMoreFrom(album: Album) { - disposable.add(ArtistLoader.getArtist(this, album.artistId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + disposable.add(ArtistLoader.getArtistFlowable(this, album.artistId) .map { GlideApp.with(this@AlbumDetailsActivity) .asBitmapPalette() diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt index 6ebdb0e72..1e4f79fc4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/MainActivity.kt @@ -6,14 +6,12 @@ import android.content.pm.PackageManager import android.os.Bundle import android.provider.MediaStore import android.util.Log -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.base.AbsSlidingMusicPanelActivity -import code.name.monkey.retromusic.dialogs.OptionsSheetDialogFragment import code.name.monkey.retromusic.fragments.mainactivity.LibraryFragment import code.name.monkey.retromusic.fragments.mainactivity.folders.FoldersFragment import code.name.monkey.retromusic.fragments.mainactivity.home.BannerHomeFragment @@ -121,7 +119,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), SharedPreferences.OnSharedP PreferenceUtil.getInstance().unregisterOnSharedPreferenceChangedListener(this) } - private fun setCurrentFragment(fragment: Fragment, b: Boolean) { + private fun setCurrentFragment(fragment: Fragment, b: Boolean = false) { val trans = supportFragmentManager.beginTransaction() trans.replace(R.id.fragment_container, fragment, null) if (b) { @@ -162,7 +160,7 @@ class MainActivity : AbsSlidingMusicPanelActivity(), SharedPreferences.OnSharedP val id = parseIdFromIntent(intent, "playlistId", "playlist").toInt() if (id >= 0) { val position = intent.getIntExtra("position", 0) - val songs = ArrayList(PlaylistSongsLoader.getPlaylistSongList(this, id).blockingFirst()) + val songs = ArrayList(PlaylistSongsLoader.getPlaylistSongList(this, id)) MusicPlayerRemote.openQueue(songs, position, true) handled = true } @@ -170,14 +168,14 @@ class MainActivity : AbsSlidingMusicPanelActivity(), SharedPreferences.OnSharedP val id = parseIdFromIntent(intent, "albumId", "album").toInt() if (id >= 0) { val position = intent.getIntExtra("position", 0) - MusicPlayerRemote.openQueue(AlbumLoader.getAlbum(this, id).blockingFirst().songs!!, position, true) + MusicPlayerRemote.openQueue(AlbumLoader.getAlbum(this, id).songs!!, position, true) handled = true } } else if (MediaStore.Audio.Artists.CONTENT_TYPE == mimeType) { val id = parseIdFromIntent(intent, "artistId", "artist").toInt() if (id >= 0) { val position = intent.getIntExtra("position", 0) - MusicPlayerRemote.openQueue(ArtistLoader.getArtist(this, id).blockingFirst().songs, position, true) + MusicPlayerRemote.openQueue(ArtistLoader.getArtist(this, id).songs, position, true) handled = true } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/PlaylistDetailActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/PlaylistDetailActivity.kt index 4c31048d6..fb63bfede 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/PlaylistDetailActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/PlaylistDetailActivity.kt @@ -161,7 +161,7 @@ class PlaylistDetailActivity : AbsSlidingMusicPanelActivity(), CabHolder, Playli // Playlist renamed val playlistName = PlaylistsUtil.getNameForPlaylist(this, playlist!!.id.toLong()) if (playlistName != playlist!!.name) { - playlist = PlaylistLoader.getPlaylist(this, playlist!!.id).blockingFirst() + playlist = PlaylistLoader.getPlaylist(this, playlist!!.id) setToolbarTitle(playlist!!.name) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/SearchActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/SearchActivity.kt index f6586f1de..099b57c10 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/SearchActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/SearchActivity.kt @@ -170,7 +170,7 @@ class SearchActivity : AbsMusicServiceActivity(), OnQueryTextListener, SearchCon } - override fun showData(list: ArrayList) { + override fun showData(list: MutableList) { searchAdapter!!.swapDataSet(list) } diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt index 893c65e8d..305f6da70 100644 --- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt @@ -4,15 +4,10 @@ import android.Manifest import android.content.* import android.os.Bundle import android.os.IBinder -import code.name.monkey.retromusic.Constants.MEDIA_STORE_CHANGED -import code.name.monkey.retromusic.Constants.META_CHANGED -import code.name.monkey.retromusic.Constants.PLAY_STATE_CHANGED -import code.name.monkey.retromusic.Constants.QUEUE_CHANGED -import code.name.monkey.retromusic.Constants.REPEAT_MODE_CHANGED -import code.name.monkey.retromusic.Constants.SHUFFLE_MODE_CHANGED import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.interfaces.MusicServiceEventListener +import code.name.monkey.retromusic.service.MusicService.* import java.lang.ref.WeakReference import java.util.* diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt index 21b0ec61c..27b9e30d0 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/AlbumTagEditorActivity.kt @@ -195,7 +195,7 @@ class AlbumTagEditorActivity : AbsTagEditorActivity(), TextWatcher { } override fun getSongPaths(): List { - val songs = AlbumLoader.getAlbum(this, id).blockingFirst().songs + val songs = AlbumLoader.getAlbum(this, id).songs val paths = ArrayList(songs!!.size) for (song in songs) { paths.add(song.data) diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt index 7b14d8503..474d163f2 100755 --- a/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/activities/tageditor/SongTagEditorActivity.kt @@ -108,7 +108,7 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher { override fun getSongPaths(): List { val paths = ArrayList(1) - paths.add(SongLoader.getSong(this, id).blockingFirst().data) + paths.add(SongLoader.getSong(this, id).data) return paths } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt index 53a157aee..9da4a7ea2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/HomeAdapter.kt @@ -105,7 +105,7 @@ class HomeAdapter(private val activity: AppCompatActivity, private var homes: Li } private inner class PlaylistViewHolder(view: View) : AbsHomeViewItem(view) { fun bindView(home: Home) { - val songs = PlaylistSongsLoader.getPlaylistSongList(activity, home.arrayList[0] as Playlist).blockingFirst() + val songs = PlaylistSongsLoader.getPlaylistSongList(activity, home.arrayList[0] as Playlist) recyclerView.apply { val songAdapter = SongAdapter(activity, songs, R.layout.item_album_card, false, null) layoutManager = GridLayoutManager(activity, 1, GridLayoutManager.HORIZONTAL, false) diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt index 64a8f3d8b..44c2e0dc5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/SearchAdapter.kt @@ -23,7 +23,7 @@ import java.util.* class SearchAdapter(private val activity: AppCompatActivity, private var dataSet: List?) : RecyclerView.Adapter() { - fun swapDataSet(dataSet: ArrayList) { + fun swapDataSet(dataSet: MutableList) { this.dataSet = dataSet notifyDataSetChanged() } diff --git a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt index effce02f3..21b19f1b1 100755 --- a/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/adapter/playlist/PlaylistAdapter.kt @@ -149,10 +149,9 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL val songs = ArrayList() for (playlist in playlists) { if (playlist is AbsCustomPlaylist) { - songs.addAll(playlist.getSongs(activity).blockingFirst()) + songs.addAll(playlist.getSongs(activity)) } else { - songs - .addAll(PlaylistSongsLoader.getPlaylistSongList(activity, playlist.id).blockingFirst()) + songs.addAll(PlaylistSongsLoader.getPlaylistSongList(activity, playlist.id)) } } return songs @@ -161,9 +160,9 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL private fun getSongs(playlist: Playlist): ArrayList { val songs = ArrayList() if (playlist is AbsSmartPlaylist) { - songs.addAll(playlist.getSongs(activity).blockingFirst()) + songs.addAll(playlist.getSongs(activity)) } else { - songs.addAll(PlaylistSongsLoader.getPlaylistSongList(activity, playlist.id).blockingFirst()) + songs.addAll(PlaylistSongsLoader.getPlaylistSongList(activity, playlist.id)) } return songs } diff --git a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt index 9be37e022..9c8b1b709 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appshortcuts/AppShortcutLauncherActivity.kt @@ -17,9 +17,7 @@ package code.name.monkey.retromusic.appshortcuts import android.app.Activity import android.content.Intent import android.os.Bundle -import code.name.monkey.retromusic.Constants.ACTION_PLAY_PLAYLIST -import code.name.monkey.retromusic.Constants.INTENT_EXTRA_PLAYLIST -import code.name.monkey.retromusic.Constants.INTENT_EXTRA_SHUFFLE_MODE +import code.name.monkey.retromusic.activities.SearchActivity import code.name.monkey.retromusic.appshortcuts.shortcuttype.LastAddedShortcutType import code.name.monkey.retromusic.appshortcuts.shortcuttype.SearchShortCutType import code.name.monkey.retromusic.appshortcuts.shortcuttype.ShuffleAllShortcutType @@ -29,7 +27,7 @@ import code.name.monkey.retromusic.model.smartplaylist.LastAddedPlaylist import code.name.monkey.retromusic.model.smartplaylist.MyTopTracksPlaylist import code.name.monkey.retromusic.model.smartplaylist.ShuffleAllPlaylist import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.SearchActivity +import code.name.monkey.retromusic.service.MusicService.* class AppShortcutLauncherActivity : Activity() { diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt index e42f8b886..668137621 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetBig.kt @@ -24,14 +24,14 @@ import android.text.TextUtils import android.view.View import android.widget.RemoteViews import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition @@ -149,15 +149,15 @@ class AppWidgetBig : BaseAppWidget() { views.setOnClickPendingIntent(R.id.clickable_area, pendingIntent) // Previous track - pendingIntent = buildPendingIntent(context, Constants.ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) views.setOnClickPendingIntent(R.id.button_prev, pendingIntent) // Play and pause - pendingIntent = buildPendingIntent(context, Constants.ACTION_TOGGLE_PAUSE, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName) views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent) // Next track - pendingIntent = buildPendingIntent(context, Constants.ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) views.setOnClickPendingIntent(R.id.button_next, pendingIntent) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt index 37e8d42e1..8aaad9af2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetCard.kt @@ -24,15 +24,15 @@ import android.text.TextUtils import android.view.View import android.widget.RemoteViews import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition @@ -50,9 +50,9 @@ class AppWidgetCard : BaseAppWidget() { appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE) appWidgetView.setImageViewResource(R.id.image, R.drawable.default_album_art) - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.Companion.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.Companion.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.Companion.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) linkButtons(context, appWidgetView) pushUpdate(context, appWidgetIds, appWidgetView) @@ -128,10 +128,10 @@ class AppWidgetCard : BaseAppWidget() { val playPauseRest = if (isPlaying) R.drawable.ic_pause_white_24dp else R.drawable.ic_play_arrow_white_32dp - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, playPauseRest, color)!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(service, playPauseRest, color)!!, 1f)) // Set prev/next button drawables - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, color)!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, color)!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, color)!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, color)!!, 1f)) val image = getAlbumArtDrawable(service.resources, bitmap) val roundedBitmap = BaseAppWidget.Companion.createRoundedBitmap(image, imageSize, imageSize, cardRadius, 0f, cardRadius, 0f) @@ -159,15 +159,15 @@ class AppWidgetCard : BaseAppWidget() { views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) // Previous track - pendingIntent = buildPendingIntent(context, Constants.ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) views.setOnClickPendingIntent(R.id.button_prev, pendingIntent) // Play and pause - pendingIntent = buildPendingIntent(context, Constants.ACTION_TOGGLE_PAUSE, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName) views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent) // Next track - pendingIntent = buildPendingIntent(context, Constants.ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) views.setOnClickPendingIntent(R.id.button_next, pendingIntent) } diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt index 90b1bc9ba..46a3a01f3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetClassic.kt @@ -33,6 +33,7 @@ import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition @@ -49,9 +50,9 @@ class AppWidgetClassic : BaseAppWidget() { appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE) appWidgetView.setImageViewResource(R.id.image, R.drawable.default_album_art) - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) linkButtons(context, appWidgetView) pushUpdate(context, appWidgetIds, appWidgetView) @@ -116,11 +117,11 @@ class AppWidgetClassic : BaseAppWidget() { else R.drawable.ic_play_arrow_white_32dp appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, - BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, playPauseRes, color)!!, 1f)) + createBitmap(RetroUtil.getTintedVectorDrawable(service, playPauseRes, color)!!, 1f)) // Set prev/next button drawables - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, color)!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, color)!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, color)!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, color)!!, 1f)) val image = getAlbumArtDrawable(service.resources, bitmap) val roundedBitmap = BaseAppWidget.createRoundedBitmap(image, imageSize, imageSize, @@ -149,15 +150,15 @@ class AppWidgetClassic : BaseAppWidget() { views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) // Previous track - pendingIntent = buildPendingIntent(context, Constants.ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) views.setOnClickPendingIntent(R.id.button_prev, pendingIntent) // Play and pause - pendingIntent = buildPendingIntent(context, Constants.ACTION_TOGGLE_PAUSE, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName) views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent) // Next track - pendingIntent = buildPendingIntent(context, Constants.ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) views.setOnClickPendingIntent(R.id.button_next, pendingIntent) } diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt index 92e01abb2..68a342bf4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetSmall.kt @@ -24,15 +24,15 @@ import android.text.TextUtils import android.view.View import android.widget.RemoteViews import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.RetroUtil import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition @@ -49,9 +49,9 @@ class AppWidgetSmall : BaseAppWidget() { appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE) appWidgetView.setImageViewResource(R.id.image, R.drawable.default_album_art) - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, MaterialValueHelper.getSecondaryTextColor(context, true))!!, 1f)) linkButtons(context, appWidgetView) pushUpdate(context, appWidgetIds, appWidgetView) @@ -153,15 +153,15 @@ class AppWidgetSmall : BaseAppWidget() { views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) // Previous track - pendingIntent = buildPendingIntent(context, Constants.ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) views.setOnClickPendingIntent(R.id.button_prev, pendingIntent) // Play and pause - pendingIntent = buildPendingIntent(context, Constants.ACTION_TOGGLE_PAUSE, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName) views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent) // Next track - pendingIntent = buildPendingIntent(context, Constants.ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) views.setOnClickPendingIntent(R.id.button_next, pendingIntent) } diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt index 0acee199a..73fc3b345 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/AppWidgetText.kt @@ -23,20 +23,20 @@ import android.view.View import android.widget.RemoteViews import androidx.core.content.ContextCompat import code.name.monkey.retromusic.App.Companion.context -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.RetroUtil class AppWidgetText : BaseAppWidget() { override fun defaultAppWidget(context: Context, appWidgetIds: IntArray) { val appWidgetView = RemoteViews(context.packageName, R.layout.app_widget_text) - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_32dp, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) appWidgetView.setTextColor(R.id.title, ContextCompat.getColor(context, R.color.md_white_1000)) appWidgetView.setTextColor(R.id.text, ContextCompat.getColor(context, R.color.md_white_1000)) @@ -61,15 +61,15 @@ class AppWidgetText : BaseAppWidget() { views.setOnClickPendingIntent(R.id.media_titles, pendingIntent) // Previous track - pendingIntent = buildPendingIntent(context, Constants.ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) views.setOnClickPendingIntent(R.id.button_prev, pendingIntent) // Play and pause - pendingIntent = buildPendingIntent(context, Constants.ACTION_TOGGLE_PAUSE, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName) views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent) // Next track - pendingIntent = buildPendingIntent(context, Constants.ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) views.setOnClickPendingIntent(R.id.button_next, pendingIntent) } @@ -95,9 +95,9 @@ class AppWidgetText : BaseAppWidget() { R.drawable.ic_pause_white_24dp else R.drawable.ic_play_arrow_white_32dp - appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, playPauseRes, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) - appWidgetView.setImageViewBitmap(R.id.button_next, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) - appWidgetView.setImageViewBitmap(R.id.button_prev, BaseAppWidget.createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000)), 1f)) + appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(RetroUtil.getTintedVectorDrawable(context, playPauseRes, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) + appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(RetroUtil.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, ContextCompat.getColor(context, R.color.md_white_1000))!!, 1f)) diff --git a/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt b/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt index 20c5304a0..53fd79e91 100644 --- a/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt +++ b/app/src/main/java/code/name/monkey/retromusic/appwidgets/base/BaseAppWidget.kt @@ -29,10 +29,10 @@ import android.text.TextUtils import android.widget.RemoteViews import androidx.core.content.ContextCompat import code.name.monkey.retromusic.App -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.service.MusicService.* abstract class BaseAppWidget : AppWidgetProvider() { @@ -42,8 +42,8 @@ abstract class BaseAppWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { defaultAppWidget(context, appWidgetIds) - val updateIntent = Intent(Constants.APP_WIDGET_UPDATE) - updateIntent.putExtra(Constants.EXTRA_APP_WIDGET_NAME, NAME) + val updateIntent = Intent(APP_WIDGET_UPDATE) + updateIntent.putExtra(EXTRA_APP_WIDGET_NAME, NAME) updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) context.sendBroadcast(updateIntent) @@ -54,7 +54,7 @@ abstract class BaseAppWidget : AppWidgetProvider() { */ fun notifyChange(service: MusicService, what: String) { if (hasInstances(service)) { - if (Constants.META_CHANGED == what || Constants.PLAY_STATE_CHANGED == what) { + if (META_CHANGED == what || PLAY_STATE_CHANGED == what) { performUpdate(service, null) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java new file mode 100644 index 000000000..35858c76c --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMediaIDHelper.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.auto; + +import androidx.annotation.NonNull; + +/** + * Created by Beesham Sarendranauth (Beesham) + */ +public class AutoMediaIDHelper { + + // Media IDs used on browseable items of MediaBrowser + public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY_ROOT__"; + public static final String MEDIA_ID_ROOT = "__ROOT__"; + public static final String MEDIA_ID_MUSICS_BY_SEARCH = "__BY_SEARCH__"; // TODO + public static final String MEDIA_ID_MUSICS_BY_HISTORY = "__BY_HISTORY__"; + public static final String MEDIA_ID_MUSICS_BY_TOP_TRACKS = "__BY_TOP_TRACKS__"; + public static final String MEDIA_ID_MUSICS_BY_PLAYLIST = "__BY_PLAYLIST__"; + public static final String MEDIA_ID_MUSICS_BY_ALBUM = "__BY_ALBUM__"; + public static final String MEDIA_ID_MUSICS_BY_ARTIST = "__BY_ARTIST__"; + public static final String MEDIA_ID_MUSICS_BY_SHUFFLE = "__BY_SHUFFLE__"; + public static final String MEDIA_ID_MUSICS_BY_QUEUE = "__BY_QUEUE__"; + + private static final String CATEGORY_SEPARATOR = "__/__"; + private static final String LEAF_SEPARATOR = "__|__"; + + /** + * Create a String value that represents a playable or a browsable media. + *

+ * Encode the media browseable categories, if any, and the unique music ID, if any, + * into a single String mediaID. + *

+ * MediaIDs are of the form __/____|__, to make it + * easy to find the category (like genre) that a music was selected from, so we + * can correctly build the playing queue. This is specially useful when + * one music can appear in more than one list, like "by genre -> genre_1" + * and "by artist -> artist_1". + * + * @param mediaID Unique ID for playable items, or null for browseable items. + * @param categories Hierarchy of categories representing this item's browsing parents. + * @return A hierarchy-aware media ID. + */ + public static String createMediaID(String mediaID, String... categories) { + StringBuilder sb = new StringBuilder(); + if (categories != null) { + for (int i = 0; i < categories.length; i++) { + if (!isValidCategory(categories[i])) { + throw new IllegalArgumentException("Invalid category: " + categories[i]); + } + sb.append(categories[i]); + if (i < categories.length - 1) { + sb.append(CATEGORY_SEPARATOR); + } + } + } + if (mediaID != null) { + sb.append(LEAF_SEPARATOR).append(mediaID); + } + return sb.toString(); + } + + public static String extractCategory(@NonNull String mediaID) { + int pos = mediaID.indexOf(LEAF_SEPARATOR); + if (pos >= 0) { + return mediaID.substring(0, pos); + } + return mediaID; + } + + public static String extractMusicID(@NonNull String mediaID) { + int pos = mediaID.indexOf(LEAF_SEPARATOR); + if (pos >= 0) { + return mediaID.substring(pos + LEAF_SEPARATOR.length()); + } + return null; + } + + public static boolean isBrowseable(@NonNull String mediaID) { + return !mediaID.contains(LEAF_SEPARATOR); + } + + private static boolean isValidCategory(String category) { + return category == null || + (!category.contains(CATEGORY_SEPARATOR) && !category.contains(LEAF_SEPARATOR)); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.java b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.java new file mode 100644 index 000000000..13900e949 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/auto/AutoMusicProvider.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.auto; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.AsyncTask; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaDescriptionCompat; + +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import code.name.monkey.appthemehelper.ThemeStore; +import code.name.monkey.retromusic.R; +import code.name.monkey.retromusic.loaders.AlbumLoader; +import code.name.monkey.retromusic.loaders.ArtistLoader; +import code.name.monkey.retromusic.loaders.PlaylistLoader; +import code.name.monkey.retromusic.loaders.TopAndRecentlyPlayedTracksLoader; +import code.name.monkey.retromusic.model.Album; +import code.name.monkey.retromusic.model.Artist; +import code.name.monkey.retromusic.model.Playlist; +import code.name.monkey.retromusic.model.Song; +import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore; +import code.name.monkey.retromusic.service.MusicService; +import code.name.monkey.retromusic.util.ImageUtil; + +/** + * Created by Beesham Sarendranauth (Beesham) + */ +public class AutoMusicProvider { + + private static final String TAG = AutoMusicProvider.class.getName(); + + private static final String BASE_URI = "androidauto://phonograph"; + private static final int PATH_SEGMENT_ID = 0; + private static final int PATH_SEGMENT_TITLE = 1; + private static final int PATH_SEGMENT_ARTIST = 2; + private static final int PATH_SEGMENT_ALBUM_ID = 3; + + private WeakReference mMusicService; + + // Categorized caches for music data + private ConcurrentMap mMusicListByHistory; + private ConcurrentMap mMusicListByTopTracks; + private ConcurrentMap mMusicListByPlaylist; + private ConcurrentMap mMusicListByAlbum; + private ConcurrentMap mMusicListByArtist; + + private Uri defaultAlbumArtUri; + + private Context mContext; + private volatile State mCurrentState = State.NON_INITIALIZED; + + public AutoMusicProvider(Context context) { + mContext = context; + + mMusicListByHistory = new ConcurrentSkipListMap<>(); + mMusicListByTopTracks = new ConcurrentSkipListMap<>(); + mMusicListByPlaylist = new ConcurrentSkipListMap<>(); + mMusicListByAlbum = new ConcurrentSkipListMap<>(); + mMusicListByArtist = new ConcurrentSkipListMap<>(); + + defaultAlbumArtUri = Uri.parse("android.resource://" + + mContext.getPackageName() + "/drawable/" + + mContext.getResources().getResourceEntryName(R.drawable.default_album_art)); + } + + public Iterable getHistory() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + return mMusicListByHistory.values(); + } + + public Iterable getTopTracks() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + return mMusicListByTopTracks.values(); + } + + public Iterable getPlaylists() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + return mMusicListByPlaylist.values(); + } + + public Iterable getAlbums() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + return mMusicListByAlbum.values(); + } + + public Iterable getArtists() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + return mMusicListByArtist.values(); + } + + public Iterable getQueue() { + if (mCurrentState != State.INITIALIZED) { + return Collections.emptyList(); + } + + // Re-built every time since the queue updates often + ConcurrentMap queueList = new ConcurrentSkipListMap<>(); + + if (mContext != null) { + final List songs = MusicPlaybackQueueStore.getInstance(mContext).getSavedOriginalPlayingQueue(); + for (int i = 0; i < songs.size(); i++) { + final Song s = songs.get(i); + Uri.Builder topTracksData = Uri.parse(BASE_URI).buildUpon(); + topTracksData.appendPath(String.valueOf(s.getId())) + .appendPath(s.getTitle()) + .appendPath(s.getArtistName()) + .appendPath(String.valueOf(s.getAlbumId())); + queueList.putIfAbsent(i, topTracksData.build()); + } + } + + return queueList.values(); + } + + public boolean isInitialized() { + return mCurrentState == State.INITIALIZED; + } + + /** + * Get the list of music tracks from a server and caches the track information + * for future reference, keying tracks by musicId + */ + public void retrieveMediaAsync(final Callback callback) { + if (mCurrentState == State.INITIALIZED) { + if (callback != null) { + // Nothing to do, execute callback immediately + callback.onMusicCatalogReady(true); + } + return; + } + + // Asynchronously load the music catalog in a separate thread + new AsyncTask() { + @Override + protected State doInBackground(Void... params) { + retrieveMedia(); + return mCurrentState; + } + + @Override + protected void onPostExecute(State current) { + if (callback != null) { + callback.onMusicCatalogReady(current == State.INITIALIZED); + } + } + }.execute(); + } + + private synchronized void buildListsByHistory() { + ConcurrentMap newMusicListByHistory = new ConcurrentSkipListMap<>(); + + final List songs = TopAndRecentlyPlayedTracksLoader.INSTANCE.getRecentlyPlayedTracks(mContext); + for (int i = 0; i < songs.size(); i++) { + final Song s = songs.get(i); + Uri.Builder topTracksData = Uri.parse(BASE_URI).buildUpon(); + topTracksData.appendPath(String.valueOf(s.getId())) + .appendPath(s.getTitle()) + .appendPath(s.getArtistName()) + .appendPath(String.valueOf(s.getAlbumId())); + newMusicListByHistory.putIfAbsent(i, topTracksData.build()); + } + + mMusicListByHistory = newMusicListByHistory; + } + + private synchronized void buildListsByTopTracks() { + ConcurrentMap newMusicListByTopTracks = new ConcurrentHashMap<>(); + + final List songs = TopAndRecentlyPlayedTracksLoader.INSTANCE.getTopTracks(mContext); + for (int i = 0; i < songs.size(); i++) { + final Song s = songs.get(i); + Uri.Builder topTracksData = Uri.parse(BASE_URI).buildUpon(); + topTracksData.appendPath(String.valueOf(s.getId())) + .appendPath(s.getTitle()) + .appendPath(s.getArtistName()) + .appendPath(String.valueOf(s.getAlbumId())); + newMusicListByTopTracks.putIfAbsent(i, topTracksData.build()); + } + + mMusicListByTopTracks = newMusicListByTopTracks; + } + + private synchronized void buildListsByPlaylist() { + ConcurrentMap newMusicListByPlaylist = new ConcurrentSkipListMap<>(); + + final List playlists = PlaylistLoader.INSTANCE.getAllPlaylists(mContext); + for (int i = 0; i < playlists.size(); i++) { + final Playlist p = playlists.get(i); + Uri.Builder playlistData = Uri.parse(BASE_URI).buildUpon(); + playlistData.appendPath(String.valueOf(p.id)) + .appendPath(p.name); + newMusicListByPlaylist.putIfAbsent(i, playlistData.build()); + } + + mMusicListByPlaylist = newMusicListByPlaylist; + } + + private synchronized void buildListsByAlbum() { + ConcurrentMap newMusicListByAlbum = new ConcurrentSkipListMap<>(); + + final List albums = AlbumLoader.INSTANCE.getAllAlbums(mContext); + for (int i = 0; i < albums.size(); i++) { + final Album a = albums.get(i); + Uri.Builder albumData = Uri.parse(BASE_URI).buildUpon(); + albumData.appendPath(String.valueOf(a.getId())) + .appendPath(a.getTitle()) + .appendPath(a.getArtistName()) + .appendPath(String.valueOf(a.getId())); + newMusicListByAlbum.putIfAbsent(i, albumData.build()); + } + + mMusicListByAlbum = newMusicListByAlbum; + } + + private synchronized void buildListsByArtist() { + ConcurrentMap newMusicListByArtist = new ConcurrentSkipListMap<>(); + + final List artists = ArtistLoader.INSTANCE.getAllArtists(mContext); + for (int i = 0; i < artists.size(); i++) { + final Artist a = artists.get(i); + Uri.Builder artistData = Uri.parse(BASE_URI).buildUpon(); + artistData.appendPath(String.valueOf(a.getId())) + .appendPath(a.getName()) + .appendPath(a.getName()); + newMusicListByArtist.putIfAbsent(i, artistData.build()); + } + + mMusicListByArtist = newMusicListByArtist; + } + + private synchronized void retrieveMedia() { + try { + if (mCurrentState == State.NON_INITIALIZED) { + mCurrentState = State.INITIALIZING; + + buildListsByHistory(); + buildListsByTopTracks(); + buildListsByPlaylist(); + buildListsByAlbum(); + buildListsByArtist(); + mCurrentState = State.INITIALIZED; + } + } finally { + if (mCurrentState != State.INITIALIZED) { + // Something bad happened, so we reset state to NON_INITIALIZED to allow + // retries (eg if the network connection is temporary unavailable) + mCurrentState = State.NON_INITIALIZED; + } + } + } + + public List getChildren(String mediaId, Resources resources) { + List mediaItems = new ArrayList<>(); + + if (!AutoMediaIDHelper.isBrowseable(mediaId)) { + return mediaItems; + } + + switch (mediaId) { + case AutoMediaIDHelper.MEDIA_ID_ROOT: + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY, resources.getString(R.string.history_label), R.drawable.ic_access_time_white_24dp)); + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS, resources.getString(R.string.top_tracks_label), R.drawable.ic_trending_up_white_24dp)); + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST, resources.getString(R.string.playlists_label), R.drawable.ic_queue_music_white_24dp)); + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM, resources.getString(R.string.albums_label), R.drawable.ic_album_white_24dp)); + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST, resources.getString(R.string.artists_label), R.drawable.ic_artist_white_24dp)); + mediaItems.add(createPlayableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE, resources.getString(R.string.action_shuffle_all), R.drawable.ic_shuffle_white_24dp, null)); + mediaItems.add(createBrowsableMediaItem(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE, resources.getString(R.string.queue_label), R.drawable.ic_playlist_play_white_24dp)); + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY: + for (final Uri uri : getHistory()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_TITLE), uri.getPathSegments().get(PATH_SEGMENT_ARTIST))); + } + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS: + for (final Uri uri : getTopTracks()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_TITLE), uri.getPathSegments().get(PATH_SEGMENT_ARTIST))); + } + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST: + for (final Uri uri : getPlaylists()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_TITLE), null)); + } + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM: + for (final Uri uri : getAlbums()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_TITLE), uri.getPathSegments().get(PATH_SEGMENT_ARTIST))); + } + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST: + for (final Uri uri : getArtists()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_ARTIST), null)); + } + break; + + case AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE: + // TODO: auto scroll to current track, indicate that it's playing + for (final Uri uri : getQueue()) { + mediaItems.add(createPlayableMediaItem(mediaId, uri, uri.getPathSegments().get(PATH_SEGMENT_TITLE), uri.getPathSegments().get(PATH_SEGMENT_ARTIST))); + } + break; + } + + return mediaItems; + } + + private MediaBrowserCompat.MediaItem createBrowsableMediaItem(String mediaId, String title, int iconDrawableId) { + MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(mediaId) + .setTitle(title) + .setIconBitmap(ImageUtil.createBitmap(ImageUtil.getTintedVectorDrawable(mContext, iconDrawableId, ThemeStore.Companion.textColorSecondary(mContext)))); + + return new MediaBrowserCompat.MediaItem(builder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); + } + + private MediaBrowserCompat.MediaItem createPlayableMediaItem(String mediaId, Uri musicSelection, + String title, @Nullable String subtitle) { + return createPlayableMediaItem(mediaId, musicSelection, title, subtitle, null, null); + } + + private MediaBrowserCompat.MediaItem createPlayableMediaItem(String mediaId, Uri musicSelection, + String title, @Nullable String subtitle, + @Nullable Bitmap albumArt, @Nullable Resources resources) { + MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(AutoMediaIDHelper.createMediaID(musicSelection.getPathSegments().get(PATH_SEGMENT_ID), mediaId)) + .setTitle(title); + + if (subtitle != null) { + builder.setSubtitle(subtitle); + } + + if (resources != null) { + if (albumArt != null) { + builder.setIconBitmap(albumArt); + } else { + builder.setIconUri(defaultAlbumArtUri); + } + } + + return new MediaBrowserCompat.MediaItem(builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); + } + + private MediaBrowserCompat.MediaItem createPlayableMediaItem(String mediaId, String title, int iconDrawableId, + @Nullable String subtitle) { + MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder() + .setMediaId(mediaId) + .setTitle(title) + .setIconBitmap(ImageUtil.createBitmap(ImageUtil.getTintedVectorDrawable(mContext, iconDrawableId, ThemeStore.Companion.textColorSecondary(mContext)))); + + if (subtitle != null) { + builder.setSubtitle(subtitle); + } + + return new MediaBrowserCompat.MediaItem(builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); + } + + private enum State { + NON_INITIALIZED, INITIALIZING, INITIALIZED + } + + public interface Callback { + void onMusicCatalogReady(boolean success); + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt index f65154eee..999f27e7a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/AddToPlaylistDialog.kt @@ -24,7 +24,6 @@ import code.name.monkey.retromusic.util.PlaylistsUtil import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.list.listItems -import kotlinx.android.synthetic.main.activity_user_info.* class AddToPlaylistDialog : DialogFragment() { @@ -33,7 +32,7 @@ class AddToPlaylistDialog : DialogFragment() { savedInstanceState: Bundle? ): Dialog { val cntx = requireContext() - val playlists = PlaylistLoader.getAllPlaylists(cntx).blockingFirst() + val playlists = PlaylistLoader.getAllPlaylists(cntx) val playlistNames: MutableList = mutableListOf() playlistNames.add(cntx.resources.getString(R.string.action_new_playlist)) for (p in playlists) { diff --git a/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt b/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt index 9691d60ab..c3121071b 100755 --- a/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt +++ b/app/src/main/java/code/name/monkey/retromusic/dialogs/SleepTimerDialog.kt @@ -18,7 +18,6 @@ import android.app.AlarmManager import android.app.Dialog import android.app.PendingIntent import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.os.CountDownTimer @@ -29,10 +28,11 @@ import android.widget.TextView import android.widget.Toast import androidx.fragment.app.DialogFragment import code.name.monkey.appthemehelper.ThemeStore -import code.name.monkey.retromusic.Constants import code.name.monkey.retromusic.R import code.name.monkey.retromusic.helper.MusicPlayerRemote import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.service.MusicService.ACTION_PENDING_QUIT +import code.name.monkey.retromusic.service.MusicService.ACTION_QUIT import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.ViewUtil @@ -153,8 +153,8 @@ class SleepTimerDialog : DialogFragment() { private fun makeTimerIntent(): Intent { val intent = Intent(activity, MusicService::class.java) return if (shouldFinishLastSong.isChecked) { - intent.setAction(Constants.ACTION_PENDING_QUIT) - } else intent.setAction(Constants.ACTION_QUIT) + intent.setAction(ACTION_PENDING_QUIT) + } else intent.setAction(ACTION_QUIT) } diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java index bdb03c396..76422277b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/folders/FoldersFragment.java @@ -572,7 +572,7 @@ public class FoldersFragment extends AbsMainActivityFragment implements return null; } - return FileUtil.matchFilesWithMediaStore(context, files).blockingFirst(); + return FileUtil.matchFilesWithMediaStore(context, files); } catch (Exception e) { e.printStackTrace(); cancel(false); diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/home/BannerHomeFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/home/BannerHomeFragment.kt index 532f49a08..fe65fea15 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/home/BannerHomeFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/mainactivity/home/BannerHomeFragment.kt @@ -105,7 +105,7 @@ class BannerHomeFragment : AbsMainActivityFragment(), MainActivityFragmentCallba } actionShuffle.setOnClickListener { - MusicPlayerRemote.openAndShuffleQueue(SongLoader.getAllSongs(mainActivity).blockingFirst(), true) + MusicPlayerRemote.openAndShuffleQueue(SongLoader.getAllSongs(mainActivity) , true) } history.setOnClickListener { diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt index 0e42bcb85..62a7512bc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/full/FullPlayerFragment.kt @@ -213,7 +213,7 @@ class FullPlayerFragment : AbsPlayerFragment(), PlayerAlbumCoverFragment.Callbac private val compositeDisposable = CompositeDisposable() private fun updateArtistImage() { - compositeDisposable.addAll(ArtistLoader.getArtist(context!!, MusicPlayerRemote.currentSong.artistId) + compositeDisposable.addAll(ArtistLoader.getArtistFlowable(context!!, MusicPlayerRemote.currentSong.artistId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.kt b/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java similarity index 72% rename from app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.kt rename to app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java index ce76a4671..9865cad24 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/M3UConstants.java @@ -12,13 +12,11 @@ * See the GNU General Public License for more details. */ -package code.name.monkey.retromusic.helper +package code.name.monkey.retromusic.helper; -interface M3UConstants { - companion object { - val EXTENSION = "m3u" - val HEADER = "#EXTM3U" - val ENTRY = "#EXTINF:" - val DURATION_SEPARATOR = "," - } +public interface M3UConstants { + String EXTENSION = "m3u"; + String HEADER = "#EXTM3U"; + String ENTRY = "#EXTINF:"; + String DURATION_SEPARATOR = ","; } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.java b/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.java index 05fd8f815..cdf121039 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.java +++ b/app/src/main/java/code/name/monkey/retromusic/helper/M3UWriter.java @@ -16,47 +16,43 @@ package code.name.monkey.retromusic.helper; import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; -import code.name.monkey.retromusic.loaders.PlaylistSongsLoader; -import code.name.monkey.retromusic.model.AbsCustomPlaylist; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Song; -import io.reactivex.Observable; public class M3UWriter implements M3UConstants { - public static Observable write(Context context, File dir, Playlist playlist) throws IOException { + @Nullable + public static File write(@NonNull Context context, + @NonNull File dir, + @NonNull Playlist playlist) throws IOException { if (!dir.exists()) //noinspection ResultOfMethodCallIgnored dir.mkdirs(); - File file = new File(dir, playlist.name.concat("." + M3UConstants.Companion.getEXTENSION())); - - ArrayList songs; - if (playlist instanceof AbsCustomPlaylist) { - songs = ((AbsCustomPlaylist) playlist).getSongs(context).blockingFirst(); - } else { - songs = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(context, playlist.id).blockingFirst(); - } + File file = new File(dir, playlist.name.concat("." + EXTENSION)); + ArrayList songs = playlist.getSongs(context); if (songs.size() > 0) { BufferedWriter bw = new BufferedWriter(new FileWriter(file)); - bw.write(M3UConstants.Companion.getHEADER()); + bw.write(HEADER); for (Song song : songs) { bw.newLine(); - bw.write(M3UConstants.Companion.getENTRY() + song.getDuration() + M3UConstants.Companion.getDURATION_SEPARATOR() + song.getArtistName() + " - " + song.getTitle()); + bw.write(ENTRY + song.getDuration() + DURATION_SEPARATOR + song.getArtistName() + " - " + song.getTitle()); bw.newLine(); bw.write(song.getData()); } bw.close(); } - - return Observable.just(file); + return file; } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt index a707ac1c3..b0c0a51b5 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicPlayerRemote.kt @@ -30,7 +30,6 @@ import code.name.monkey.retromusic.loaders.SongLoader import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.util.PreferenceUtil -import io.reactivex.schedulers.Schedulers import java.io.File import java.util.* @@ -366,6 +365,7 @@ object MusicPlayerRemote { fun playFromUri(uri: Uri) { if (musicService != null) { + var songs: ArrayList? = null if (uri.scheme != null && uri.authority != null) { if (uri.scheme == ContentResolver.SCHEME_CONTENT) { @@ -376,23 +376,18 @@ object MusicPlayerRemote { songId = uri.lastPathSegment } if (songId != null) { - /* songs = SongLoader.getSongs(SongLoader.makeSongCursor( - musicService, - MediaStore.Audio.AudioColumns._ID + "=?", - new String[]{songId} - ));*/ songs = SongLoader.getSongs(SongLoader.makeSongCursor( musicService!!, MediaStore.Audio.AudioColumns._ID + "=?", - arrayOf(songId))) - .subscribeOn(Schedulers.io()).blockingFirst() + arrayOf(songId) + )) } } } if (songs == null) { var songFile: File? = null if (uri.authority != null && uri.authority == "com.android.externalstorage.documents") { - songFile = File(Environment.getExternalStorageDirectory(), uri.path!!.split(":".toRegex(), 2).toTypedArray()[1]) + songFile = File(Environment.getExternalStorageDirectory(), uri.path?.split(":".toRegex(), 2)?.get(1)) } if (songFile == null) { val path = getFilePathFromUri(musicService!!, uri) @@ -403,8 +398,11 @@ object MusicPlayerRemote { songFile = File(uri.path) } if (songFile != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(musicService!!, MediaStore.Audio.AudioColumns.DATA + "=?", arrayOf(songFile.absolutePath) - )).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor( + musicService!!, + MediaStore.Audio.AudioColumns.DATA + "=?", + arrayOf(songFile.absolutePath) + )) } } if (songs != null && songs.isNotEmpty()) { diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt index 9a6137eea..88ae6f23a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/SearchQueryHelper.kt @@ -38,52 +38,52 @@ object SearchQueryHelper { var songs = ArrayList() if (artistName != null && albumName != null && titleName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION + AND + ALBUM_SELECTION + AND + TITLE_SELECTION, arrayOf(artistName.toLowerCase(), albumName.toLowerCase(), titleName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION + AND + ALBUM_SELECTION + AND + TITLE_SELECTION, arrayOf(artistName.toLowerCase(), albumName.toLowerCase(), titleName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } if (artistName != null && titleName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION + AND + TITLE_SELECTION, arrayOf(artistName.toLowerCase(), titleName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION + AND + TITLE_SELECTION, arrayOf(artistName.toLowerCase(), titleName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } if (albumName != null && titleName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION + AND + TITLE_SELECTION, arrayOf(albumName.toLowerCase(), titleName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION + AND + TITLE_SELECTION, arrayOf(albumName.toLowerCase(), titleName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } if (artistName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION, arrayOf(artistName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION, arrayOf(artistName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } if (albumName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION, arrayOf(albumName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION, arrayOf(albumName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } if (titleName != null) { - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, TITLE_SELECTION, arrayOf(titleName.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, TITLE_SELECTION, arrayOf(titleName.toLowerCase()))) } - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION, arrayOf(query.toLowerCase()))).blockingFirst() + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ARTIST_SELECTION, arrayOf(query.toLowerCase()))) - if (!songs.isEmpty()) { + if (songs.isNotEmpty()) { return songs } - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION, arrayOf(query.toLowerCase()))).blockingFirst() - if (!songs.isEmpty()) { + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, ALBUM_SELECTION, arrayOf(query.toLowerCase()))) + if (songs.isNotEmpty()) { return songs } - songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, TITLE_SELECTION, arrayOf(query.toLowerCase()))).blockingFirst() - return if (!songs.isEmpty()) { + songs = SongLoader.getSongs(SongLoader.makeSongCursor(context, TITLE_SELECTION, arrayOf(query.toLowerCase()))) + return if (songs.isNotEmpty()) { songs } else ArrayList() } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt index 88f6616f0..506104994 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/GenreMenuHelper.kt @@ -51,6 +51,6 @@ object GenreMenuHelper { } private fun getGenreSongs(activity: Activity, genre: Genre): ArrayList { - return GenreLoader.getSongs(activity, genre.id).blockingFirst() + return GenreLoader.getSongs(activity, genre.id) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt index cc2063add..c776d8373 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/menu/PlaylistMenuHelper.kt @@ -77,20 +77,18 @@ object PlaylistMenuHelper { private fun getPlaylistSongs(activity: Activity, playlist: Playlist): ArrayList { - val songs: ArrayList - if (playlist is AbsCustomPlaylist) { - songs = playlist.getSongs(activity).blockingFirst() + return if (playlist is AbsCustomPlaylist) { + playlist.getSongs(activity) } else { - songs = PlaylistSongsLoader.getPlaylistSongList(activity, playlist).blockingFirst() + PlaylistSongsLoader.getPlaylistSongList(activity, playlist) } - return songs } private class SavePlaylistAsyncTask internal constructor(context: Context) : WeakContextAsyncTask(context) { override fun doInBackground(vararg params: Playlist): String { return String.format(App.instance.applicationContext.getString(R.string - .saved_playlist_to), PlaylistsUtil.savePlaylist(App.instance.applicationContext, params[0]).blockingFirst()) + .saved_playlist_to), PlaylistsUtil.savePlaylist(App.instance.applicationContext, params[0])) } override fun onPostExecute(string: String) { diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/AlbumLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/AlbumLoader.kt index e1949a083..de47266c8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/AlbumLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/AlbumLoader.kt @@ -21,89 +21,164 @@ import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil import io.reactivex.Observable import java.util.* +import kotlin.collections.ArrayList + /** * Created by hemanths on 11/08/17. */ -open class AlbumLoader { - companion object { - fun getAllAlbums(context: Context): Observable> { - val songs = SongLoader.getSongs(SongLoader.makeSongCursor( - context, null, null, - getSongLoaderSortOrder()) - ) +object AlbumLoader { + fun getAllAlbumsFlowable( + context: Context + ): Observable> { + val songs = SongLoader.getSongsFlowable(SongLoader.makeSongCursor( + context, null, null, + getSongLoaderSortOrder()) + ) - return splitIntoAlbums(songs) - } + return splitIntoAlbumsFlowable(songs) + } - fun getAlbums(context: Context, - query: String): Observable> { - val songs = SongLoader.getSongs(SongLoader.makeSongCursor( - context, - AudioColumns.ALBUM + " LIKE ?", - arrayOf("%$query%"), - getSongLoaderSortOrder()) - ) - return splitIntoAlbums(songs) - } + fun getAlbumsFlowable(context: Context, query: String): Observable> { + val songs = SongLoader.getSongsFlowable(SongLoader.makeSongCursor( + context, + AudioColumns.ALBUM + " LIKE ?", + arrayOf("%$query%"), + getSongLoaderSortOrder()) + ) + return splitIntoAlbumsFlowable(songs) + } - fun getAlbum(context: Context, albumId: Int): Observable { - return Observable.create { e -> - val songs = SongLoader.getSongs(SongLoader - .makeSongCursor(context, AudioColumns.ALBUM_ID + "=?", - arrayOf(albumId.toString()), getSongLoaderSortOrder())) - songs.subscribe { songs1 -> - e.onNext(Album(songs1)) - e.onComplete() - } - } - } + fun getAlbums( + context: Context, + query: String + ): ArrayList { + val songs = SongLoader.getSongs(SongLoader.makeSongCursor( + context, + AudioColumns.ALBUM + " LIKE ?", + arrayOf("%$query%"), + getSongLoaderSortOrder()) + ) + return splitIntoAlbums(songs) + } - fun splitIntoAlbums( - songs: Observable>?): Observable> { - return Observable.create { e -> - val albums = ArrayList() - songs?.subscribe { songs1 -> - for (song in songs1) { - getOrCreateAlbum(albums, song.albumId).subscribe { album -> album.songs!!.add(song) } - } - } - e.onNext(albums) + fun getAlbumFlowable( + context: Context, + albumId: Int + ): Observable { + return Observable.create { e -> + val songs = SongLoader.getSongsFlowable(SongLoader.makeSongCursor(context, AudioColumns.ALBUM_ID + "=?", arrayOf(albumId.toString()), getSongLoaderSortOrder())) + songs.subscribe { songs1 -> + e.onNext(Album(songs1)) e.onComplete() } } - - fun splitIntoAlbums(songs: ArrayList?): ArrayList { - val albums = ArrayList() - if (songs != null) { - for (song in songs) { - getOrCreateAlbum(albums, song.albumId).subscribe { album -> album.songs!!.add(song) } - } - } - return albums - } - - private fun getOrCreateAlbum(albums: ArrayList, albumId: Int): Observable { - return Observable.create { e -> - for (album in albums) { - if (!album.songs!!.isEmpty() && album.songs[0].albumId == albumId) { - e.onNext(album) - e.onComplete() - return@create - } - } - val album = Album() - albums.add(album) - e.onNext(album) - e.onComplete() - } - } - - private fun getSongLoaderSortOrder(): String { - return PreferenceUtil.getInstance().albumSortOrder + ", " + - //PreferenceUtil.getInstance().getAlbumSongSortOrder() + "," + - PreferenceUtil.getInstance().albumDetailSongSortOrder - } + } + + fun getAlbum( + context: Context, + albumId: Int + ): Album { + val songs = SongLoader.getSongs( + SongLoader.makeSongCursor( + context, + AudioColumns.ALBUM_ID + "=?", + arrayOf(albumId.toString()), + getSongLoaderSortOrder())) + val album = Album(songs) + sortSongsByTrackNumber(album) + return album + } + + fun splitIntoAlbumsFlowable( + songs: Observable>? + ): Observable> { + return Observable.create { e -> + val albums = ArrayList() + songs?.subscribe { songs1 -> + for (song in songs1) { + getOrCreateAlbumFlowable(albums, song.albumId).subscribe { album -> + album.songs!!.add(song) + } + } + } + for (album in albums) { + sortSongsByTrackNumber(album) + } + e.onNext(albums) + e.onComplete() + } + } + + fun getAllAlbums( + context: Context + ): ArrayList { + val songs = SongLoader.getSongs(SongLoader.makeSongCursor( + context, null, null, + getSongLoaderSortOrder()) + ) + + return splitIntoAlbums(songs) + } + + fun splitIntoAlbums( + songs: ArrayList? + ): ArrayList { + val albums = ArrayList() + if (songs != null) { + for (song in songs) { + getOrCreateAlbum(albums, song.albumId).songs?.add(song) + } + } + for (album in albums) { + sortSongsByTrackNumber(album) + } + return albums + } + + private fun getOrCreateAlbumFlowable( + albums: ArrayList, + albumId: Int + ): Observable { + return Observable.create { e -> + for (album in albums) { + if (!album.songs!!.isEmpty() && album.songs[0].albumId == albumId) { + e.onNext(album) + e.onComplete() + return@create + } + } + val album = Album() + albums.add(album) + e.onNext(album) + e.onComplete() + } + } + + private fun getOrCreateAlbum( + albums: ArrayList, + albumId: Int + ): Album { + for (album in albums) { + if (album.songs!!.isNotEmpty() && album.songs[0].albumId == albumId) { + return album + } + } + val album = Album() + albums.add(album) + return album + } + + private fun sortSongsByTrackNumber( + album: Album + ) { + album.songs?.sortWith(Comparator { o1, o2 -> o1.trackNumber - o2.trackNumber }) + } + + private fun getSongLoaderSortOrder(): String { + return PreferenceUtil.getInstance().albumSortOrder + ", " + + //PreferenceUtil.getInstance().getAlbumSongSortOrder() + "," + + PreferenceUtil.getInstance().albumDetailSongSortOrder } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/ArtistLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/ArtistLoader.kt index ea54d9f39..d1610663e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/ArtistLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/ArtistLoader.kt @@ -20,7 +20,7 @@ import code.name.monkey.retromusic.model.Album import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.util.PreferenceUtil import io.reactivex.Observable -import kotlin.collections.ArrayList + object ArtistLoader { private fun getSongLoaderSortOrder(): String { @@ -30,36 +30,32 @@ object ArtistLoader { PreferenceUtil.getInstance().artistDetailSongSortOrder } - fun getArtist(context: Context, artistId: Int): Observable { + fun getAllArtistsFlowable( + context: Context + ): Observable> { return Observable.create { e -> - SongLoader.getSongs(SongLoader.makeSongCursor(context, AudioColumns.ARTIST_ID + "=?", - arrayOf(artistId.toString()), - getSongLoaderSortOrder())) - .subscribe { songs -> - val artist = Artist(AlbumLoader.splitIntoAlbums(songs)) - e.onNext(artist) - e.onComplete() - } + SongLoader.getSongsFlowable(SongLoader.makeSongCursor( + context, null, null, + getSongLoaderSortOrder()) + ).subscribe { songs -> + e.onNext(splitIntoArtists(AlbumLoader.splitIntoAlbums(songs))) + e.onComplete() + } } } - fun getAllArtists(context: Context): Observable> { - return Observable.create { e -> - SongLoader - .getSongs(SongLoader.makeSongCursor( - context, null, null, - getSongLoaderSortOrder()) - ).subscribe { songs -> - e.onNext(splitIntoArtists(AlbumLoader.splitIntoAlbums(songs))) - e.onComplete() - } - } - + fun getAllArtists(context: Context): ArrayList { + val songs = SongLoader.getSongs(SongLoader.makeSongCursor( + context, + null, null, + getSongLoaderSortOrder()) + ) + return splitIntoArtists(AlbumLoader.splitIntoAlbums(songs)) } - fun getArtists(context: Context, query: String): Observable> { + fun getArtistsFlowable(context: Context, query: String): Observable> { return Observable.create { e -> - SongLoader.getSongs(SongLoader.makeSongCursor( + SongLoader.getSongsFlowable(SongLoader.makeSongCursor( context, AudioColumns.ARTIST + " LIKE ?", arrayOf("%$query%"), @@ -71,6 +67,16 @@ object ArtistLoader { } } + fun getArtists(context: Context, query: String): ArrayList { + val songs = SongLoader.getSongs(SongLoader.makeSongCursor( + context, + AudioColumns.ARTIST + " LIKE ?", + arrayOf("%$query%"), + getSongLoaderSortOrder()) + ) + return splitIntoArtists(AlbumLoader.splitIntoAlbums(songs)) + } + fun splitIntoArtists(albums: ArrayList?): ArrayList { val artists = ArrayList() if (albums != null) { @@ -83,7 +89,7 @@ object ArtistLoader { private fun getOrCreateArtist(artists: ArrayList, artistId: Int): Artist { for (artist in artists) { - if (!artist.albums!!.isEmpty() && !artist.albums[0].songs!!.isEmpty() && artist.albums[0].songs!![0].artistId == artistId) { + if (artist.albums!!.isNotEmpty() && artist.albums[0].songs!!.isNotEmpty() && artist.albums[0].songs!![0].artistId == artistId) { return artist } } @@ -106,4 +112,27 @@ object ArtistLoader { } } } + + fun getArtistFlowable(context: Context, artistId: Int): Observable { + return Observable.create { e -> + SongLoader.getSongsFlowable(SongLoader.makeSongCursor(context, AudioColumns.ARTIST_ID + "=?", + arrayOf(artistId.toString()), + getSongLoaderSortOrder())) + .subscribe { songs -> + val artist = Artist(AlbumLoader.splitIntoAlbums(songs)) + e.onNext(artist) + e.onComplete() + } + } + } + + fun getArtist(context: Context, artistId: Int): Artist { + val songs = SongLoader.getSongs(SongLoader.makeSongCursor( + context, + AudioColumns.ARTIST_ID + "=?", + arrayOf(artistId.toString()), + getSongLoaderSortOrder()) + ) + return Artist(AlbumLoader.splitIntoAlbums(songs)) + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/GenreLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/GenreLoader.kt index 571132279..e06d0a92d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/GenreLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/GenreLoader.kt @@ -27,13 +27,27 @@ import code.name.monkey.retromusic.util.PreferenceUtil import io.reactivex.Observable import java.util.* + object GenreLoader { - fun getAllGenres(context: Context): Observable> { + fun getAllGenresFlowable(context: Context): Observable> { + return getGenresFromCursorFlowable(context, makeGenreCursor(context)) + } + + fun getAllGenres(context: Context): ArrayList { return getGenresFromCursor(context, makeGenreCursor(context)) } - fun getSongs(context: Context, genreId: Int): Observable> { + fun getSongsFlowable(context: Context, genreId: Int): Observable> { + // The genres table only stores songs that have a genre specified, + // so we need to get songs without a genre a different way. + return if (genreId == -1) { + getSongsWithNoGenreFlowable(context) + } else SongLoader.getSongsFlowable(makeGenreSongCursor(context, genreId)) + + } + + fun getSongs(context: Context, genreId: Int): ArrayList { // The genres table only stores songs that have a genre specified, // so we need to get songs without a genre a different way. return if (genreId == -1) { @@ -45,12 +59,18 @@ object GenreLoader { private fun getGenreFromCursor(context: Context, cursor: Cursor): Genre { val id = cursor.getInt(0) val name = cursor.getString(1) - val songCount = getSongs(context, id).blockingFirst().size + val songCount = getSongs(context, id).size return Genre(id, name, songCount) } - private fun getSongsWithNoGenre(context: Context): Observable> { + private fun getSongsWithNoGenreFlowable(context: Context): Observable> { + val selection = BaseColumns._ID + " NOT IN " + + "(SELECT " + Genres.Members.AUDIO_ID + " FROM audio_genres_map)" + return SongLoader.getSongsFlowable(SongLoader.makeSongCursor(context, selection, null)) + } + + private fun getSongsWithNoGenre(context: Context): ArrayList { val selection = BaseColumns._ID + " NOT IN " + "(SELECT " + Genres.Members.AUDIO_ID + " FROM audio_genres_map)" return SongLoader.getSongs(SongLoader.makeSongCursor(context, selection, null)) @@ -92,7 +112,7 @@ object GenreLoader { } - private fun getGenresFromCursor(context: Context, cursor: Cursor?): Observable> { + private fun getGenresFromCursorFlowable(context: Context, cursor: Cursor?): Observable> { return Observable.create { e -> val genres = ArrayList() if (cursor != null) { @@ -120,6 +140,31 @@ object GenreLoader { } } + private fun getGenresFromCursor(context: Context, cursor: Cursor?): ArrayList { + val genres = arrayListOf() + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + val genre = getGenreFromCursor(context, cursor) + if (genre.songCount > 0) { + genres.add(genre) + } else { + // try to remove the empty genre from the media store + try { + context.contentResolver.delete(Genres.EXTERNAL_CONTENT_URI, Genres._ID + " == " + genre.id, null) + } catch (e: Exception) { + e.printStackTrace() + // nothing we can do then + } + + } + } while (cursor.moveToNext()) + } + cursor.close() + } + return genres + } + private fun makeGenreCursor(context: Context): Cursor? { val projection = arrayOf(Genres._ID, Genres.NAME) diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/HomeLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/HomeLoader.kt index 9aa45b95a..7b0fd672c 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/HomeLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/HomeLoader.kt @@ -25,47 +25,5 @@ import io.reactivex.Observable object HomeLoader { - fun getRecentAndTopThings(context: Context): Observable> { - val objects = ArrayList() - return Observable.create { e -> - - HistoryPlaylist(context).getSongs(context).subscribe { songs -> - if (!songs.isEmpty()) { - objects.add(HistoryPlaylist(context)) - } - } - LastAddedPlaylist(context).getSongs(context).subscribe { songs -> - if (!songs.isEmpty()) { - objects.add(LastAddedPlaylist(context)) - } - } - MyTopTracksPlaylist(context).getSongs(context).subscribe { songs -> - if (!songs.isEmpty()) { - objects.add(MyTopTracksPlaylist(context)) - } - } - - e.onNext(objects) - e.onComplete() - } - } - - fun getHomeLoader(context: Context): Observable> { - val playlists = ArrayList() - PlaylistLoader.getAllPlaylists(context) - .subscribe { playlists1 -> - if (playlists1.size > 0) { - for (playlist in playlists1) { - PlaylistSongsLoader.getPlaylistSongList(context, playlist) - .subscribe { songs -> - if (songs.size > 0) { - playlists.add(playlist) - } - } - } - } - } - return Observable.just(playlists) - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/LastAddedSongsLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/LastAddedSongsLoader.kt index 0fbe79ee0..e9cd9821f 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/LastAddedSongsLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/LastAddedSongsLoader.kt @@ -22,8 +22,8 @@ import code.name.monkey.retromusic.model.Artist import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.util.PreferenceUtil import io.reactivex.Observable -import io.reactivex.annotations.NonNull import java.util.* +import kotlin.collections.ArrayList /** * Created by hemanths on 16/08/17. @@ -31,12 +31,16 @@ import java.util.* object LastAddedSongsLoader { - @NonNull - fun getLastAddedSongs(@NonNull context: Context): Observable> { + + fun getLastAddedSongsFlowable(context: Context): Observable> { + return SongLoader.getSongsFlowable(makeLastAddedCursor(context)) + } + + fun getLastAddedSongs(context: Context): ArrayList { return SongLoader.getSongs(makeLastAddedCursor(context)) } - private fun makeLastAddedCursor(@NonNull context: Context): Cursor? { + private fun makeLastAddedCursor(context: Context): Cursor? { val cutoff = PreferenceUtil.getInstance().lastAddedCutoff return SongLoader.makeSongCursor( @@ -46,13 +50,22 @@ object LastAddedSongsLoader { MediaStore.Audio.Media.DATE_ADDED + " DESC") } - @NonNull - fun getLastAddedAlbums(@NonNull context: Context): Observable> { + + fun getLastAddedAlbumsFlowable(context: Context): Observable> { + return AlbumLoader.splitIntoAlbumsFlowable(getLastAddedSongsFlowable(context)) + } + + + fun getLastAddedAlbums(context: Context): ArrayList { return AlbumLoader.splitIntoAlbums(getLastAddedSongs(context)) } - @NonNull - fun getLastAddedArtists(@NonNull context: Context): Observable> { + + fun getLastAddedArtistsFlowable(context: Context): Observable> { + return ArtistLoader.splitIntoArtists(getLastAddedAlbumsFlowable(context)) + } + + fun getLastAddedArtists(context: Context): ArrayList { return ArtistLoader.splitIntoArtists(getLastAddedAlbums(context)) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistLoader.kt index df8da8b75..f89cb6b46 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistLoader.kt @@ -19,31 +19,20 @@ import android.database.Cursor import android.provider.BaseColumns import android.provider.MediaStore import android.provider.MediaStore.Audio.PlaylistsColumns -import code.name.monkey.retromusic.R import code.name.monkey.retromusic.model.Playlist import io.reactivex.Observable import java.util.* + /** * Created by hemanths on 16/08/17. */ object PlaylistLoader { - private fun makePlaylistCursor(context: Context, selection: String?, values: Array?): Cursor? { - return try { - context.contentResolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, - arrayOf( - /* 0 */ - BaseColumns._ID, - /* 1 */ - PlaylistsColumns.NAME), selection, values, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER) - } catch (e: SecurityException) { - null - } - } - - private fun getPlaylist(cursor: Cursor?): Observable { + private fun getPlaylistFlowable( + cursor: Cursor? + ): Observable { return Observable.create { e -> var playlist = Playlist() @@ -55,11 +44,35 @@ object PlaylistLoader { e.onNext(playlist) e.onComplete() } - - } - fun getPlaylist(context: Context, playlistName: String): Observable { + fun getPlaylist( + cursor: Cursor? + ): Playlist { + var playlist = Playlist() + + if (cursor != null && cursor.moveToFirst()) { + playlist = getPlaylistFromCursorImpl(cursor) + } + cursor?.close() + return playlist + } + + fun getPlaylistFlowable( + context: Context, + playlistName: String + ): Observable { + return getPlaylistFlowable(makePlaylistCursor( + context, + PlaylistsColumns.NAME + "=?", + arrayOf(playlistName) + )) + } + + fun getPlaylist( + context: Context, + playlistName: String + ): Playlist { return getPlaylist(makePlaylistCursor( context, PlaylistsColumns.NAME + "=?", @@ -67,23 +80,31 @@ object PlaylistLoader { )) } - fun getPlaylist(context: Context, playlistId: Int): Observable { - return getPlaylist(makePlaylistCursor( + fun getPlaylistFlowable( + context: Context, + playlistId: Int + ): Observable { + return getPlaylistFlowable(makePlaylistCursor( context, BaseColumns._ID + "=?", arrayOf(playlistId.toString()) )) } - private fun getPlaylistFromCursorImpl(cursor: Cursor): Playlist { - - val id = cursor.getInt(0) - val name = cursor.getString(1) - return Playlist(id, name) + fun getAllPlaylistsFlowoable( + context: Context + ): Observable> { + return getAllPlaylistsFlowable(makePlaylistCursor(context, null, null)) } + fun getFavoritePlaylistFlowable(context: Context): Observable> { + return getAllPlaylistsFlowable(makePlaylistCursor( + context, + PlaylistsColumns.NAME + "=?", + arrayOf(context.getString(code.name.monkey.retromusic.R.string.favorites)))) + } - private fun getAllPlaylists(cursor: Cursor?): Observable> { + private fun getAllPlaylistsFlowable(cursor: Cursor?): Observable> { return Observable.create { e -> val playlists = ArrayList() @@ -99,15 +120,27 @@ object PlaylistLoader { } } - fun getAllPlaylists(context: Context): Observable> { + fun getAllPlaylists(context: Context): ArrayList { return getAllPlaylists(makePlaylistCursor(context, null, null)) } - fun getFavoritePlaylist(context: Context): Observable> { + fun getFavoritePlaylist(context: Context): ArrayList { return getAllPlaylists(makePlaylistCursor( context, PlaylistsColumns.NAME + "=?", - arrayOf(context.getString(R.string.favorites)))) + arrayOf(context.getString(code.name.monkey.retromusic.R.string.favorites)))) + } + + fun getAllPlaylists(cursor: Cursor?): ArrayList { + val playlists = ArrayList() + + if (cursor != null && cursor.moveToFirst()) { + do { + playlists.add(getPlaylistFromCursorImpl(cursor)) + } while (cursor.moveToNext()) + } + cursor?.close() + return playlists } fun deletePlaylists(context: Context, playlistId: Long) { @@ -118,4 +151,42 @@ object PlaylistLoader { localStringBuilder.append(")") context.contentResolver.delete(localUri, localStringBuilder.toString(), null) } + + private fun makePlaylistCursor( + context: Context, + selection: String?, + values: Array? + ): Cursor? { + try { + return context.contentResolver.query( + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, + arrayOf(BaseColumns._ID, /* 0 */ + PlaylistsColumns.NAME), /* 1 */ + selection, + values, + MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER) + } catch (e: SecurityException) { + return null + } + } + + fun getPlaylist( + context: Context, + playlistId: Int + ): Playlist { + return getPlaylist(makePlaylistCursor( + context, + BaseColumns._ID + "=?", + arrayOf(playlistId.toString()) + )) + } + + private fun getPlaylistFromCursorImpl( + cursor: Cursor + ): Playlist { + val id = cursor.getInt(0) + val name = cursor.getString(1) + return Playlist(id, name) + } + } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistSongsLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistSongsLoader.kt index 1f1af046f..0a09d9e0d 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistSongsLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/PlaylistSongsLoader.kt @@ -24,23 +24,33 @@ import code.name.monkey.retromusic.model.Playlist import code.name.monkey.retromusic.model.PlaylistSong import code.name.monkey.retromusic.model.Song import io.reactivex.Observable -import io.reactivex.annotations.NonNull import java.util.* + /** * Created by hemanths on 16/08/17. */ object PlaylistSongsLoader { - @NonNull - fun getPlaylistSongList(@NonNull context: Context, playlist: Playlist): Observable> { + fun getPlaylistSongListFlowable( + context: Context, + playlist: Playlist + ): Observable> { + return (playlist as? AbsCustomPlaylist)?.getSongsFlowable(context) + ?: getPlaylistSongListFlowable(context, playlist.id) + } + + fun getPlaylistSongList( + context: Context, + playlist: Playlist + ): ArrayList { return (playlist as? AbsCustomPlaylist)?.getSongs(context) ?: getPlaylistSongList(context, playlist.id) } - @NonNull - fun getPlaylistSongList(@NonNull context: Context, playlistId: Int): Observable> { + + fun getPlaylistSongListFlowable(context: Context, playlistId: Int): Observable> { return Observable.create { e -> val songs = ArrayList() val cursor = makePlaylistSongCursor(context, playlistId) @@ -56,8 +66,21 @@ object PlaylistSongsLoader { } } - @NonNull - private fun getPlaylistSongFromCursorImpl(@NonNull cursor: Cursor, playlistId: Int): PlaylistSong { + fun getPlaylistSongList(context: Context, playlistId: Int): ArrayList { + val songs = arrayListOf() + val cursor = makePlaylistSongCursor(context, playlistId) + + if (cursor != null && cursor.moveToFirst()) { + do { + songs.add(getPlaylistSongFromCursorImpl(cursor, playlistId)) + } while (cursor.moveToNext()) + } + cursor?.close() + return songs + } + + + private fun getPlaylistSongFromCursorImpl(cursor: Cursor, playlistId: Int): PlaylistSong { val id = cursor.getInt(0) val title = cursor.getString(1) val trackNumber = cursor.getInt(2) @@ -75,7 +98,7 @@ object PlaylistSongsLoader { return PlaylistSong(id, title, trackNumber, year, duration, data, dateModified, albumId, albumName, artistId, artistName, playlistId, idInPlaylist, composer) } - private fun makePlaylistSongCursor(@NonNull context: Context, playlistId: Int): Cursor? { + private fun makePlaylistSongCursor(context: Context, playlistId: Int): Cursor? { try { return context.contentResolver.query( MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId.toLong()), diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/SearchLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/SearchLoader.kt index ed6d44413..ef7395bfc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/SearchLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/SearchLoader.kt @@ -15,42 +15,33 @@ package code.name.monkey.retromusic.loaders import android.content.Context -import android.text.TextUtils import code.name.monkey.retromusic.R import io.reactivex.Observable -import java.util.* + object SearchLoader { - - fun searchAll(context: Context, query: String?): Observable> { - val results = ArrayList() - return Observable.create { e -> - if (!TextUtils.isEmpty(query)) { - SongLoader.getSongs(context, query!!) - .subscribe { songs -> - if (!songs.isEmpty()) { - results.add(context.resources.getString(R.string.songs)) - results.addAll(songs) - } - } - - ArtistLoader.getArtists(context, query) - .subscribe { artists -> - if (!artists.isEmpty()) { - results.add(context.resources.getString(R.string.artists)) - results.addAll(artists) - } - } - AlbumLoader.getAlbums(context, query) - .subscribe { albums -> - if (!albums.isEmpty()) { - results.add(context.resources.getString(R.string.albums)) - results.addAll(albums) - } - } + fun searchAll(context: Context, query: String?): MutableList { + val results = mutableListOf() + query?.let { + val songs = SongLoader.getSongs(context, it) + if (songs.isNotEmpty()) { + results.add(context.resources.getString(R.string.songs)) + results.addAll(songs) + } + + val artists = ArtistLoader.getArtists(context, it) + if (artists.isNotEmpty()) { + results.add(context.resources.getString(R.string.artists)) + results.addAll(artists) + } + + val albums = AlbumLoader.getAlbums(context, it) + if (albums.isNotEmpty()) { + results.add(context.resources.getString(R.string.albums)) + results.addAll(albums) } - e.onNext(results) - e.onComplete() } + return results + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt index 6649fb3b8..2f1b1f502 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/SongLoader.kt @@ -27,24 +27,29 @@ import code.name.monkey.retromusic.util.PreferenceUtil import io.reactivex.Observable import java.util.* + /** * Created by hemanths on 10/08/17. */ - object SongLoader { + fun getAllSongsFlowable( + context: Context + ): Observable> { + val cursor = makeSongCursor(context, null, null) + return getSongsFlowable(cursor) + } - fun getAllSongs(context: Context): Observable> { + fun getAllSongs( + context: Context + ): ArrayList { val cursor = makeSongCursor(context, null, null) return getSongs(cursor) } - fun getSongs(context: Context, query: String): Observable> { - val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%")) - return getSongs(cursor) - } - - fun getSongs(cursor: Cursor?): Observable> { + fun getSongsFlowable( + cursor: Cursor? + ): Observable> { return Observable.create { e -> val songs = ArrayList() if (cursor != null && cursor.moveToFirst()) { @@ -59,7 +64,96 @@ object SongLoader { } } - private fun getSongFromCursorImpl(cursor: Cursor): Song { + fun getSongs( + cursor: Cursor? + ): ArrayList { + val songs = arrayListOf() + if (cursor != null && cursor.moveToFirst()) { + do { + songs.add(getSongFromCursorImpl(cursor)) + } while (cursor.moveToNext()) + } + + cursor?.close() + return songs + } + + fun getSongsFlowable( + context: Context, + query: String + ): Observable> { + val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%")) + return getSongsFlowable(cursor) + } + + fun getSongs( + context: Context, + query: String + ): ArrayList { + val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%")) + return getSongs(cursor) + } + + + private fun getSongFlowable( + cursor: Cursor? + ): Observable { + return Observable.create { e -> + val song: Song = if (cursor != null && cursor.moveToFirst()) { + getSongFromCursorImpl(cursor) + } else { + Song.emptySong + } + cursor?.close() + e.onNext(song) + e.onComplete() + } + } + + fun getSong( + cursor: Cursor? + ): Song { + val song: Song + if (cursor != null && cursor.moveToFirst()) { + song = getSongFromCursorImpl(cursor) + } else { + song = Song.emptySong + } + cursor?.close() + return song + } + + fun getSongFlowable( + context: Context, + queryId: Int + ): Observable { + val cursor = makeSongCursor(context, AudioColumns._ID + "=?", + arrayOf(queryId.toString())) + return getSongFlowable(cursor) + } + + fun getSong(context: Context, queryId: Int): Song { + val cursor = makeSongCursor(context, AudioColumns._ID + "=?", arrayOf(queryId.toString())) + return getSong(cursor) + } + + fun suggestSongs( + context: Context + ): Observable> { + return SongLoader.getAllSongsFlowable(context) + .flatMap { + val list = ArrayList() + ShuffleHelper.makeShuffleList(it, -1) + if (it.size >= 7) { + list.addAll(it.subList(0, 7)) + } + return@flatMap Observable.just(list) + } + } + + private fun getSongFromCursorImpl( + cursor: Cursor + ): Song { val id = cursor.getInt(0) val title = cursor.getString(1) val trackNumber = cursor.getInt(2) @@ -78,7 +172,12 @@ object SongLoader { } @JvmOverloads - fun makeSongCursor(context: Context, selection: String?, selectionValues: Array?, sortOrder: String = PreferenceUtil.getInstance().songSortOrder): Cursor? { + fun makeSongCursor( + context: Context, + selection: String?, + selectionValues: Array?, + sortOrder: String = PreferenceUtil.getInstance().songSortOrder + ): Cursor? { var selectionFinal = selection var selectionValuesFinal = selectionValues selectionFinal = if (selection != null && selection.trim { it <= ' ' } != "") { @@ -103,7 +202,10 @@ object SongLoader { } - private fun generateBlacklistSelection(selection: String?, pathCount: Int): String { + private fun generateBlacklistSelection( + selection: String?, + pathCount: Int + ): String { val newSelection = StringBuilder( if (selection != null && selection.trim { it <= ' ' } != "") "$selection AND " else "") newSelection.append(AudioColumns.DATA + " NOT LIKE ?") @@ -113,8 +215,10 @@ object SongLoader { return newSelection.toString() } - private fun addBlacklistSelectionValues(selectionValues: Array?, - paths: ArrayList): Array? { + private fun addBlacklistSelectionValues( + selectionValues: Array?, + paths: ArrayList + ): Array? { var selectionValuesFinal = selectionValues if (selectionValuesFinal == null) { selectionValuesFinal = emptyArray() @@ -128,35 +232,4 @@ object SongLoader { } return newSelectionValues } - - private fun getSong(cursor: Cursor?): Observable { - return Observable.create { e -> - val song: Song = if (cursor != null && cursor.moveToFirst()) { - getSongFromCursorImpl(cursor) - } else { - Song.emptySong - } - cursor?.close() - e.onNext(song) - e.onComplete() - } - } - - fun getSong(context: Context, queryId: Int): Observable { - val cursor = makeSongCursor(context, AudioColumns._ID + "=?", - arrayOf(queryId.toString())) - return getSong(cursor) - } - - fun suggestSongs(context: Context): Observable> { - return SongLoader.getAllSongs(context) - .flatMap { - val list = ArrayList() - ShuffleHelper.makeShuffleList(it, -1) - if (it.size >= 7) { - list.addAll(it.subList(0, 7)) - } - return@flatMap Observable.just(list) - } - } } diff --git a/app/src/main/java/code/name/monkey/retromusic/loaders/TopAndRecentlyPlayedTracksLoader.kt b/app/src/main/java/code/name/monkey/retromusic/loaders/TopAndRecentlyPlayedTracksLoader.kt index 8cb39e7ce..c868fb915 100644 --- a/app/src/main/java/code/name/monkey/retromusic/loaders/TopAndRecentlyPlayedTracksLoader.kt +++ b/app/src/main/java/code/name/monkey/retromusic/loaders/TopAndRecentlyPlayedTracksLoader.kt @@ -32,11 +32,19 @@ import java.util.* object TopAndRecentlyPlayedTracksLoader { - fun getRecentlyPlayedTracks(context: Context): Observable> { + fun getRecentlyPlayedTracksFlowable(context: Context): Observable> { + return SongLoader.getSongsFlowable(makeRecentTracksCursorAndClearUpDatabase(context)) + } + + fun getRecentlyPlayedTracks(context: Context): ArrayList { return SongLoader.getSongs(makeRecentTracksCursorAndClearUpDatabase(context)) } - fun getTopTracks(context: Context): Observable> { + fun getTopTracksFlowable(context: Context): Observable> { + return SongLoader.getSongsFlowable(makeTopTracksCursorAndClearUpDatabase(context)) + } + + fun getTopTracks(context: Context): ArrayList { return SongLoader.getSongs(makeTopTracksCursorAndClearUpDatabase(context)) } @@ -130,9 +138,11 @@ object TopAndRecentlyPlayedTracksLoader { return null } - fun getTopAlbums(context: Context): Observable> { + fun getTopAlbumsFlowable( + context: Context + ): Observable> { return Observable.create { e -> - getTopTracks(context).subscribe { songs -> + getTopTracksFlowable(context).subscribe { songs -> if (songs.size > 0) { e.onNext(AlbumLoader.splitIntoAlbums(songs)) } @@ -141,9 +151,16 @@ object TopAndRecentlyPlayedTracksLoader { } } - fun getTopArtists(context: Context): Observable> { + fun getTopAlbums( + context: Context + ): ArrayList { + arrayListOf() + return AlbumLoader.splitIntoAlbums(getTopTracks(context)) + } + + fun getTopArtistsFlowable(context: Context): Observable> { return Observable.create { e -> - getTopAlbums(context).subscribe { albums -> + getTopAlbumsFlowable(context).subscribe { albums -> if (albums.size > 0) { e.onNext(ArtistLoader.splitIntoArtists(albums)) } @@ -151,4 +168,8 @@ object TopAndRecentlyPlayedTracksLoader { } } } + + fun getTopArtists(context: Context): ArrayList { + return ArtistLoader.splitIntoArtists(getTopAlbums(context)) + } } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/AbsCustomPlaylist.java b/app/src/main/java/code/name/monkey/retromusic/model/AbsCustomPlaylist.java index 031e48c68..cf3dc05dc 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/AbsCustomPlaylist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/AbsCustomPlaylist.java @@ -19,6 +19,8 @@ import android.os.Parcel; import androidx.annotation.NonNull; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import io.reactivex.Observable; @@ -40,5 +42,8 @@ public abstract class AbsCustomPlaylist extends Playlist { } @NonNull - public abstract Observable> getSongs(Context context); + public abstract Observable> getSongsFlowable(@NotNull Context context); + + @NonNull + public abstract ArrayList getSongs(@NotNull Context context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java b/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java index a814d1dfc..85da31bec 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/Playlist.java @@ -14,11 +14,28 @@ package code.name.monkey.retromusic.model; +import android.content.Context; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.NonNull; + +import java.util.ArrayList; + +import code.name.monkey.retromusic.loaders.PlaylistSongsLoader; +import io.reactivex.Observable; + public class Playlist implements Parcelable { + public static final Creator CREATOR = new Creator() { + public Playlist createFromParcel(Parcel source) { + return new Playlist(source); + } + + public Playlist[] newArray(int size) { + return new Playlist[size]; + } + }; public final int id; public final String name; @@ -32,6 +49,23 @@ public class Playlist implements Parcelable { this.name = ""; } + protected Playlist(Parcel in) { + this.id = in.readInt(); + this.name = in.readString(); + } + + @NonNull + public Observable> getSongsFlowable(@NonNull Context context) { + // this default implementation covers static playlists + return PlaylistSongsLoader.INSTANCE.getPlaylistSongListFlowable(context, id); + } + + @NonNull + public ArrayList getSongs(@NonNull Context context) { + // this default implementation covers static playlists + return PlaylistSongsLoader.INSTANCE.getPlaylistSongList(context, id); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -59,7 +93,6 @@ public class Playlist implements Parcelable { '}'; } - @Override public int describeContents() { return 0; @@ -71,20 +104,5 @@ public class Playlist implements Parcelable { dest.writeString(this.name); } - protected Playlist(Parcel in) { - this.id = in.readInt(); - this.name = in.readString(); - } - - public static final Creator CREATOR = new Creator() { - public Playlist createFromParcel(Parcel source) { - return new Playlist(source); - } - - public Playlist[] newArray(int size) { - return new Playlist[size]; - } - }; - } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/HistoryPlaylist.java b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/HistoryPlaylist.java index 949700963..11fae6a6b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/HistoryPlaylist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/HistoryPlaylist.java @@ -17,9 +17,12 @@ package code.name.monkey.retromusic.model.smartplaylist; import android.content.Context; import android.os.Parcel; +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; -import androidx.annotation.NonNull; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.loaders.TopAndRecentlyPlayedTracksLoader; import code.name.monkey.retromusic.model.Song; @@ -51,7 +54,13 @@ public class HistoryPlaylist extends AbsSmartPlaylist { @NonNull @Override - public Observable> getSongs(@NonNull Context context) { + public Observable> getSongsFlowable(@NotNull @NonNull Context context) { + return TopAndRecentlyPlayedTracksLoader.INSTANCE.getRecentlyPlayedTracksFlowable(context); + } + + @NonNull + @Override + public ArrayList getSongs(@NotNull @NonNull Context context) { return TopAndRecentlyPlayedTracksLoader.INSTANCE.getRecentlyPlayedTracks(context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/LastAddedPlaylist.java b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/LastAddedPlaylist.java index b21e4be72..dc781a77a 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/LastAddedPlaylist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/LastAddedPlaylist.java @@ -17,9 +17,12 @@ package code.name.monkey.retromusic.model.smartplaylist; import android.content.Context; import android.os.Parcel; +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; -import androidx.annotation.NonNull; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.loaders.LastAddedSongsLoader; import code.name.monkey.retromusic.model.Song; @@ -48,7 +51,13 @@ public class LastAddedPlaylist extends AbsSmartPlaylist { @NonNull @Override - public Observable> getSongs(@NonNull Context context) { + public Observable> getSongsFlowable(@NotNull @NonNull Context context) { + return LastAddedSongsLoader.INSTANCE.getLastAddedSongsFlowable(context); + } + + @NonNull + @Override + public ArrayList getSongs(@NotNull @NonNull Context context) { return LastAddedSongsLoader.INSTANCE.getLastAddedSongs(context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/MyTopTracksPlaylist.java b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/MyTopTracksPlaylist.java index 5d58b9222..d54459372 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/MyTopTracksPlaylist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/MyTopTracksPlaylist.java @@ -17,9 +17,12 @@ package code.name.monkey.retromusic.model.smartplaylist; import android.content.Context; import android.os.Parcel; +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; -import androidx.annotation.NonNull; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.loaders.TopAndRecentlyPlayedTracksLoader; import code.name.monkey.retromusic.model.Song; @@ -51,7 +54,13 @@ public class MyTopTracksPlaylist extends AbsSmartPlaylist { @NonNull @Override - public Observable> getSongs(@NonNull Context context) { + public Observable> getSongsFlowable(@NotNull @NonNull Context context) { + return TopAndRecentlyPlayedTracksLoader.INSTANCE.getTopTracksFlowable(context); + } + + @NonNull + @Override + public ArrayList getSongs(@NotNull @NonNull Context context) { return TopAndRecentlyPlayedTracksLoader.INSTANCE.getTopTracks(context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/ShuffleAllPlaylist.java b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/ShuffleAllPlaylist.java index e990e7b17..ab3cecfc6 100644 --- a/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/ShuffleAllPlaylist.java +++ b/app/src/main/java/code/name/monkey/retromusic/model/smartplaylist/ShuffleAllPlaylist.java @@ -17,9 +17,12 @@ package code.name.monkey.retromusic.model.smartplaylist; import android.content.Context; import android.os.Parcel; +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; -import androidx.annotation.NonNull; import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.loaders.SongLoader; import code.name.monkey.retromusic.model.Song; @@ -47,7 +50,13 @@ public class ShuffleAllPlaylist extends AbsSmartPlaylist { @NonNull @Override - public Observable> getSongs(@NonNull Context context) { + public Observable> getSongsFlowable(@NotNull @NonNull Context context) { + return SongLoader.INSTANCE.getAllSongsFlowable(context); + } + + @NonNull + @Override + public ArrayList getSongs(@NotNull Context context) { return SongLoader.INSTANCE.getAllSongs(context); } diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/contract/SearchContract.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/contract/SearchContract.kt index 06f27d547..1fbf0273e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/contract/SearchContract.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/contract/SearchContract.kt @@ -24,7 +24,7 @@ import java.util.* */ interface SearchContract { - interface SearchView : BaseView> + interface SearchView : BaseView> interface SearchPresenter : BasePresenter { fun search(query: String?) diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumDetailsPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumDetailsPresenter.kt index acbe7d2ad..6ae472522 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumDetailsPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumDetailsPresenter.kt @@ -34,9 +34,7 @@ class AlbumDetailsPresenter(private val view: AlbumDetailsContract.AlbumDetailsV } override fun loadAlbumSongs(albumId: Int) { - disposable.add(repository.getAlbum(albumId) - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.getAlbumFlowable(albumId) .doOnSubscribe { view.loading() } .subscribe({ this.showAlbum(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumPresenter.kt index 8897e73d9..7ea2bcd81 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/AlbumPresenter.kt @@ -39,9 +39,7 @@ class AlbumPresenter(private val view: AlbumContract.AlbumView) : Presenter(), A } override fun loadAlbums() { - disposable.add(repository.allAlbums - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.allAlbumsFlowable .doOnSubscribe { view.loading() } .subscribe({ this.showList(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistDetailsPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistDetailsPresenter.kt index ddc0665ed..a627ae7fe 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistDetailsPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistDetailsPresenter.kt @@ -37,9 +37,7 @@ class ArtistDetailsPresenter(private val view: ArtistDetailContract.ArtistsDetai } override fun loadArtistById() { - disposable.add(repository.getArtistById(bundle.getInt(ArtistDetailActivity.EXTRA_ARTIST_ID).toLong()) - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.getArtistByIdFlowable(bundle.getInt(ArtistDetailActivity.EXTRA_ARTIST_ID)) .doOnSubscribe { view.loading() } .subscribe({ this.showArtist(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistPresenter.kt index f7763d0ac..f80d263fa 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/ArtistPresenter.kt @@ -38,9 +38,7 @@ class ArtistPresenter(private val mView: ArtistContract.ArtistView) : Presenter( } override fun loadArtists() { - disposable.add(repository.allArtists - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.allArtistsFlowable .doOnSubscribe { mView.loading() } .subscribe({ this.showList(it) }, { mView.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenreDetailsPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenreDetailsPresenter.kt index 5a9aa75b1..2a92f4815 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenreDetailsPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenreDetailsPresenter.kt @@ -36,9 +36,7 @@ class GenreDetailsPresenter(private val view: GenreDetailsContract.GenreDetailsV } override fun loadGenre(genreId: Int) { - disposable.add(repository.getGenre(genreId) - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.getGenreFlowable(genreId) .doOnSubscribe { view.loading() } .subscribe({ this.showGenre(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenrePresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenrePresenter.kt index 4f99db2c8..da0b10266 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenrePresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/GenrePresenter.kt @@ -35,9 +35,7 @@ class GenrePresenter( } override fun loadGenre() { - disposable.add(repository.allGenres - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.allGenresFlowable .doOnSubscribe { view.loading() } .subscribe({ this.showList(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/HomePresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/HomePresenter.kt index 7b837ce0b..ab39d83da 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/HomePresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/HomePresenter.kt @@ -53,7 +53,7 @@ class HomePresenter(private val view: HomeContract.HomeView) : Presenter(), Home } private fun loadRecentArtists() { - disposable += repository.recentArtists + disposable += repository.recentArtistsFlowable .subscribe({ if (it.isNotEmpty()) hashSet.add(Home(0, R.string.recent_artists, 0, it, RECENT_ARTISTS, R.drawable.ic_artist_white_24dp)) view.showData(ArrayList(hashSet)) @@ -63,7 +63,7 @@ class HomePresenter(private val view: HomeContract.HomeView) : Presenter(), Home } private fun loadRecentAlbums() { - disposable += repository.recentAlbums + disposable += repository.recentAlbumsFlowable .subscribe({ if (it.isNotEmpty()) hashSet.add(Home(1, R.string.recent_albums, 0, it, RECENT_ALBUMS, R.drawable.ic_album_white_24dp)) view.showData(ArrayList(hashSet)) @@ -73,7 +73,7 @@ class HomePresenter(private val view: HomeContract.HomeView) : Presenter(), Home } private fun loadATopAlbums() { - disposable += repository.topAlbums + disposable += repository.topAlbumsFlowable .subscribe({ if (it.isNotEmpty()) hashSet.add(Home(3, R.string.top_albums, 0, it, TOP_ALBUMS, R.drawable.ic_album_white_24dp)) view.showData(ArrayList(hashSet)) @@ -83,7 +83,7 @@ class HomePresenter(private val view: HomeContract.HomeView) : Presenter(), Home } private fun loadTopArtists() { - disposable += repository.topArtists + disposable += repository.topArtistsFlowable .subscribe({ if (it.isNotEmpty()) hashSet.add(Home(2, R.string.top_artists, 0, it, TOP_ARTISTS, R.drawable.ic_artist_white_24dp)) view.showData(ArrayList(hashSet)) @@ -93,7 +93,7 @@ class HomePresenter(private val view: HomeContract.HomeView) : Presenter(), Home } private fun loadFavorite() { - disposable += repository.favoritePlaylist + disposable += repository.favoritePlaylistFlowable .subscribe({ if (it.isNotEmpty()) hashSet.add(Home(4, R.string.favorites, 0, it, PLAYLISTS, R.drawable.ic_favorite_white_24dp)) view.showData(ArrayList(hashSet)) diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistPresenter.kt index 9b217999d..916093f20 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistPresenter.kt @@ -35,9 +35,7 @@ class PlaylistPresenter(private val view: PlaylistContract.PlaylistView) : Prese } override fun loadPlaylists() { - disposable.add(repository.allPlaylists - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.allPlaylistsFlowable .doOnSubscribe { view.loading() } .subscribe({ this.showList(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistSongsPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistSongsPresenter.kt index dffd81013..47e37a5f4 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistSongsPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/PlaylistSongsPresenter.kt @@ -35,9 +35,7 @@ class PlaylistSongsPresenter(private val view: PlaylistSongsContract.PlaylistSon } override fun loadSongs(playlist: Playlist) { - disposable.add(repository.getPlaylistSongs(playlist) - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.getPlaylistSongsFlowable(playlist) .doOnSubscribe { view.loading() } .subscribe({ songs -> view.showData(songs) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SearchPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SearchPresenter.kt index 2a88bd2bf..e3681d930 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SearchPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SearchPresenter.kt @@ -42,13 +42,6 @@ class SearchPresenter(private val view: SearchContract.SearchView) : Presenter() } override fun search(query: String?) { - disposable.add(repository.search(query) - .debounce(500, TimeUnit.MILLISECONDS) - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) - .doOnSubscribe { view.loading() } - .subscribe({ this.showList(it) }, - { view.showEmptyView() }, - { view.completed() })) + view.showData(repository.search(query)) } } diff --git a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SongPresenter.kt b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SongPresenter.kt index 896a97616..9cb4c1a40 100644 --- a/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SongPresenter.kt +++ b/app/src/main/java/code/name/monkey/retromusic/mvp/presenter/SongPresenter.kt @@ -26,9 +26,7 @@ import java.util.* class SongPresenter(private val view: SongContract.SongView) : Presenter(), SongContract.Presenter { override fun loadSongs() { - disposable.add(repository.allSongs - .subscribeOn(schedulerProvider.computation()) - .observeOn(schedulerProvider.ui()) + disposable.add(repository.allSongsFlowable .doOnSubscribe { view.loading() } .subscribe({ this.showList(it) }, { view.showEmptyView() }, diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java index 42d2daa01..4e3ac52e2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/BlacklistStore.java @@ -22,14 +22,15 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; +import androidx.annotation.NonNull; + import java.io.File; import java.util.ArrayList; -import androidx.annotation.NonNull; import code.name.monkey.retromusic.util.FileUtil; import code.name.monkey.retromusic.util.PreferenceUtil; -import static code.name.monkey.retromusic.Constants.MEDIA_STORE_CHANGED; +import static code.name.monkey.retromusic.service.MusicService.MEDIA_STORE_CHANGED; public class BlacklistStore extends SQLiteOpenHelper { public static final String DATABASE_NAME = "blacklist.db"; diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java b/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java index 229c570aa..009787be3 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java +++ b/app/src/main/java/code/name/monkey/retromusic/providers/MusicPlaybackQueueStore.java @@ -20,20 +20,20 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.provider.MediaStore.Audio.AudioColumns; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import code.name.monkey.retromusic.loaders.SongLoader; - import java.util.ArrayList; +import code.name.monkey.retromusic.loaders.SongLoader; import code.name.monkey.retromusic.model.Song; import io.reactivex.Observable; /** * @author Andrew Neal, modified for Phonograph by Karim Abou Zeid - *

- * This keeps track of the music playback and history state of the playback service + *

+ * This keeps track of the music playback and history state of the playback service */ public class MusicPlaybackQueueStore extends SQLiteOpenHelper { public static final String DATABASE_NAME = "music_playback_state.db"; @@ -187,17 +187,34 @@ public class MusicPlaybackQueueStore extends SQLiteOpenHelper { } @NonNull - public Observable> getSavedPlayingQueue() { + public Observable> getSavedPlayingQueueFlowable() { + return getQueueFlowable(PLAYING_QUEUE_TABLE_NAME); + } + + @NonNull + public Observable> getSavedOriginalPlayingQueueFlowable() { + return getQueueFlowable(ORIGINAL_PLAYING_QUEUE_TABLE_NAME); + } + + @NonNull + public ArrayList getSavedPlayingQueue() { return getQueue(PLAYING_QUEUE_TABLE_NAME); } @NonNull - public Observable> getSavedOriginalPlayingQueue() { + public ArrayList getSavedOriginalPlayingQueue() { return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME); } @NonNull - private Observable> getQueue(@NonNull final String tableName) { + private Observable> getQueueFlowable(@NonNull final String tableName) { + Cursor cursor = getReadableDatabase().query(tableName, null, + null, null, null, null, null); + return SongLoader.INSTANCE.getSongsFlowable(cursor); + } + + @NonNull + private ArrayList getQueue(@NonNull final String tableName) { Cursor cursor = getReadableDatabase().query(tableName, null, null, null, null, null, null); return SongLoader.INSTANCE.getSongs(cursor); diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/RepositoryImpl.kt b/app/src/main/java/code/name/monkey/retromusic/providers/RepositoryImpl.kt index ec46adf1b..bbf38f704 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/RepositoryImpl.kt +++ b/app/src/main/java/code/name/monkey/retromusic/providers/RepositoryImpl.kt @@ -18,113 +18,163 @@ import android.content.Context import code.name.monkey.retromusic.App import code.name.monkey.retromusic.loaders.* import code.name.monkey.retromusic.model.* -import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist import code.name.monkey.retromusic.providers.interfaces.Repository import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers class RepositoryImpl(private val context: Context) : Repository { - override val favoritePlaylist: Observable> + override fun search(query: String?): MutableList { + return SearchLoader.searchAll(context, query) + } + + override fun allAlbums(): ArrayList { + return AlbumLoader.getAllAlbums(context) + } + + override fun recentAlbums(): ArrayList { + return LastAddedSongsLoader.getLastAddedAlbums(context) + } + + override fun topAlbums(): ArrayList { + return TopAndRecentlyPlayedTracksLoader.getTopAlbums(context) + } + + override fun allArtists(): ArrayList { + return ArtistLoader.getAllArtists(context) + } + + override fun recentArtists(): ArrayList { + return LastAddedSongsLoader.getLastAddedArtists(context) + } + + override fun topArtists(): ArrayList { + return TopAndRecentlyPlayedTracksLoader.getTopArtists(context) + } + + override fun allPlaylists(): ArrayList { + return PlaylistLoader.getAllPlaylists(context) + } + + override fun allGenres(): ArrayList { + return GenreLoader.getAllGenres(context) + } + + override fun getSongFlowable(id: Int): Observable { + return SongLoader.getSongFlowable(context, id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + override fun getAlbumFlowable(albumId: Int): Observable { + return AlbumLoader.getAlbumFlowable(context, albumId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + override fun getArtistByIdFlowable(artistId: Int): Observable { + return ArtistLoader.getArtistFlowable(context, artistId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + + override fun getPlaylistSongsFlowable(playlist: Playlist): Observable> { + return PlaylistSongsLoader.getPlaylistSongListFlowable(context, playlist) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + override fun getGenreFlowable(genreId: Int): Observable> { + return GenreLoader.getSongsFlowable(context, genreId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + + override val favoritePlaylist: ArrayList get() = PlaylistLoader.getFavoritePlaylist(context) + + override fun allSongs(): ArrayList { + return SongLoader.getAllSongs(context) + } + + override val favoritePlaylistFlowable: Observable> + get() = PlaylistLoader.getFavoritePlaylistFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val allSongs: Observable> - get() = SongLoader.getAllSongs(context) + override val allSongsFlowable: Observable> + get() = SongLoader.getAllSongsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val suggestionSongs: Observable> + override val suggestionSongsFlowable: Observable> get() = SongLoader.suggestSongs(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val allAlbums: Observable> - get() = AlbumLoader.getAllAlbums(context) + override val allAlbumsFlowable: Observable> + get() = AlbumLoader.getAllAlbumsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val recentAlbums: Observable> - get() = LastAddedSongsLoader.getLastAddedAlbums(context) + override val recentAlbumsFlowable: Observable> + get() = LastAddedSongsLoader.getLastAddedAlbumsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val topAlbums: Observable> - get() = TopAndRecentlyPlayedTracksLoader.getTopAlbums(context) + override val topAlbumsFlowable: Observable> + get() = TopAndRecentlyPlayedTracksLoader.getTopAlbumsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val allArtists: Observable> - get() = ArtistLoader.getAllArtists(context) + override val allArtistsFlowable: Observable> + get() = ArtistLoader.getAllArtistsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val recentArtists: Observable> - get() = LastAddedSongsLoader.getLastAddedArtists(context) + override val recentArtistsFlowable: Observable> + get() = LastAddedSongsLoader.getLastAddedArtistsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val topArtists: Observable> - get() = TopAndRecentlyPlayedTracksLoader.getTopArtists(context) + override val topArtistsFlowable: Observable> + get() = TopAndRecentlyPlayedTracksLoader.getTopArtistsFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val allPlaylists: Observable> - get() = PlaylistLoader.getAllPlaylists(context) + override val allPlaylistsFlowable: Observable> + get() = PlaylistLoader.getAllPlaylistsFlowoable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val homeList: Observable> - get() = HomeLoader.getHomeLoader(context) + + override val allGenresFlowable: Observable> + get() = GenreLoader.getAllGenresFlowable(context) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - override val allThings: Observable> - get() = HomeLoader.getRecentAndTopThings(context) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - - override val allGenres: Observable> - get() = GenreLoader.getAllGenres(context) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - - override fun getSong(id: Int): Observable { + override fun getSong(id: Int): Song { return SongLoader.getSong(context, id) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) } - override fun getAlbum(albumId: Int): Observable { + override fun getAlbum(albumId: Int): Album { return AlbumLoader.getAlbum(context, albumId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) } - override fun getArtistById(artistId: Long): Observable { + override fun getArtistById(artistId: Long): Artist { return ArtistLoader.getArtist(context, artistId.toInt()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) } - override fun search(query: String?): Observable> { - return SearchLoader.searchAll(context, query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - } - - override fun getPlaylistSongs(playlist: Playlist): Observable> { + override fun getPlaylistSongs(playlist: Playlist): ArrayList { return PlaylistSongsLoader.getPlaylistSongList(context, playlist) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + } - override fun getGenre(genreId: Int): Observable> { + override fun getGenre(genreId: Int): ArrayList { return GenreLoader.getSongs(context, genreId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + } diff --git a/app/src/main/java/code/name/monkey/retromusic/providers/interfaces/Repository.kt b/app/src/main/java/code/name/monkey/retromusic/providers/interfaces/Repository.kt index 994eaa289..cc5755256 100644 --- a/app/src/main/java/code/name/monkey/retromusic/providers/interfaces/Repository.kt +++ b/app/src/main/java/code/name/monkey/retromusic/providers/interfaces/Repository.kt @@ -15,9 +15,7 @@ package code.name.monkey.retromusic.providers.interfaces import code.name.monkey.retromusic.model.* -import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist import io.reactivex.Observable -import kotlin.collections.ArrayList /** * Created by hemanths on 11/08/17. @@ -25,42 +23,68 @@ import kotlin.collections.ArrayList interface Repository { - val allSongs: Observable> + val allSongsFlowable: Observable> - val suggestionSongs: Observable> + fun allSongs(): ArrayList - val allAlbums: Observable> + val suggestionSongsFlowable: Observable> - val recentAlbums: Observable> + val allAlbumsFlowable: Observable> - val topAlbums: Observable> + fun allAlbums(): ArrayList - val allArtists: Observable> + val recentAlbumsFlowable: Observable> - val recentArtists: Observable> + fun recentAlbums(): ArrayList - val topArtists: Observable> + val topAlbumsFlowable: Observable> - val allPlaylists: Observable> + fun topAlbums(): ArrayList - val homeList: Observable> + val allArtistsFlowable: Observable> - val allThings: Observable> + fun allArtists(): ArrayList - val allGenres: Observable> + val recentArtistsFlowable: Observable> - fun getSong(id: Int): Observable + fun recentArtists(): ArrayList - fun getAlbum(albumId: Int): Observable + val topArtistsFlowable: Observable> - fun getArtistById(artistId: Long): Observable + fun topArtists(): ArrayList - fun search(query: String?): Observable> + val allPlaylistsFlowable: Observable> - fun getPlaylistSongs(playlist: Playlist): Observable> + fun allPlaylists(): ArrayList - fun getGenre(genreId: Int): Observable> + val allGenresFlowable: Observable> - val favoritePlaylist: Observable> + fun allGenres(): ArrayList + + fun getSongFlowable(id: Int): Observable + + fun getSong(id: Int): Song + + fun getAlbumFlowable(albumId: Int): Observable + + fun getAlbum(albumId: Int): Album + + fun getArtistByIdFlowable(artistId: Int): Observable + + fun getArtistById(artistId: Long): Artist + + fun search(query: String?): MutableList + + fun getPlaylistSongsFlowable(playlist: Playlist): Observable> + + fun getPlaylistSongs(playlist: Playlist): ArrayList + + fun getGenreFlowable(genreId: Int): Observable> + + fun getGenre(genreId: Int): ArrayList + + val favoritePlaylistFlowable: Observable> + + val favoritePlaylist: ArrayList } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt b/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt index ab0c95d9e..09c282a73 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt @@ -27,12 +27,7 @@ import android.util.Log import android.view.KeyEvent import androidx.core.content.ContextCompat import code.name.monkey.retromusic.BuildConfig -import code.name.monkey.retromusic.Constants.ACTION_PAUSE -import code.name.monkey.retromusic.Constants.ACTION_PLAY -import code.name.monkey.retromusic.Constants.ACTION_REWIND -import code.name.monkey.retromusic.Constants.ACTION_SKIP -import code.name.monkey.retromusic.Constants.ACTION_STOP -import code.name.monkey.retromusic.Constants.ACTION_TOGGLE_PAUSE +import code.name.monkey.retromusic.service.MusicService.* /** diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MediaSessionCallback.kt b/app/src/main/java/code/name/monkey/retromusic/service/MediaSessionCallback.kt new file mode 100644 index 000000000..dfa91487a --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/MediaSessionCallback.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v4.media.session.MediaSessionCompat +import code.name.monkey.retromusic.auto.AutoMediaIDHelper +import code.name.monkey.retromusic.helper.MusicPlayerRemote +import code.name.monkey.retromusic.helper.ShuffleHelper +import code.name.monkey.retromusic.loaders.* +import code.name.monkey.retromusic.model.Song +import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore +import code.name.monkey.retromusic.service.MusicService.TOGGLE_FAVORITE +import code.name.monkey.retromusic.util.MusicUtil +import java.util.* + + +/** + * Created by hemanths on 2019-08-01. + */ + +class MediaSessionCallback(private val context: Context, + private val musicService: MusicService) : MediaSessionCompat.Callback() { + + override fun onPlay() { + super.onPlay() + musicService.play() + } + + override fun onPause() { + super.onPause() + musicService.pause() + } + + override fun onSkipToNext() { + super.onSkipToNext() + musicService.playNextSong(true) + } + + override fun onSkipToPrevious() { + super.onSkipToPrevious() + musicService.back(true) + } + + override fun onStop() { + super.onStop() + musicService.quit() + } + + override fun onSeekTo(pos: Long) { + super.onSeekTo(pos) + musicService.seek(pos.toInt()) + } + + override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean { + return MediaButtonIntentReceiver.handleIntent(context, mediaButtonIntent) + } + + override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { + super.onPlayFromMediaId(mediaId, extras) + + val musicId = mediaId?.let { AutoMediaIDHelper.extractMusicID(it) } + val itemId = musicId?.toInt() ?: -1 + val songs = arrayListOf() + when (val category = mediaId?.let { AutoMediaIDHelper.extractCategory(it) }) { + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM -> { + val album = AlbumLoader.getAlbum(context, itemId) + album.songs?.let { songs.addAll(it) } + openQueue(songs, 0, true) + } + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST -> { + val artist = ArtistLoader.getArtist(context, itemId) + songs.addAll(artist.songs) + openQueue(songs, 0, true) + } + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST -> { + val playlist = PlaylistLoader.getPlaylist(context, itemId) + songs.addAll(playlist.getSongs(context)) + openQueue(songs, 0, true) + } + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY, + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS, + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE -> { + val tracks: List + if (category.equals(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY)) { + tracks = TopAndRecentlyPlayedTracksLoader.getRecentlyPlayedTracks(context) + } else if (category.equals(AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS)) { + tracks = TopAndRecentlyPlayedTracksLoader.getTopTracks(context) + } else { + tracks = MusicPlaybackQueueStore.getInstance(context).savedOriginalPlayingQueue + } + songs.addAll(tracks) + var songIndex = MusicUtil.indexOfSongInList(tracks, itemId) + if (songIndex == -1) { + songIndex = 0 + } + openQueue(songs, songIndex, true) + } + AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE -> { + val allSongs = SongLoader.getAllSongs(context) + ShuffleHelper.makeShuffleList(allSongs, -1) + openQueue(allSongs, 0, true) + } + } + + } + + override fun onCustomAction(action: String, extras: Bundle?) { + when (action) { + /* CYCLE_REPEAT -> { + cycleRepeatMode() + musicService.updateMediaSessionPlaybackState() + } + + TOGGLE_SHUFFLE -> { + musicService.toggleShuffle() + musicService.updateMediaSessionPlaybackState() + } + */ + TOGGLE_FAVORITE -> { + MusicUtil.toggleFavorite(context, MusicPlayerRemote.currentSong) + musicService.updateMediaSessionPlaybackState() + } + else -> { + println("Unsupported action: $action") + } + } + } + + private fun checkAndStartPlaying(songs: ArrayList, itemId: Int) { + var songIndex = MusicUtil.indexOfSongInList(songs, itemId) + if (songIndex == -1) { + songIndex = 0 + } + openQueue(songs, songIndex) + } + + private fun openQueue(songs: ArrayList, index: Int, startPlaying: Boolean = true) { + MusicPlayerRemote.openQueue(songs, index, startPlaying) + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.java b/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.java new file mode 100644 index 000000000..14c9780fc --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/MediaStoreObserver.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service; + +import android.database.ContentObserver; +import android.os.Handler; + +public class MediaStoreObserver extends ContentObserver implements Runnable { + // milliseconds to delay before calling refresh to aggregate events + private static final long REFRESH_DELAY = 500; + private final MusicService musicService; + private Handler mHandler; + + MediaStoreObserver(MusicService musicService, Handler handler) { + super(handler); + this.musicService = musicService; + mHandler = handler; + } + + @Override + public void onChange(boolean selfChange) { + // if a change is detected, remove any scheduled callback + // then post a new one. This is intended to prevent closely + // spaced events from generating multiple refresh calls + mHandler.removeCallbacks(this); + mHandler.postDelayed(this, REFRESH_DELAY); + } + + @Override + public void run() { + // actually call refresh when the delayed callback fires + // do not send a sticky broadcast here + musicService.handleAndSendChangeInternal(MusicService.MEDIA_STORE_CHANGED); + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index 2243d1b53..c9234377e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -15,7 +15,6 @@ package code.name.monkey.retromusic.service; import android.app.PendingIntent; -import android.app.Service; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -31,15 +30,15 @@ import android.media.AudioManager; import android.media.audiofx.AudioEffect; import android.os.Binder; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; -import android.os.Looper; -import android.os.Message; import android.os.PowerManager; import android.os.Process; import android.preference.PreferenceManager; import android.provider.MediaStore; +import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; @@ -50,10 +49,10 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.media.MediaBrowserServiceCompat; import com.bumptech.glide.request.transition.Transition; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -64,15 +63,14 @@ import code.name.monkey.retromusic.appwidgets.AppWidgetCard; import code.name.monkey.retromusic.appwidgets.AppWidgetClassic; import code.name.monkey.retromusic.appwidgets.AppWidgetSmall; import code.name.monkey.retromusic.appwidgets.AppWidgetText; +import code.name.monkey.retromusic.auto.AutoMediaIDHelper; +import code.name.monkey.retromusic.auto.AutoMusicProvider; import code.name.monkey.retromusic.glide.BlurTransformation; import code.name.monkey.retromusic.glide.GlideApp; import code.name.monkey.retromusic.glide.GlideRequest; import code.name.monkey.retromusic.glide.RetroGlideExtension; import code.name.monkey.retromusic.glide.RetroSimpleTarget; import code.name.monkey.retromusic.helper.ShuffleHelper; -import code.name.monkey.retromusic.helper.StopWatch; -import code.name.monkey.retromusic.loaders.PlaylistSongsLoader; -import code.name.monkey.retromusic.model.AbsCustomPlaylist; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.providers.HistoryStore; @@ -83,57 +81,73 @@ import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl2 import code.name.monkey.retromusic.service.notification.PlayingNotificationOreo; import code.name.monkey.retromusic.service.playback.Playback; import code.name.monkey.retromusic.util.MusicUtil; +import code.name.monkey.retromusic.util.PackageValidator; import code.name.monkey.retromusic.util.PreferenceUtil; import code.name.monkey.retromusic.util.RetroUtil; -import io.reactivex.schedulers.Schedulers; - -import static code.name.monkey.retromusic.Constants.ACTION_PAUSE; -import static code.name.monkey.retromusic.Constants.ACTION_PENDING_QUIT; -import static code.name.monkey.retromusic.Constants.ACTION_PLAY; -import static code.name.monkey.retromusic.Constants.ACTION_PLAY_PLAYLIST; -import static code.name.monkey.retromusic.Constants.ACTION_QUIT; -import static code.name.monkey.retromusic.Constants.ACTION_REWIND; -import static code.name.monkey.retromusic.Constants.ACTION_SKIP; -import static code.name.monkey.retromusic.Constants.ACTION_STOP; -import static code.name.monkey.retromusic.Constants.ACTION_TOGGLE_PAUSE; -import static code.name.monkey.retromusic.Constants.APP_WIDGET_UPDATE; -import static code.name.monkey.retromusic.Constants.EXTRA_APP_WIDGET_NAME; -import static code.name.monkey.retromusic.Constants.INTENT_EXTRA_PLAYLIST; -import static code.name.monkey.retromusic.Constants.INTENT_EXTRA_SHUFFLE_MODE; -import static code.name.monkey.retromusic.Constants.MEDIA_STORE_CHANGED; -import static code.name.monkey.retromusic.Constants.META_CHANGED; -import static code.name.monkey.retromusic.Constants.MUSIC_PACKAGE_NAME; -import static code.name.monkey.retromusic.Constants.PLAY_STATE_CHANGED; -import static code.name.monkey.retromusic.Constants.QUEUE_CHANGED; -import static code.name.monkey.retromusic.Constants.REPEAT_MODE_CHANGED; -import static code.name.monkey.retromusic.Constants.RETRO_MUSIC_PACKAGE_NAME; -import static code.name.monkey.retromusic.Constants.SHUFFLE_MODE_CHANGED; /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal */ -public class MusicService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { +public class MusicService extends MediaBrowserServiceCompat implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { public static final String TAG = MusicService.class.getSimpleName(); + + public static final String RETRO_MUSIC_PACKAGE_NAME = "code.name.monkey.retromusic"; + public static final String MUSIC_PACKAGE_NAME = "com.android.music"; + + public static final String ACTION_TOGGLE_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".togglepause"; + public static final String ACTION_PLAY = RETRO_MUSIC_PACKAGE_NAME + ".play"; + public static final String ACTION_PLAY_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + ".play.playlist"; + public static final String ACTION_PAUSE = RETRO_MUSIC_PACKAGE_NAME + ".pause"; + public static final String ACTION_STOP = RETRO_MUSIC_PACKAGE_NAME + ".stop"; + public static final String ACTION_SKIP = RETRO_MUSIC_PACKAGE_NAME + ".skip"; + public static final String ACTION_REWIND = RETRO_MUSIC_PACKAGE_NAME + ".rewind"; + public static final String ACTION_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".quitservice"; + public static final String ACTION_PENDING_QUIT = RETRO_MUSIC_PACKAGE_NAME + ".pendingquitservice"; + public static final String INTENT_EXTRA_PLAYLIST = RETRO_MUSIC_PACKAGE_NAME + "intentextra.playlist"; + public static final String INTENT_EXTRA_SHUFFLE_MODE = RETRO_MUSIC_PACKAGE_NAME + ".intentextra.shufflemode"; + + public static final String APP_WIDGET_UPDATE = RETRO_MUSIC_PACKAGE_NAME + ".appwidgetupdate"; + public static final String EXTRA_APP_WIDGET_NAME = RETRO_MUSIC_PACKAGE_NAME + "app_widget_name"; + + // Do not change these three strings as it will break support with other apps (e.g. last.fm scrobbling) + public static final String META_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".metachanged"; + public static final String QUEUE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".queuechanged"; + public static final String PLAY_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".playstatechanged"; + + public static final String FAVORITE_STATE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + "favoritestatechanged"; + + public static final String REPEAT_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".repeatmodechanged"; + public static final String SHUFFLE_MODE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".shufflemodechanged"; + public static final String MEDIA_STORE_CHANGED = RETRO_MUSIC_PACKAGE_NAME + ".mediastorechanged"; + + public static final String CYCLE_REPEAT = RETRO_MUSIC_PACKAGE_NAME + ".cyclerepeat"; + public static final String TOGGLE_SHUFFLE = RETRO_MUSIC_PACKAGE_NAME + ".toggleshuffle"; + public static final String TOGGLE_FAVORITE = RETRO_MUSIC_PACKAGE_NAME + ".togglefavorite"; + public static final String SAVED_POSITION = "POSITION"; public static final String SAVED_POSITION_IN_TRACK = "POSITION_IN_TRACK"; public static final String SAVED_SHUFFLE_MODE = "SHUFFLE_MODE"; public static final String SAVED_REPEAT_MODE = "REPEAT_MODE"; + public static final int RELEASE_WAKELOCK = 0; public static final int TRACK_ENDED = 1; public static final int TRACK_WENT_TO_NEXT = 2; public static final int PLAY_SONG = 3; public static final int PREPARE_NEXT = 4; public static final int SET_POSITION = 5; + public static final int FOCUS_CHANGE = 6; + public static final int DUCK = 7; + public static final int UNDUCK = 8; public static final int RESTORE_QUEUES = 9; + public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_SHUFFLE = 1; + public static final int REPEAT_MODE_NONE = 0; public static final int REPEAT_MODE_ALL = 1; public static final int REPEAT_MODE_THIS = 2; + public static final int SAVE_QUEUES = 0; - private static final int FOCUS_CHANGE = 6; - private static final int DUCK = 7; - private static final int UNDUCK = 8; private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE @@ -143,6 +157,9 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP | PlaybackStateCompat.ACTION_SEEK_TO; private final IBinder musicBind = new MusicBinder(); public boolean pendingQuit = false; + public Playback playback; + public int position = -1; + public int nextPosition = -1; private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance(); private AppWidgetClassic appWidgetClassic = AppWidgetClassic.Companion.getInstance(); private AppWidgetSmall appWidgetSmall = AppWidgetSmall.Companion.getInstance(); @@ -179,11 +196,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } }; - private Playback playback; private ArrayList playingQueue = new ArrayList<>(); private ArrayList originalPlayingQueue = new ArrayList<>(); - private int position = -1; - private int nextPosition = -1; private int shuffleMode; private int repeatMode; private boolean queuesRestored; @@ -198,7 +212,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP }; private PlayingNotification playingNotification; private AudioManager audioManager; - @SuppressWarnings("deprecation") private MediaSessionCompat mediaSession; private PowerManager.WakeLock wakeLock; private PlaybackHandler playerHandler; @@ -235,7 +248,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP super.onCallStateChanged(state, incomingNumber); } }; - private boolean isServiceBound; private Handler uiThreadHandler; private IntentFilter headsetReceiverIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); private boolean headsetReceiverRegistered = false; @@ -260,6 +272,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } }; + private PackageValidator mPackageValidator; + private AutoMusicProvider mMusicProvider; private static String getTrackUri(@NonNull Song song) { return MusicUtil.getSongFileUri(song.getId()).toString(); @@ -278,7 +292,6 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } - @Override public void onCreate() { super.onCreate(); @@ -314,8 +327,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP initNotification(); - mediaStoreObserver = new MediaStoreObserver(playerHandler); - throttledSeekHandler = new ThrottledSeekHandler(playerHandler); + mediaStoreObserver = new MediaStoreObserver(this, playerHandler); + throttledSeekHandler = new ThrottledSeekHandler(this, playerHandler); getContentResolver().registerContentObserver( MediaStore.Audio.Media.INTERNAL_CONTENT_URI, true, mediaStoreObserver); getContentResolver().registerContentObserver( @@ -324,14 +337,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP PreferenceUtil.getInstance().registerOnSharedPreferenceChangedListener(this); restoreState(); - - mediaSession.setActive(true); - + mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers); + mMusicProvider = new AutoMusicProvider(this); sendBroadcast(new Intent("code.name.monkey.retromusic.RETRO_MUSIC_SERVICE_CREATED")); registerHeadsetEvents(); - - } private AudioManager getAudioManager() { @@ -354,50 +364,14 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP "RetroMusicPlayer", mediaButtonReceiverComponentName, mediaButtonReceiverPendingIntent); - mediaSession.setCallback(new MediaSessionCompat.Callback() { - - @Override - public void onPlay() { - play(); - } - - @Override - public void onPause() { - pause(); - } - - @Override - public void onSkipToNext() { - playNextSong(true); - } - - @Override - public void onSkipToPrevious() { - back(true); - } - - @Override - public void onStop() { - quit(); - } - - @Override - public void onSeekTo(long pos) { - seek((int) pos); - updateMediaSessionPlaybackState(); - } - - @Override - public boolean onMediaButtonEvent(Intent mediaButtonEvent) { - return MediaButtonIntentReceiver.Companion.handleIntent(MusicService.this, mediaButtonEvent); - } - }); - + MediaSessionCallback mediasessionCallback = new MediaSessionCallback(getApplicationContext(), this); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ); - + mediaSession.setCallback(mediasessionCallback); + mediaSession.setActive(true); mediaSession.setMediaButtonReceiver(mediaButtonReceiverPendingIntent); + setSessionToken(mediaSession.getSessionToken()); } @Override @@ -437,6 +411,9 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP case ACTION_PENDING_QUIT: pendingQuit = true; break; + case TOGGLE_FAVORITE: + MusicUtil.toggleFavorite(getApplicationContext(), getCurrentSong()); + break; } } } @@ -447,22 +424,21 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private void playFromPlaylist(Intent intent) { Playlist playlist = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST); int shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode()); - if (playlist != null) { - if (playlist instanceof AbsCustomPlaylist) { - ((AbsCustomPlaylist) playlist).getSongs(getApplicationContext()) - .subscribeOn(Schedulers.io()) - .subscribe(songs -> { - playSongs(shuffleMode, songs); - }, throwable -> { - }); + ArrayList playlistSongs = playlist.getSongs(getApplicationContext()); + if (!playlistSongs.isEmpty()) { + if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { + int startPosition = 0; + if (!playlistSongs.isEmpty()) { + startPosition = new Random().nextInt(playlistSongs.size()); + } + openQueue(playlistSongs, startPosition, true); + setShuffleMode(shuffleMode); + } else { + openQueue(playlistSongs, 0, true); + } } else { - PlaylistSongsLoader.INSTANCE.getPlaylistSongList(getApplicationContext(), playlist.id) - .subscribeOn(Schedulers.io()) - .subscribe(songs -> { - playSongs(shuffleMode, songs); - }, throwable -> { - }); + Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); } } else { Toast.makeText(getApplicationContext(), R.string.playlist_is_empty, Toast.LENGTH_LONG).show(); @@ -507,25 +483,43 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP @Override public IBinder onBind(Intent intent) { - isServiceBound = true; return musicBind; } + @Nullable + @Override + public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + if (!mPackageValidator.isKnownCaller(clientPackageName, clientUid)) { + return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null); + } + return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_ROOT, null); + } + + @Override + public void onLoadChildren(@NonNull String parentId, @NonNull Result> result) { + if (AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT.equals(parentId)) { + result.sendResult(new ArrayList<>()); + } else if (mMusicProvider.isInitialized()) { + result.sendResult(mMusicProvider.getChildren(parentId, getResources())); + } else { + result.detach(); + mMusicProvider.retrieveMediaAsync(success -> result.sendResult(mMusicProvider.getChildren(parentId, getResources()))); + } + } + @Override public void onRebind(Intent intent) { - isServiceBound = true; } @Override public boolean onUnbind(Intent intent) { - isServiceBound = false; if (!isPlaying()) { stopSelf(); } return true; } - private void saveQueuesImpl() { + public void saveQueuesImpl() { MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue); } @@ -533,7 +527,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION, getPosition()).apply(); } - private void savePositionInTrack() { + public void savePositionInTrack() { PreferenceManager.getDefaultSharedPreferences(this).edit().putInt(SAVED_POSITION_IN_TRACK, getSongProgressMillis()).apply(); } @@ -558,14 +552,10 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP playerHandler.sendEmptyMessage(RESTORE_QUEUES); } - private synchronized void restoreQueuesAndPositionIfNecessary() { + public synchronized void restoreQueuesAndPositionIfNecessary() { if (!queuesRestored && playingQueue.isEmpty()) { - ArrayList restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue() - .blockingFirst(); - - ArrayList restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue() - .blockingFirst(); - + ArrayList restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue(); + ArrayList restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue(); int restoredPosition = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION, -1); int restoredPositionInTrack = PreferenceManager.getDefaultSharedPreferences(this).getInt(SAVED_POSITION_IN_TRACK, -1); @@ -587,20 +577,16 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP queuesRestored = true; } - private int quit() { + public void quit() { pause(); playingNotification.stop(); - if (isServiceBound) { - return START_STICKY; - } else { - closeAudioEffectSession(); - getAudioManager().abandonAudioFocus(audioFocusListener); - stopSelf(); - return START_NOT_STICKY; - } + closeAudioEffectSession(); + getAudioManager().abandonAudioFocus(audioFocusListener); + stopSelf(); } + private void releaseResources() { playerHandler.removeCallbacksAndMessages(null); musicPlayerHandlerThread.quitSafely(); @@ -629,7 +615,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP playSongAt(getNextPosition(force)); } - private boolean openTrackAndPrepareNextAt(int position) { + public boolean openTrackAndPrepareNextAt(int position) { synchronized (this) { this.position = position; boolean prepared = openCurrent(); @@ -655,7 +641,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP playerHandler.obtainMessage(PREPARE_NEXT).sendToTarget(); } - private boolean prepareNextImpl() { + public boolean prepareNextImpl() { synchronized (this) { try { int nextPosition = getNextPosition(false); @@ -694,13 +680,14 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } - private void updateMediaSessionPlaybackState() { - mediaSession.setPlaybackState( - new PlaybackStateCompat.Builder() - .setActions(MEDIA_SESSION_ACTIONS) - .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, - getSongProgressMillis(), 1) - .build()); + public void updateMediaSessionPlaybackState() { + PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder() + .setActions(MEDIA_SESSION_ACTIONS) + .setState(isPlaying() ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED, + getSongProgressMillis(), 1); + + + mediaSession.setPlaybackState(stateBuilder.build()); } private void updateMediaSessionMetaData() { @@ -801,7 +788,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP return position; } - private boolean isLastTrack() { + public boolean isLastTrack() { return getPosition() == getPlayingQueue().size() - 1; } @@ -946,7 +933,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); } - private void playSongAtImpl(int position) { + public void playSongAtImpl(int position) { if (openTrackAndPrepareNextAt(position)) { play(); } else { @@ -1128,18 +1115,18 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP notifyChange(QUEUE_CHANGED); } - private void notifyChange(@NonNull final String what) { + public void notifyChange(@NonNull final String what) { handleAndSendChangeInternal(what); sendPublicIntent(what); } - private void handleAndSendChangeInternal(@NonNull final String what) { + public void handleAndSendChangeInternal(@NonNull final String what) { handleChangeInternal(what); sendChangeInternal(what); } // to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch - private void sendPublicIntent(@NonNull final String what) { + public void sendPublicIntent(@NonNull final String what) { final Intent intent = new Intent(what.replace(RETRO_MUSIC_PACKAGE_NAME, MUSIC_PACKAGE_NAME)); final Song song = getCurrentSong(); @@ -1266,187 +1253,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP playerHandler.sendEmptyMessage(TRACK_ENDED); } - - private static final class QueueSaveHandler extends Handler { - @NonNull - private final WeakReference mService; - - QueueSaveHandler(final MusicService service, @NonNull final Looper looper) { - super(looper); - mService = new WeakReference<>(service); - } - - @Override - public void handleMessage(@NonNull Message msg) { - final MusicService service = mService.get(); - if (msg.what == SAVE_QUEUES) { - service.saveQueuesImpl(); - } - } + public boolean isPausedByTransientLossOfFocus() { + return pausedByTransientLossOfFocus; } - private static final class PlaybackHandler extends Handler { - @NonNull - private final WeakReference mService; - private float currentDuckVolume = 1.0f; - - PlaybackHandler(final MusicService service, @NonNull final Looper looper) { - super(looper); - mService = new WeakReference<>(service); - } - - @Override - public void handleMessage(@NonNull final Message msg) { - final MusicService service = mService.get(); - if (service == null) { - return; - } - - switch (msg.what) { - case DUCK: - if (PreferenceUtil.getInstance().audioDucking()) { - currentDuckVolume -= .05f; - if (currentDuckVolume > .2f) { - sendEmptyMessageDelayed(DUCK, 10); - } else { - currentDuckVolume = .2f; - } - } else { - currentDuckVolume = 1f; - } - service.playback.setVolume(currentDuckVolume); - break; - - case UNDUCK: - if (PreferenceUtil.getInstance().audioDucking()) { - currentDuckVolume += .03f; - if (currentDuckVolume < 1f) { - sendEmptyMessageDelayed(UNDUCK, 10); - } else { - currentDuckVolume = 1f; - } - } else { - currentDuckVolume = 1f; - } - service.playback.setVolume(currentDuckVolume); - break; - - case TRACK_WENT_TO_NEXT: - if (service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { - service.pause(); - service.seek(0); - } else { - service.position = service.nextPosition; - service.prepareNextImpl(); - service.notifyChange(META_CHANGED); - } - break; - - case TRACK_ENDED: - // if there is a timer finished, don't continue - if (service.pendingQuit || - service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { - service.notifyChange(PLAY_STATE_CHANGED); - service.seek(0); - if (service.pendingQuit) { - service.pendingQuit = false; - service.quit(); - break; - } - } else { - service.playNextSong(false); - } - sendEmptyMessage(RELEASE_WAKELOCK); - break; - - case RELEASE_WAKELOCK: - service.releaseWakeLock(); - break; - - case PLAY_SONG: - service.playSongAtImpl(msg.arg1); - break; - - case SET_POSITION: - service.openTrackAndPrepareNextAt(msg.arg1); - service.notifyChange(PLAY_STATE_CHANGED); - break; - - case PREPARE_NEXT: - service.prepareNextImpl(); - break; - - case RESTORE_QUEUES: - service.restoreQueuesAndPositionIfNecessary(); - break; - - case FOCUS_CHANGE: - switch (msg.arg1) { - case AudioManager.AUDIOFOCUS_GAIN: - if (!service.isPlaying() && service.pausedByTransientLossOfFocus) { - service.play(); - service.pausedByTransientLossOfFocus = false; - } - removeMessages(DUCK); - sendEmptyMessage(UNDUCK); - break; - - case AudioManager.AUDIOFOCUS_LOSS: - // Lost focus for an unbounded amount of time: stop playback and release media playback - service.pause(); - break; - - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - // Lost focus for a short time, but we have to stop - // playback. We don't release the media playback because playback - // is likely to resume - boolean wasPlaying = service.isPlaying(); - service.pause(); - service.pausedByTransientLossOfFocus = wasPlaying; - break; - - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - // Lost focus for a short time, but it's ok to keep playing - // at an attenuated level - removeMessages(UNDUCK); - sendEmptyMessage(DUCK); - break; - } - break; - } - } - } - - private static class SongPlayCountHelper { - public static final String TAG = SongPlayCountHelper.class.getSimpleName(); - - private StopWatch stopWatch = new StopWatch(); - private Song song = Song.getEmptySong(); - - public Song getSong() { - return song; - } - - boolean shouldBumpPlayCount() { - return song.getDuration() * 0.5d < stopWatch.getElapsedTime(); - } - - void notifySongChanged(Song song) { - synchronized (this) { - stopWatch.reset(); - this.song = song; - } - } - - void notifyPlayStateChanged(boolean isPlaying) { - synchronized (this) { - if (isPlaying) { - stopWatch.start(); - } else { - stopWatch.pause(); - } - } - } + public void setPausedByTransientLossOfFocus(boolean pausedByTransientLossOfFocus) { + this.pausedByTransientLossOfFocus = pausedByTransientLossOfFocus; } public class MusicBinder extends Binder { @@ -1455,52 +1267,4 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP return MusicService.this; } } - - private class MediaStoreObserver extends ContentObserver implements Runnable { - // milliseconds to delay before calling refresh to aggregate events - private static final long REFRESH_DELAY = 500; - private Handler mHandler; - - MediaStoreObserver(Handler handler) { - super(handler); - mHandler = handler; - } - - @Override - public void onChange(boolean selfChange) { - // if a change is detected, remove any scheduled callback - // then post a new one. This is intended to prevent closely - // spaced events from generating multiple refresh calls - mHandler.removeCallbacks(this); - mHandler.postDelayed(this, REFRESH_DELAY); - } - - @Override - public void run() { - // actually call refresh when the delayed callback fires - // do not send a sticky broadcast here - handleAndSendChangeInternal(MEDIA_STORE_CHANGED); - } - } - - private class ThrottledSeekHandler implements Runnable { - // milliseconds to throttle before calling run() to aggregate events - private static final long THROTTLE = 500; - private Handler mHandler; - - ThrottledSeekHandler(Handler handler) { - mHandler = handler; - } - - void notifySeek() { - mHandler.removeCallbacks(this); - mHandler.postDelayed(this, THROTTLE); - } - - @Override - public void run() { - savePositionInTrack(); - sendPublicIntent(PLAY_STATE_CHANGED); // for musixmatch synced lyrics - } - } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java new file mode 100644 index 000000000..d86bc2c27 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackHandler.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service; + +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; + +import code.name.monkey.retromusic.util.PreferenceUtil; + +import static code.name.monkey.retromusic.service.MusicService.DUCK; +import static code.name.monkey.retromusic.service.MusicService.META_CHANGED; +import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED; +import static code.name.monkey.retromusic.service.MusicService.REPEAT_MODE_NONE; +import static code.name.monkey.retromusic.service.MusicService.TRACK_ENDED; +import static code.name.monkey.retromusic.service.MusicService.TRACK_WENT_TO_NEXT; + +class PlaybackHandler extends Handler { + @NonNull + private final WeakReference mService; + private float currentDuckVolume = 1.0f; + + PlaybackHandler(final MusicService service, @NonNull final Looper looper) { + super(looper); + mService = new WeakReference<>(service); + } + + @Override + public void handleMessage(@NonNull final Message msg) { + final MusicService service = mService.get(); + if (service == null) { + return; + } + + switch (msg.what) { + case MusicService.DUCK: + if (PreferenceUtil.getInstance().audioDucking()) { + currentDuckVolume -= .05f; + if (currentDuckVolume > .2f) { + sendEmptyMessageDelayed(DUCK, 10); + } else { + currentDuckVolume = .2f; + } + } else { + currentDuckVolume = 1f; + } + service.playback.setVolume(currentDuckVolume); + break; + + case MusicService.UNDUCK: + if (PreferenceUtil.getInstance().audioDucking()) { + currentDuckVolume += .03f; + if (currentDuckVolume < 1f) { + sendEmptyMessageDelayed(MusicService.UNDUCK, 10); + } else { + currentDuckVolume = 1f; + } + } else { + currentDuckVolume = 1f; + } + service.playback.setVolume(currentDuckVolume); + break; + + case TRACK_WENT_TO_NEXT: + if (service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { + service.pause(); + service.seek(0); + } else { + service.position = service.nextPosition; + service.prepareNextImpl(); + service.notifyChange(META_CHANGED); + } + break; + + case TRACK_ENDED: + // if there is a timer finished, don't continue + if (service.pendingQuit || + service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) { + service.notifyChange(PLAY_STATE_CHANGED); + service.seek(0); + if (service.pendingQuit) { + service.pendingQuit = false; + service.quit(); + break; + } + } else { + service.playNextSong(false); + } + sendEmptyMessage(MusicService.RELEASE_WAKELOCK); + break; + + case MusicService.RELEASE_WAKELOCK: + service.releaseWakeLock(); + break; + + case MusicService.PLAY_SONG: + service.playSongAtImpl(msg.arg1); + break; + + case MusicService.SET_POSITION: + service.openTrackAndPrepareNextAt(msg.arg1); + service.notifyChange(PLAY_STATE_CHANGED); + break; + + case MusicService.PREPARE_NEXT: + service.prepareNextImpl(); + break; + + case MusicService.RESTORE_QUEUES: + service.restoreQueuesAndPositionIfNecessary(); + break; + + case MusicService.FOCUS_CHANGE: + switch (msg.arg1) { + case AudioManager.AUDIOFOCUS_GAIN: + if (!service.isPlaying() && service.isPausedByTransientLossOfFocus()) { + service.play(); + service.setPausedByTransientLossOfFocus(false); + } + removeMessages(DUCK); + sendEmptyMessage(MusicService.UNDUCK); + break; + + case AudioManager.AUDIOFOCUS_LOSS: + // Lost focus for an unbounded amount of time: stop playback and release media playback + service.pause(); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + // Lost focus for a short time, but we have to stop + // playback. We don't release the media playback because playback + // is likely to resume + boolean wasPlaying = service.isPlaying(); + service.pause(); + service.setPausedByTransientLossOfFocus(wasPlaying); + break; + + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + // Lost focus for a short time, but it's ok to keep playing + // at an attenuated level + removeMessages(MusicService.UNDUCK); + sendEmptyMessage(DUCK); + break; + } + break; + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.java b/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.java new file mode 100644 index 000000000..0d60ca2e4 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; + +import static code.name.monkey.retromusic.service.MusicService.SAVE_QUEUES; + +class QueueSaveHandler extends Handler { + @NonNull + private final WeakReference mService; + + QueueSaveHandler(final MusicService service, @NonNull final Looper looper) { + super(looper); + mService = new WeakReference<>(service); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final MusicService service = mService.get(); + if (msg.what == SAVE_QUEUES) { + service.saveQueuesImpl(); + } + } +} diff --git a/app/src/main/java/code/name/monkey/retromusic/service/SongPlayCountHelper.java b/app/src/main/java/code/name/monkey/retromusic/service/SongPlayCountHelper.java new file mode 100644 index 000000000..463396fbd --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/SongPlayCountHelper.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service; + +import code.name.monkey.retromusic.helper.StopWatch; +import code.name.monkey.retromusic.model.Song; + +public class SongPlayCountHelper { + public static final String TAG = SongPlayCountHelper.class.getSimpleName(); + + private StopWatch stopWatch = new StopWatch(); + private Song song = Song.getEmptySong(); + + public Song getSong() { + return song; + } + + boolean shouldBumpPlayCount() { + return song.getDuration() * 0.5d < stopWatch.getElapsedTime(); + } + + void notifySongChanged(Song song) { + synchronized (this) { + stopWatch.reset(); + this.song = song; + } + } + + void notifyPlayStateChanged(boolean isPlaying) { + synchronized (this) { + if (isPlaying) { + stopWatch.start(); + } else { + stopWatch.pause(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/ThrottledSeekHandler.java b/app/src/main/java/code/name/monkey/retromusic/service/ThrottledSeekHandler.java new file mode 100644 index 000000000..20f075e70 --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/service/ThrottledSeekHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ + +package code.name.monkey.retromusic.service; + + +import android.os.Handler; + +import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED; + +public class ThrottledSeekHandler implements Runnable { + // milliseconds to throttle before calling run() to aggregate events + private static final long THROTTLE = 500; + private final MusicService musicService; + private Handler handler; + + ThrottledSeekHandler(MusicService musicService, Handler handler) { + this.musicService = musicService; + this.handler = handler; + } + + void notifySeek() { + handler.removeCallbacks(this); + handler.postDelayed(this, THROTTLE); + } + + @Override + public void run() { + musicService.savePositionInTrack(); + musicService.sendPublicIntent(PLAY_STATE_CHANGED); // for musixmatch synced lyrics + } +} \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/WearBrowserService.java b/app/src/main/java/code/name/monkey/retromusic/service/WearBrowserService.java index 5ef1df4b4..56c476408 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/WearBrowserService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/WearBrowserService.java @@ -24,12 +24,13 @@ import android.os.AsyncTask; import android.os.Bundle; import android.service.media.MediaBrowserService; -import java.util.ArrayList; -import java.util.List; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.List; + import code.name.monkey.retromusic.R; import code.name.monkey.retromusic.helper.MusicPlayerRemote; import code.name.monkey.retromusic.loaders.AlbumLoader; @@ -194,7 +195,7 @@ public class WearBrowserService extends MediaBrowserService { } else { switch (Integer.parseInt(Character.toString(parentId.charAt(0)))) { case TYPE_ARTIST: - List artistList = ArtistLoader.INSTANCE.getAllArtists(mContext).blockingFirst(); + List artistList = ArtistLoader.INSTANCE.getAllArtists(mContext) ; for (Artist artist : artistList) { String albumNmber = String.format("%d %s", artist.getAlbums().size(), artist.getAlbums().size() > 1 ? "Albums" : "Album"); String songCount = String.format("%d %s", artist.getSongs().size(), artist.getSongs().size() > 1 ? "Songs" : "Song"); @@ -215,7 +216,7 @@ public class WearBrowserService extends MediaBrowserService { Uri.parse("android.resource://code.name.monkey.retromusic/drawable/default_artist_art"), MediaBrowser.MediaItem.FLAG_BROWSABLE); - List artistAlbums = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getAlbums(); //ArtistAlbumLoader.getAlbumsForArtist(mContext, Long.parseLong(parentId.substring(1))); + List artistAlbums = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).getAlbums(); //ArtistAlbumLoader.getAlbumsForArtist(mContext, Long.parseLong(parentId.substring(1))); for (Album album : artistAlbums) { String songCount = String.format("%d %s", album.getSongs().size(), album.getSongs().size() > 1 ? "Songs" : "Song"); fillMediaItems(mediaItems, @@ -227,7 +228,7 @@ public class WearBrowserService extends MediaBrowserService { } break; case TYPE_ALBUM: - List albumList = AlbumLoader.Companion.getAllAlbums(mContext).blockingFirst(); + List albumList = AlbumLoader.INSTANCE.getAllAlbums(mContext); for (Album album : albumList) { fillMediaItems(mediaItems, Integer.toString(TYPE_ALBUM_SONGS) + Long.toString(album.getId()), @@ -238,7 +239,7 @@ public class WearBrowserService extends MediaBrowserService { } break; case TYPE_SONG: - List songList = SongLoader.INSTANCE.getAllSongs(mContext).blockingFirst(); + List songList = SongLoader.INSTANCE.getAllSongs(mContext); for (Song song : songList) { fillMediaItems(mediaItems, String.valueOf(song.getId()), @@ -250,7 +251,7 @@ public class WearBrowserService extends MediaBrowserService { break; case TYPE_ALBUM_SONGS: - List albumSongList = AlbumLoader.Companion.getAlbum(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getSongs(); + List albumSongList = AlbumLoader.INSTANCE.getAlbum(mContext, Integer.parseInt(parentId.substring(1))).getSongs(); for (Song song : albumSongList) { fillMediaItems(mediaItems, String.valueOf(song.getId()), @@ -261,7 +262,7 @@ public class WearBrowserService extends MediaBrowserService { } break; case TYPE_ARTIST_ALL_SONGS: - List artistSongs = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getSongs(); + List artistSongs = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).getSongs(); for (Song song : artistSongs) { fillMediaItems(mediaItems, String.valueOf(song.getId()), @@ -272,9 +273,9 @@ public class WearBrowserService extends MediaBrowserService { } break; case TYPE_PLAYLIST: - List playlistList = PlaylistLoader.INSTANCE.getAllPlaylists(mContext).blockingFirst(); + List playlistList = PlaylistLoader.INSTANCE.getAllPlaylists(mContext); for (Playlist playlist : playlistList) { - int size = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(mContext, playlist).blockingFirst().size(); + int size = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(mContext, playlist).size(); String songCount = String.format("%d %s", size, size > 1 ? "Songs" : "Song"); fillMediaItems(mediaItems, Integer.toString(TYPE_PLAYLIST_ALL_SONGS) + Long.toString(playlist.id), @@ -285,7 +286,7 @@ public class WearBrowserService extends MediaBrowserService { } break; case TYPE_PLAYLIST_ALL_SONGS: - List playlistSongs = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst(); + List playlistSongs = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(mContext, Integer.parseInt(parentId.substring(1))); for (Song song : playlistSongs) { fillMediaItems(mediaItems, String.valueOf(song.getId()), @@ -326,7 +327,7 @@ public class WearBrowserService extends MediaBrowserService { long songId = Long.parseLong(mediaId); setSessionActive(); ArrayList songs = new ArrayList<>(); - songs.add(SongLoader.INSTANCE.getSong(mContext, Integer.parseInt(mediaId)).blockingFirst()); + songs.add(SongLoader.INSTANCE.getSong(mContext, Integer.parseInt(mediaId))); MusicPlayerRemote.INSTANCE.openQueue(songs, 0, true); } diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt index 764c255da..2b53cb30e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt @@ -28,10 +28,6 @@ import android.widget.RemoteViews import androidx.core.app.NotificationCompat import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.retromusic.Constants.ACTION_QUIT -import code.name.monkey.retromusic.Constants.ACTION_REWIND -import code.name.monkey.retromusic.Constants.ACTION_SKIP -import code.name.monkey.retromusic.Constants.ACTION_TOGGLE_PAUSE import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.appwidgets.base.BaseAppWidget.Companion.createBitmap @@ -40,6 +36,7 @@ import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroColorUtil import code.name.monkey.retromusic.util.RetroUtil diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl24.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl24.kt index 3577b5833..e97f482be 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl24.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl24.kt @@ -24,17 +24,15 @@ import android.graphics.drawable.Drawable import android.os.Build import android.text.Html import androidx.core.app.NotificationCompat -import code.name.monkey.retromusic.Constants.ACTION_QUIT -import code.name.monkey.retromusic.Constants.ACTION_REWIND -import code.name.monkey.retromusic.Constants.ACTION_SKIP -import code.name.monkey.retromusic.Constants.ACTION_TOGGLE_PAUSE import code.name.monkey.retromusic.R +import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.service.MusicService -import code.name.monkey.retromusic.activities.MainActivity +import code.name.monkey.retromusic.service.MusicService.* +import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroColorUtil import com.bumptech.glide.request.target.Target @@ -48,12 +46,18 @@ class PlayingNotificationImpl24 : PlayingNotification() { val song = service.currentSong val isPlaying = service.isPlaying - + val isFavorite = MusicUtil.isFavorite(service, song) val playButtonResId = if (isPlaying) R.drawable.ic_pause_white_24dp else R.drawable.ic_play_arrow_white_32dp + val favoriteResId = if (isFavorite) + R.drawable.ic_favorite_white_24dp + else + R.drawable.ic_favorite_border_white_24dp + + val action = Intent(service, MainActivity::class.java) action.putExtra("expand", true) action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP @@ -93,6 +97,12 @@ class PlayingNotificationImpl24 : PlayingNotification() { if (bitmapFinal == null) { bitmapFinal = BitmapFactory.decodeResource(service.resources, R.drawable.default_album_art) } + + + val toggleFavorite = NotificationCompat.Action(favoriteResId, + service.getString(R.string.action_toggle_favorite), + retrievePlaybackAction(TOGGLE_FAVORITE)) + val playPauseAction = NotificationCompat.Action( playButtonResId, service.getString(R.string.action_play_pause), @@ -114,7 +124,7 @@ class PlayingNotificationImpl24 : PlayingNotification() { retrievePlaybackAction(ACTION_SKIP)) val builder = NotificationCompat.Builder(service, - PlayingNotification.NOTIFICATION_CHANNEL_ID) + NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setLargeIcon(bitmapFinal) .setContentIntent(clickIntent) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt index 15c24a5bd..84d3554ab 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt @@ -25,10 +25,6 @@ import android.widget.RemoteViews import androidx.core.app.NotificationCompat import code.name.monkey.appthemehelper.util.ColorUtil import code.name.monkey.appthemehelper.util.MaterialValueHelper -import code.name.monkey.retromusic.Constants.ACTION_QUIT -import code.name.monkey.retromusic.Constants.ACTION_REWIND -import code.name.monkey.retromusic.Constants.ACTION_SKIP -import code.name.monkey.retromusic.Constants.ACTION_TOGGLE_PAUSE import code.name.monkey.retromusic.R import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.glide.GlideApp @@ -37,6 +33,7 @@ import code.name.monkey.retromusic.glide.RetroSimpleTarget import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.model.Song import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.RetroUtil.createBitmap @@ -155,14 +152,14 @@ class PlayingNotificationOreo : PlayingNotification() { val primary = MaterialValueHelper.getPrimaryTextColor(service, dark) val secondary = MaterialValueHelper.getSecondaryTextColor(service, dark) - val close = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_close_white_24dp, primary)!!, PlayingNotification.NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) - val prev = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, primary)!!, PlayingNotification.NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) - val next = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, primary)!!, PlayingNotification.NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) + val close = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_close_white_24dp, primary)!!, NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) + val prev = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, primary)!!, NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) + val next = createBitmap(RetroUtil.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, primary)!!, NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) val playPause = createBitmap(RetroUtil.getTintedVectorDrawable(service, if (isPlaying) R.drawable.ic_pause_white_24dp else - R.drawable.ic_play_arrow_white_32dp, primary)!!, PlayingNotification.NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) + R.drawable.ic_play_arrow_white_32dp, primary)!!, NOTIFICATION_CONTROLS_SIZE_MULTIPLIER) notificationLayout.setTextColor(R.id.title, primary) notificationLayout.setTextColor(R.id.subtitle, secondary) diff --git a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java index 342e74d96..9476e86d1 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/FileUtil.java @@ -20,6 +20,9 @@ import android.os.Environment; import android.provider.MediaStore; import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -34,12 +37,9 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import code.name.monkey.retromusic.loaders.SongLoader; import code.name.monkey.retromusic.loaders.SortedCursor; import code.name.monkey.retromusic.model.Song; -import io.reactivex.Observable; public final class FileUtil { @@ -57,9 +57,10 @@ public final class FileUtil { stream.close(); return baos.toByteArray(); } + @NonNull - public static Observable> matchFilesWithMediaStore(@NonNull Context context, - @Nullable List files) { + public static ArrayList matchFilesWithMediaStore(@NonNull Context context, + @Nullable List files) { return SongLoader.INSTANCE.getSongs(makeSongCursor(context, files)); } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java index 1d1dfb3ee..e87d3b58e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/ImageUtil.java @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.util; +import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -21,13 +23,22 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; import android.media.ExifInterface; +import android.os.Build; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import code.name.monkey.appthemehelper.util.TintHelper; + /** * Created on : June 18, 2016 Author : zetbaitsu Name : Zetra GitHub : * https://github.com/zetbaitsu @@ -57,6 +68,37 @@ public class ImageUtil { return true; } + public static Bitmap createBitmap(Drawable drawable) { + return createBitmap(drawable, 1f); + } + + public static Bitmap createBitmap(Drawable drawable, float sizeMultiplier) { + Bitmap bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * sizeMultiplier), (int) (drawable.getIntrinsicHeight() * sizeMultiplier), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bitmap); + drawable.setBounds(0, 0, c.getWidth(), c.getHeight()); + drawable.draw(c); + return bitmap; + } + + public static Drawable getTintedVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme, @ColorInt int color) { + return TintHelper.createTintedDrawable(getVectorDrawable(res, resId, theme), color); + } + + public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id, @ColorInt int color) { + return TintHelper.createTintedDrawable(getVectorDrawable(context.getResources(), id, context.getTheme()), color); + } + + public static Drawable getVectorDrawable(@NonNull Context context, @DrawableRes int id) { + return getVectorDrawable(context.getResources(), id, context.getTheme()); + } + + public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) { + if (Build.VERSION.SDK_INT >= 21) { + return res.getDrawable(resId, theme); + } + return VectorDrawableCompat.create(res, resId, theme); + } + /** * Makes sure that {@code mTempBuffer} has at least length {@code size}. */ diff --git a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java index 76bc6b922..9c2d4a4f8 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.java @@ -51,7 +51,6 @@ import code.name.monkey.retromusic.model.Genre; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.Song; import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics; -import io.reactivex.Observable; public class MusicUtil { @@ -303,7 +302,7 @@ public class MusicUtil { cursor.moveToFirst(); while (!cursor.isAfterLast()) { final int id = cursor.getInt(0); - final Song song = SongLoader.INSTANCE.getSong(activity, id).blockingFirst(); + final Song song = SongLoader.INSTANCE.getSong(activity, id); MusicPlayerRemote.INSTANCE.removeFromQueue(song); cursor.moveToNext(); } @@ -404,9 +403,9 @@ public class MusicUtil { public static void toggleFavorite(@NonNull final Context context, @NonNull final Song song) { if (isFavorite(context, song)) { - PlaylistsUtil.removeFromPlaylist(context, song, getFavoritesPlaylist(context).blockingFirst().id); + PlaylistsUtil.removeFromPlaylist(context, song, getFavoritesPlaylist(context).id); } else { - PlaylistsUtil.addToPlaylist(context, song, getOrCreateFavoritesPlaylist(context).blockingFirst().id, + PlaylistsUtil.addToPlaylist(context, song, getOrCreateFavoritesPlaylist(context).id, false); } } @@ -416,18 +415,18 @@ public class MusicUtil { return playlist.name != null && playlist.name.equals(context.getString(R.string.favorites)); } - private static Observable getFavoritesPlaylist(@NonNull final Context context) { + private static Playlist getFavoritesPlaylist(@NonNull final Context context) { return PlaylistLoader.INSTANCE.getPlaylist(context, context.getString(R.string.favorites)); } - private static Observable getOrCreateFavoritesPlaylist(@NonNull final Context context) { + private static Playlist getOrCreateFavoritesPlaylist(@NonNull final Context context) { return PlaylistLoader.INSTANCE.getPlaylist(context, PlaylistsUtil.createPlaylist(context, context.getString(R.string.favorites))); } public static boolean isFavorite(@NonNull final Context context, @NonNull final Song song) { return PlaylistsUtil - .doPlaylistContains(context, getFavoritesPlaylist(context).blockingFirst().id, song.getId()); + .doPlaylistContains(context, getFavoritesPlaylist(context).id, song.getId()); } public static boolean isArtistNameUnknown(@Nullable String artistName) { @@ -475,6 +474,15 @@ public class MusicUtil { return duration; } + public static int indexOfSongInList(List songs, int songId) { + for (int i = 0; i < songs.size(); i++) { + if (songs.get(i).getId() == songId) { + return i; + } + } + return -1; + } + @NonNull public static String getYearString(int year) { return year > 0 ? String.valueOf(year) : "-"; diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PackageValidator.kt b/app/src/main/java/code/name/monkey/retromusic/util/PackageValidator.kt new file mode 100644 index 000000000..d6df60add --- /dev/null +++ b/app/src/main/java/code/name/monkey/retromusic/util/PackageValidator.kt @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2019 Hemanth Savarala. + * + * Licensed under the GNU General Public License v3 + * + * This is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by + * the Free Software Foundation either version 3 of the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + */ +package code.name.monkey.retromusic.util + +import android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE +import android.Manifest.permission.MEDIA_CONTENT_CONTROL +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED +import android.content.pm.PackageManager +import android.content.res.XmlResourceParser +import android.os.Process +import android.support.v4.media.session.MediaSessionCompat +import android.util.Base64 +import android.util.Log +import androidx.annotation.XmlRes +import androidx.media.MediaBrowserServiceCompat +import code.name.monkey.retromusic.BuildConfig +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat]. + * + * The list of allowed signing certificates and their corresponding package names is defined in + * res/xml/allowed_media_browser_callers.xml. + * + * If you want to add a new caller to allowed_media_browser_callers.xml and you don't know + * its signature, this class will print to logcat (INFO level) a message with the proper + * xml tags to add to allow the caller. + * + * For more information, see res/xml/allowed_media_browser_callers.xml. + */ +class PackageValidator(context: Context, @XmlRes xmlResId: Int) { + private val context: Context + private val packageManager: PackageManager + + private val certificateWhitelist: Map + private val platformSignature: String + + private val callerChecked = mutableMapOf>() + + init { + val parser = context.resources.getXml(xmlResId) + this.context = context.applicationContext + this.packageManager = this.context.packageManager + + certificateWhitelist = buildCertificateWhitelist(parser) + platformSignature = getSystemSignature() + } + + /** + * Checks whether the caller attempting to connect to a [MediaBrowserServiceCompat] is known. + * See [MusicService.onGetRoot] for where this is utilized. + * + * @param callingPackage The package name of the caller. + * @param callingUid The user id of the caller. + * @return `true` if the caller is known, `false` otherwise. + */ + fun isKnownCaller(callingPackage: String, callingUid: Int): Boolean { + // If the caller has already been checked, return the previous result here. + val (checkedUid, checkResult) = callerChecked[callingPackage] ?: Pair(0, false) + if (checkedUid == callingUid) { + return checkResult + } + + /** + * Because some of these checks can be slow, we save the results in [callerChecked] after + * this code is run. + * + * In particular, there's little reason to recompute the calling package's certificate + * signature (SHA-256) each call. + * + * This is safe to do as we know the UID matches the package's UID (from the check above), + * and app UIDs are set at install time. Additionally, a package name + UID is guaranteed to + * be constant until a reboot. (After a reboot then a previously assigned UID could be + * reassigned.) + */ + + // Build the caller info for the rest of the checks here. + val callerPackageInfo = buildCallerInfo(callingPackage) + ?: throw IllegalStateException("Caller wasn't found in the system?") + + // Verify that things aren't ... broken. (This test should always pass.) + if (callerPackageInfo.uid != callingUid) { + throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?") + } + + val callerSignature = callerPackageInfo.signature + val isPackageInWhitelist = certificateWhitelist[callingPackage]?.signatures?.first { + it.signature == callerSignature + } != null + + val isCallerKnown = when { + // If it's our own app making the call, allow it. + callingUid == Process.myUid() -> true + // If it's one of the apps on the whitelist, allow it. + isPackageInWhitelist -> true + // If the system is making the call, allow it. + callingUid == Process.SYSTEM_UID -> true + // If the app was signed by the same certificate as the platform itself, also allow it. + callerSignature == platformSignature -> true + /** + * [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and + * while it isn't required to allow these apps to connect to a + * [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps + * such as Android TV and the Google Assistant. + */ + callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true + /** + * This last permission can be specifically granted to apps, and, in addition to + * allowing them to retrieve notifications, it also allows them to connect to an + * active [MediaSessionCompat]. + * As with the above, it's not required to allow apps holding this permission to + * connect to your [MediaBrowserServiceCompat], but it does allow easy comparability + * with apps such as Wear OS. + */ + callerPackageInfo.permissions.contains(BIND_NOTIFICATION_LISTENER_SERVICE) -> true + // If none of the pervious checks succeeded, then the caller is unrecognized. + else -> false + } + + if (!isCallerKnown) { + logUnknownCaller(callerPackageInfo) + } + + // Save our work for next time. + callerChecked[callingPackage] = Pair(callingUid, isCallerKnown) + return isCallerKnown + } + + /** + * Logs an info level message with details of how to add a caller to the allowed callers list + * when the app is debuggable. + */ + private fun logUnknownCaller(callerPackageInfo: CallerPackageInfo) { + if (BuildConfig.DEBUG && callerPackageInfo.signature != null) { + Log.i(TAG, "PackageValidator call" + callerPackageInfo.name + callerPackageInfo.packageName + callerPackageInfo.signature) + } + } + + /** + * Builds a [CallerPackageInfo] for a given package that can be used for all the + * various checks that are performed before allowing an app to connect to a + * [MediaBrowserServiceCompat]. + */ + private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? { + val packageInfo = getPackageInfo(callingPackage) ?: return null + + val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString() + val uid = packageInfo.applicationInfo.uid + val signature = getSignature(packageInfo) + + val requestedPermissions = packageInfo.requestedPermissions + val permissionFlags = packageInfo.requestedPermissionsFlags + val activePermissions = mutableSetOf() + requestedPermissions?.forEachIndexed { index, permission -> + if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) { + activePermissions += permission + } + } + + return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet()) + } + + /** + * Looks up the [PackageInfo] for a package name. + * This requests both the signatures (for checking if an app is on the whitelist) and + * the app's permissions, which allow for more flexibility in the whitelist. + * + * @return [PackageInfo] for the package name or null if it's not found. + */ + @SuppressLint("PackageManagerGetSignatures") + private fun getPackageInfo(callingPackage: String): PackageInfo? = + packageManager.getPackageInfo(callingPackage, + PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS) + + /** + * Gets the signature of a given package's [PackageInfo]. + * + * The "signature" is a SHA-256 hash of the public key of the signing certificate used by + * the app. + * + * If the app is not found, or if the app does not have exactly one signature, this method + * returns `null` as the signature. + */ + private fun getSignature(packageInfo: PackageInfo): String? { + // Security best practices dictate that an app should be signed with exactly one (1) + // signature. Because of this, if there are multiple signatures, reject it. + if (packageInfo.signatures == null || packageInfo.signatures.size != 1) { + return null + } else { + val certificate = packageInfo.signatures[0].toByteArray() + return getSignatureSha256(certificate) + } + } + + private fun buildCertificateWhitelist(parser: XmlResourceParser): Map { + + val certificateWhitelist = LinkedHashMap() + try { + var eventType = parser.next() + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG) { + val callerInfo = when (parser.name) { + "signing_certificate" -> parseV1Tag(parser) + "signature" -> parseV2Tag(parser) + else -> null + } + + callerInfo?.let { info -> + val packageName = info.packageName + val existingCallerInfo = certificateWhitelist[packageName] + if (existingCallerInfo != null) { + existingCallerInfo.signatures += callerInfo.signatures + } else { + certificateWhitelist[packageName] = callerInfo + } + } + } + + eventType = parser.next() + } + } catch (xmlException: XmlPullParserException) { + Log.e(TAG, "Could not read allowed callers from XML.", xmlException) + } catch (ioException: IOException) { + Log.e(TAG, "Could not read allowed callers from XML.", ioException) + } + + return certificateWhitelist + } + + /** + * Parses a v1 format tag. See allowed_media_browser_callers.xml for more details. + */ + private fun parseV1Tag(parser: XmlResourceParser): KnownCallerInfo { + val name = parser.getAttributeValue(null, "name") + val packageName = parser.getAttributeValue(null, "package") + val isRelease = parser.getAttributeBooleanValue(null, "release", false) + val certificate = parser.nextText().replace(WHITESPACE_REGEX, "") + val signature = getSignatureSha256(certificate) + + val callerSignature = KnownSignature(signature, isRelease) + return KnownCallerInfo(name, packageName, mutableSetOf(callerSignature)) + } + + /** + * Parses a v2 format tag. See allowed_media_browser_callers.xml for more details. + */ + private fun parseV2Tag(parser: XmlResourceParser): KnownCallerInfo { + val name = parser.getAttributeValue(null, "name") + val packageName = parser.getAttributeValue(null, "package") + + val callerSignatures = mutableSetOf() + var eventType = parser.next() + while (eventType != XmlResourceParser.END_TAG) { + val isRelease = parser.getAttributeBooleanValue(null, "release", false) + val signature = parser.nextText().replace(WHITESPACE_REGEX, "").toLowerCase() + callerSignatures += KnownSignature(signature, isRelease) + + eventType = parser.next() + } + + return KnownCallerInfo(name, packageName, callerSignatures) + } + + /** + * Finds the Android platform signing key signature. This key is never null. + */ + private fun getSystemSignature(): String = + getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo -> + getSignature(platformInfo) + } ?: throw IllegalStateException("Platform signature not found") + + /** + * Creates a SHA-256 signature given a Base64 encoded certificate. + */ + private fun getSignatureSha256(certificate: String): String { + return getSignatureSha256(Base64.decode(certificate, Base64.DEFAULT)) + } + + /** + * Creates a SHA-256 signature given a certificate byte array. + */ + private fun getSignatureSha256(certificate: ByteArray): String { + val md: MessageDigest + try { + md = MessageDigest.getInstance("SHA256") + } catch (noSuchAlgorithmException: NoSuchAlgorithmException) { + Log.e(TAG, "No such algorithm: $noSuchAlgorithmException") + throw RuntimeException("Could not find SHA256 hash algorithm", noSuchAlgorithmException) + } + md.update(certificate) + + // This code takes the byte array generated by `md.digest()` and joins each of the bytes + // to a string, applying the string format `%02x` on each digit before it's appended, with + // a colon (':') between each of the items. + // For example: input=[0,2,4,6,8,10,12], output="00:02:04:06:08:0a:0c" + return md.digest().joinToString(":") { String.format("%02x", it) } + } + + private data class KnownCallerInfo( + internal val name: String, + internal val packageName: String, + internal val signatures: MutableSet + ) + + private data class KnownSignature( + internal val signature: String, + internal val release: Boolean + ) + + /** + * Convenience class to hold all of the information about an app that's being checked + * to see if it's a known caller. + */ + private data class CallerPackageInfo( + internal val name: String, + internal val packageName: String, + internal val uid: Int, + internal val signature: String?, + internal val permissions: Set + ) +} + +private const val TAG = "PackageValidator" +private const val ANDROID_PLATFORM = "android" +private val WHITESPACE_REGEX = "\\s|\\n".toRegex() diff --git a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java index 9a425637b..9208e632b 100644 --- a/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/PlaylistsUtil.java @@ -38,7 +38,6 @@ import code.name.monkey.retromusic.helper.M3UWriter; import code.name.monkey.retromusic.model.Playlist; import code.name.monkey.retromusic.model.PlaylistSong; import code.name.monkey.retromusic.model.Song; -import io.reactivex.Observable; import static android.provider.MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; @@ -262,7 +261,9 @@ public class PlaylistsUtil { return ""; } - public static Observable savePlaylist(Context context, Playlist playlist) throws IOException { + @Nullable + public static File savePlaylist(@NonNull Context context, + @NonNull Playlist playlist) throws IOException { return M3UWriter.write(context, new File(Environment.getExternalStorageDirectory(), "Playlists"), playlist); } diff --git a/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java b/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java index c499ef4a7..3d324d8d3 100755 --- a/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java +++ b/app/src/main/java/code/name/monkey/retromusic/util/RetroUtil.java @@ -61,10 +61,8 @@ import java.util.Collections; import java.util.List; import code.name.monkey.appthemehelper.ThemeStore; -import code.name.monkey.appthemehelper.util.ATHUtil; import code.name.monkey.appthemehelper.util.TintHelper; import code.name.monkey.retromusic.App; -import code.name.monkey.retromusic.R; public class RetroUtil { @@ -176,6 +174,7 @@ public class RetroUtil { } } + @Nullable public static Drawable getVectorDrawable(@NonNull Resources res, @DrawableRes int resId, @Nullable Resources.Theme theme) { if (Build.VERSION.SDK_INT >= 21) { @@ -184,11 +183,11 @@ public class RetroUtil { return VectorDrawableCompat.create(res, resId, theme); } + @Nullable public static Drawable getTintedVectorDrawable(@NonNull Context context, @DrawableRes int id, @ColorInt int color) { - return TintHelper - .createTintedDrawable(getVectorDrawable(context.getResources(), id, context.getTheme()), - color); + return TintHelper.createTintedDrawable( + getVectorDrawable(context.getResources(), id, context.getTheme()), color); } public static Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int id, diff --git a/app/src/main/res/layout-land/activity_artist_details.xml b/app/src/main/res/layout-land/activity_artist_details.xml index c1fec3fe0..ddc1940d8 100644 --- a/app/src/main/res/layout-land/activity_artist_details.xml +++ b/app/src/main/res/layout-land/activity_artist_details.xml @@ -69,7 +69,7 @@ tools:ignore="MissingPrefix" tools:text="Title" /> - - - - Do not open any sub-folders Deleting songs Hemanth Savarala + + Albums + Artists + Playlists + History + Top Tracks + Queue + Cycle repeat mode + Toggle shuffle mode + Toggle favorite diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d0f0e17ff..5fe800bc5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -150,4 +150,12 @@ + + diff --git a/app/src/main/res/xml/allowed_media_browser_callers.xml b/app/src/main/res/xml/allowed_media_browser_callers.xml new file mode 100644 index 000000000..a8861593c --- /dev/null +++ b/app/src/main/res/xml/allowed_media_browser_callers.xml @@ -0,0 +1,309 @@ + + + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIDvTCCAqWgAwIBAgIJAOfkBvDXw5bzMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwMjUxWhcNNDExMDEyMjMwMjUxWjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAou7wwBKFyznqpRretJ3EVp55/Yr049Ag5wlGvrCnjIP8DrMrU+skfKe1 + DmwpsLNtnhhiNH+J000Lok3hc8jdWKeKOopzKGDNvL/HvnS70Zyk26gj9jtMMHz9 + 2aZdpmwD67FNmTlG2FERr+TwMD5agaPnsFR2zla6ugUvHGzz65YDxpCZsQ/TowyD + LnxgMagvhvS+Oex3yh2FN7pJfwS03KdGdkWPbLqf9Fem09s5jjeZW/O3RgnKoRPI + J4QLK70efjAZqJyBGcDZyQMwOs+8HIknraf8+cRZJDzqOx7rttl8M3KGB2EFljTp + 6/FyxJLnAo6QlXn7GrYalTI0yLU9dQIDAQABo1AwTjAdBgNVHQ4EFgQU9QPJ5xJE + DA8MDQMrj0hm2/A2BRkwHwYDVR0jBBgwFoAU9QPJ5xJEDA8MDQMrj0hm2/A2BRkw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEADcr5h1FR8IpmN4hSsUA9 + SnCQVyXa1GQhzpQgRbF+npkgOn2Mebp8bd28VpfgooD2OBNQXCUcZkn7pWj++ut9 + HhObHVaV5FNg0pdDqLna9QZ9Y4oS+ZrijK70XZ/EjlYUHvhu0pIjZAbD8CmCFlow + SR55qCSjM5iS37LZB32SMr1BBiYrNAvncKjYQVK8ctTRzhpNQQPBgXBA98Xl+d1D + Py00JWQuF0ssmhKcJuvfdEnFF7Hvaxz/gCQ9nzarQI3CJB8dOXVwF8mcyDRBz4JR + +YDpXo6BD+fGt15ov+zmqC8xaT9P1/JgoDXiMhy/6rwgdi9WxPf8mb7TnBC+CksX + 0A== + + + MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD + Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+ + 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM + oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm + k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6 + 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu + 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/ + wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m + 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9 + QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC + 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/ + yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W + IQ== + + + MIIDvzCCAqegAwIBAgIJANqYw9kVc9PvMA0GCSqGSIb3DQEBBQUAMHYxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDESMBAG + A1UEAwwJQ2xvY2tXb3JrMB4XDTE0MDMwNzIyMDExOFoXDTQxMDcyMzIyMDExOFow + djELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1v + dW50YWluIFZpZXcxFDASBgNVBAoMC0dvb2dsZSBJbmMuMRAwDgYDVQQLDAdBbmRy + b2lkMRIwEAYDVQQDDAlDbG9ja1dvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw + ggEKAoIBAQDcHW9LKO04MBSynIL22v/THd57jB5jCEBlC1ixZaNqrrYscVOVLgRF + Ca+CH5S6n08YZMOntdZTzAAVnQAQ4eVm+jeq/xg2Xa57SoXdsfODzEdwoj6VYpH+ + tXLBaTFar0706qWuhh/N1ufl6tQxE3RGRgx8KPsyLJKVXFx6qJV3w3A/l+CYt362 + oG6sa3LqoK0hCrAqH9z8dmJ0dEGpPzzqihb0jJciweMyQTJ+wsn3MDEujRvv7ikL + RRo0iSys71sUctbZfvlUKMyK1e8EuMTx9Q3SQtVdclhmhVBbXksbHlmtjB2FL6CC + SBVnO8bmQynsxOrU24RkqWsxg5+f28kHAgMBAAGjUDBOMB0GA1UdDgQWBBSEhUcQ + hKQ8s+r4P6shYqCVAM4sejAfBgNVHSMEGDAWgBSEhUcQhKQ8s+r4P6shYqCVAM4s + ejAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAHOWILon0qD1SIQ60b + YI4cKdkBKIHq/D/WKF9fYmqXPvBX5pfusqxcouYFyj0z9ZCZa4sAMsRH5lAPJb0X + yvmVAzmDQMjubNy1O+3ksfJI59AgmZ6B58rqpTLP2pn+SqXtQEBORPdb79J/yts1 + uLIblHhGXhci8nr7KwtuFY5ExKsMT2V7Gdd9j1PJz7nuLU9FtlTgEryN6YHkwuLD + 055RkwPYrk0swchijXhXrnU/HXsCo6cFeMYF4wUcbB2pSRrOE7uI0H2BfdSUJlGX + hK6WlaRHNQ2J60BekPKr82auL8pY0va/Hb9LHEie4KABVN/PAiUS9aHHIp5zHePw + R9b4 + + + MIIFYTCCA0sCBgFEnpGW0zALBgkqhkiG9w0BAQUwdjELMAkGA1UEBhMCVVMxEzAR + BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNV + BAoTC0dvb2dsZSBJbmMuMRAwDgYDVQQLEwdBbmRyb2lkMRIwEAYDVQQDEwlDbG9j + a1dvcmswHhcNMTQwMzA3MjIwMjI1WhcNMzgwMTE5MDMxNDA3WjB2MQswCQYDVQQG + EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll + dzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEjAQBgNV + BAMTCUNsb2NrV29yazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALo8 + fzkL/lmKYrz8izyUxidamRXt3N03OlVqmQvi/UP3nxizAdJAJ+NyfwnO/eKcfCaw + iiDeNn0a3+NOp4+uN/OQ9eAmcnpOCCg773b49kO8FSc2oEg/ybeRq1I8872Ge2Yt + KidMANiQ550R6LAmX+2pddzI5UKZiY6QE2picYwuCy85eVHbJLFXob/nxWvOSjgL + Jfq5JmM+qJQEOOC2lAu5nol+LvoFPDIpm9lhb6S9loIhezdDH83Ygu0hp/LwRn/g + lRy8Wphi40oVa+FaF/8CF5hkRNYTsR7XX4OAGO60/ZTkj1rjHOSvpDY4tpcshVzS + 2woBxDJhKOTFGXq+rMxtwuitpEJfD5DVpaVYJGG/eBHhLs4O6gYDP5ZUOe3gcf/E + bCDy374jIzp/ZMHOCa2hy85r9ryiLpuYnErAyWqdbHVP7Bhx2HsQmMGg3mC8fXfe + MNVOuEfOaxJ8GR6nk28KRsFG8za5NOq6Wl8cA2S3UpZVRDJQ/WOq5Xvrq+AmPwkI + TRlEBgw62bu6f3n03jwrLTe6sw1LuRHcUWngr5VS9NOOPbPyy4AcUgJScGG/AbBC + 0H6J5I8RqaqgJ/BElZ7aKMXd2FNXpx45u4JRs1frb3IY/MwXGIGmMGdGMeBVlDka + emea8lqgYgHWIrjQCd0R1QaiAw8kJ65n2Bs0N3l5AgMBAAEwCwYJKoZIhvcNAQEF + A4ICAQCkxJaWNGHIlTWlsQrNASQ3aonaJ0OdrDADSGcLICut4z8vuioHZAO1C+hx + yiqym769u8QG2wk5QcmMF2oORv+Q/wAWFgREgG7cguEw/hCGHuMFnbd+PZ2poq00 + qdK02hsm/VpbcBzVbP7pZHrkFDuXpnwCgGWxf54U8jKl7xfhZKFJF5KWlBwwvVo2 + q/jzQsjjr7xvSUNzB31qnBHXOSINWte1GS+bHP6Wj0pysbhUdeDpiL5ocohmZbEr + 9O7DIlZU9eHyK4vrVY6+ZneL1l8JkS35XoCd/u8Px+rKXQ6+HUEEH+cgyzKbMH45 + LhOX8SA5VGkwhIt/AhdAiS32x6By5984usPXIjVv5lR/anxXit9nyT0rNYiTVDXw + +aETzi3szW2hncNLQYLsrtYg61KFMCXF4ATstFG8ReFIWsw2f7ZJkq9ZTFUbC8k4 + y9Ya1WdZkCD3OmXhqcikiNusgx7rkY8MkikJXt5BBXs8rupOFsW5RUuS4lmKEbSU + oh8/er+DwGf0GC6YQZuk5JOKNIDwhi/tr1dySlUzV4/aX7PN/PlUgH//2MlRd+d1 + BKZCvlzboOEAZfx8aBKc7SezqATXpM3ZDNPsywWoyIpgmtBWoE60ih4Flf05XB+n + e7MdpSQ0Xgq9TgG1BoJP6rpC0y3Ukmc+z8AXnYYdJunNXEbv0A== + + + MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD + Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+ + 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM + oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm + k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6 + 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu + 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/ + wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m + 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9 + QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC + 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/ + yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W + IQ== + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD + VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g + VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE + AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe + Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET + MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G + A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p + ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI + hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR + 24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy + xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X + W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC + 69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA + cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw + HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c + xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE + CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH + QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG + CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud + EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP + zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla + XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a + IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a + ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW + Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs= + + + MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV + BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW + aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G + A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1 + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 + bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv + aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD + Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+ + 02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM + oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm + k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6 + 67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu + 6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw + DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/ + wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m + 31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9 + QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC + 1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/ + yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W + IQ== + + \ No newline at end of file