Rx Java and Normal compatible for all query

This commit is contained in:
h4h13 2019-08-02 14:29:40 +05:30
parent 850036e5cc
commit c2759e3ec0
89 changed files with 2900 additions and 1040 deletions

View file

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

View file

@ -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<*, *>>(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()

View file

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

View file

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

View file

@ -170,7 +170,7 @@ class SearchActivity : AbsMusicServiceActivity(), OnQueryTextListener, SearchCon
}
override fun showData(list: ArrayList<Any>) {
override fun showData(list: MutableList<Any>) {
searchAdapter!!.swapDataSet(list)
}

View file

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

View file

@ -195,7 +195,7 @@ class AlbumTagEditorActivity : AbsTagEditorActivity(), TextWatcher {
}
override fun getSongPaths(): List<String> {
val songs = AlbumLoader.getAlbum(this, id).blockingFirst().songs
val songs = AlbumLoader.getAlbum(this, id).songs
val paths = ArrayList<String>(songs!!.size)
for (song in songs) {
paths.add(song.data)

View file

@ -108,7 +108,7 @@ class SongTagEditorActivity : AbsTagEditorActivity(), TextWatcher {
override fun getSongPaths(): List<String> {
val paths = ArrayList<String>(1)
paths.add(SongLoader.getSong(this, id).blockingFirst().data)
paths.add(SongLoader.getSong(this, id).data)
return paths
}

View file

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

View file

@ -23,7 +23,7 @@ import java.util.*
class SearchAdapter(private val activity: AppCompatActivity, private var dataSet: List<Any>?) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() {
fun swapDataSet(dataSet: ArrayList<Any>) {
fun swapDataSet(dataSet: MutableList<Any>) {
this.dataSet = dataSet
notifyDataSetChanged()
}

View file

@ -149,10 +149,9 @@ class PlaylistAdapter(protected val activity: AppCompatActivity, dataSet: ArrayL
val songs = ArrayList<Song>()
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<Song> {
val songs = ArrayList<Song>()
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
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
* <p/>
* Encode the media browseable categories, if any, and the unique music ID, if any,
* into a single String mediaID.
* <p/>
* MediaIDs are of the form <categoryType>__/__<categoryValue>__|__<musicUniqueId>, 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));
}
}

View file

@ -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<MusicService> mMusicService;
// Categorized caches for music data
private ConcurrentMap<Integer, Uri> mMusicListByHistory;
private ConcurrentMap<Integer, Uri> mMusicListByTopTracks;
private ConcurrentMap<Integer, Uri> mMusicListByPlaylist;
private ConcurrentMap<Integer, Uri> mMusicListByAlbum;
private ConcurrentMap<Integer, Uri> 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<Uri> getHistory() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
return mMusicListByHistory.values();
}
public Iterable<Uri> getTopTracks() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
return mMusicListByTopTracks.values();
}
public Iterable<Uri> getPlaylists() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
return mMusicListByPlaylist.values();
}
public Iterable<Uri> getAlbums() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
return mMusicListByAlbum.values();
}
public Iterable<Uri> getArtists() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
return mMusicListByArtist.values();
}
public Iterable<Uri> getQueue() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
// Re-built every time since the queue updates often
ConcurrentMap<Integer, Uri> queueList = new ConcurrentSkipListMap<>();
if (mContext != null) {
final List<Song> 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<Void, Void, State>() {
@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<Integer, Uri> newMusicListByHistory = new ConcurrentSkipListMap<>();
final List<Song> 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<Integer, Uri> newMusicListByTopTracks = new ConcurrentHashMap<>();
final List<Song> 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<Integer, Uri> newMusicListByPlaylist = new ConcurrentSkipListMap<>();
final List<Playlist> 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<Integer, Uri> newMusicListByAlbum = new ConcurrentSkipListMap<>();
final List<Album> 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<Integer, Uri> newMusicListByArtist = new ConcurrentSkipListMap<>();
final List<Artist> 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<MediaBrowserCompat.MediaItem> getChildren(String mediaId, Resources resources) {
List<MediaBrowserCompat.MediaItem> 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);
}
}

View file

@ -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<String> = mutableListOf()
playlistNames.add(cntx.resources.getString(R.string.action_new_playlist))
for (p in playlists) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = ",";
}

View file

@ -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<File> 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<? extends Song> 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<Song> 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;
}
}

View file

@ -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<Song>? = 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()) {

View file

@ -38,52 +38,52 @@ object SearchQueryHelper {
var songs = ArrayList<Song>()
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()
}

View file

@ -51,6 +51,6 @@ object GenreMenuHelper {
}
private fun getGenreSongs(activity: Activity, genre: Genre): ArrayList<Song> {
return GenreLoader.getSongs(activity, genre.id).blockingFirst()
return GenreLoader.getSongs(activity, genre.id)
}
}

View file

@ -77,20 +77,18 @@ object PlaylistMenuHelper {
private fun getPlaylistSongs(activity: Activity,
playlist: Playlist): ArrayList<Song> {
val songs: ArrayList<Song>
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<Playlist, String, String>(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) {

View file

@ -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<ArrayList<Album>> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context, null, null,
getSongLoaderSortOrder())
)
object AlbumLoader {
fun getAllAlbumsFlowable(
context: Context
): Observable<ArrayList<Album>> {
val songs = SongLoader.getSongsFlowable(SongLoader.makeSongCursor(
context, null, null,
getSongLoaderSortOrder())
)
return splitIntoAlbums(songs)
}
return splitIntoAlbumsFlowable(songs)
}
fun getAlbums(context: Context,
query: String): Observable<ArrayList<Album>> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context,
AudioColumns.ALBUM + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder())
)
return splitIntoAlbums(songs)
}
fun getAlbumsFlowable(context: Context, query: String): Observable<ArrayList<Album>> {
val songs = SongLoader.getSongsFlowable(SongLoader.makeSongCursor(
context,
AudioColumns.ALBUM + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder())
)
return splitIntoAlbumsFlowable(songs)
}
fun getAlbum(context: Context, albumId: Int): Observable<Album> {
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<Album> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context,
AudioColumns.ALBUM + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder())
)
return splitIntoAlbums(songs)
}
fun splitIntoAlbums(
songs: Observable<ArrayList<Song>>?): Observable<ArrayList<Album>> {
return Observable.create { e ->
val albums = ArrayList<Album>()
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<Album> {
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<Song>?): ArrayList<Album> {
val albums = ArrayList<Album>()
if (songs != null) {
for (song in songs) {
getOrCreateAlbum(albums, song.albumId).subscribe { album -> album.songs!!.add(song) }
}
}
return albums
}
private fun getOrCreateAlbum(albums: ArrayList<Album>, albumId: Int): Observable<Album> {
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<ArrayList<Song>>?
): Observable<ArrayList<Album>> {
return Observable.create { e ->
val albums = ArrayList<Album>()
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<Album> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context, null, null,
getSongLoaderSortOrder())
)
return splitIntoAlbums(songs)
}
fun splitIntoAlbums(
songs: ArrayList<Song>?
): ArrayList<Album> {
val albums = ArrayList<Album>()
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<Album>,
albumId: Int
): Observable<Album> {
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<Album>,
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
}
}

View file

@ -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<Artist> {
fun getAllArtistsFlowable(
context: Context
): Observable<ArrayList<Artist>> {
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<ArrayList<Artist>> {
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<Artist> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context,
null, null,
getSongLoaderSortOrder())
)
return splitIntoArtists(AlbumLoader.splitIntoAlbums(songs))
}
fun getArtists(context: Context, query: String): Observable<ArrayList<Artist>> {
fun getArtistsFlowable(context: Context, query: String): Observable<ArrayList<Artist>> {
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<Artist> {
val songs = SongLoader.getSongs(SongLoader.makeSongCursor(
context,
AudioColumns.ARTIST + " LIKE ?",
arrayOf("%$query%"),
getSongLoaderSortOrder())
)
return splitIntoArtists(AlbumLoader.splitIntoAlbums(songs))
}
fun splitIntoArtists(albums: ArrayList<Album>?): ArrayList<Artist> {
val artists = ArrayList<Artist>()
if (albums != null) {
@ -83,7 +89,7 @@ object ArtistLoader {
private fun getOrCreateArtist(artists: ArrayList<Artist>, 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<Artist> {
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))
}
}

View file

@ -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<ArrayList<Genre>> {
fun getAllGenresFlowable(context: Context): Observable<ArrayList<Genre>> {
return getGenresFromCursorFlowable(context, makeGenreCursor(context))
}
fun getAllGenres(context: Context): ArrayList<Genre> {
return getGenresFromCursor(context, makeGenreCursor(context))
}
fun getSongs(context: Context, genreId: Int): Observable<ArrayList<Song>> {
fun getSongsFlowable(context: Context, genreId: Int): Observable<ArrayList<Song>> {
// 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<Song> {
// 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<ArrayList<Song>> {
private fun getSongsWithNoGenreFlowable(context: Context): Observable<ArrayList<Song>> {
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<Song> {
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<ArrayList<Genre>> {
private fun getGenresFromCursorFlowable(context: Context, cursor: Cursor?): Observable<ArrayList<Genre>> {
return Observable.create { e ->
val genres = ArrayList<Genre>()
if (cursor != null) {
@ -120,6 +140,31 @@ object GenreLoader {
}
}
private fun getGenresFromCursor(context: Context, cursor: Cursor?): ArrayList<Genre> {
val genres = arrayListOf<Genre>()
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)

View file

@ -25,47 +25,5 @@ import io.reactivex.Observable
object HomeLoader {
fun getRecentAndTopThings(context: Context): Observable<ArrayList<AbsSmartPlaylist>> {
val objects = ArrayList<AbsSmartPlaylist>()
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<ArrayList<Playlist>> {
val playlists = ArrayList<Playlist>()
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)
}
}

View file

@ -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<ArrayList<Song>> {
fun getLastAddedSongsFlowable(context: Context): Observable<ArrayList<Song>> {
return SongLoader.getSongsFlowable(makeLastAddedCursor(context))
}
fun getLastAddedSongs(context: Context): ArrayList<Song> {
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<ArrayList<Album>> {
fun getLastAddedAlbumsFlowable(context: Context): Observable<ArrayList<Album>> {
return AlbumLoader.splitIntoAlbumsFlowable(getLastAddedSongsFlowable(context))
}
fun getLastAddedAlbums(context: Context): ArrayList<Album> {
return AlbumLoader.splitIntoAlbums(getLastAddedSongs(context))
}
@NonNull
fun getLastAddedArtists(@NonNull context: Context): Observable<ArrayList<Artist>> {
fun getLastAddedArtistsFlowable(context: Context): Observable<ArrayList<Artist>> {
return ArtistLoader.splitIntoArtists(getLastAddedAlbumsFlowable(context))
}
fun getLastAddedArtists(context: Context): ArrayList<Artist> {
return ArtistLoader.splitIntoArtists(getLastAddedAlbums(context))
}
}

View file

@ -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<String>?): 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<Playlist> {
private fun getPlaylistFlowable(
cursor: Cursor?
): Observable<Playlist> {
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<Playlist> {
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<Playlist> {
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<Playlist> {
return getPlaylist(makePlaylistCursor(
fun getPlaylistFlowable(
context: Context,
playlistId: Int
): Observable<Playlist> {
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<ArrayList<Playlist>> {
return getAllPlaylistsFlowable(makePlaylistCursor(context, null, null))
}
fun getFavoritePlaylistFlowable(context: Context): Observable<ArrayList<Playlist>> {
return getAllPlaylistsFlowable(makePlaylistCursor(
context,
PlaylistsColumns.NAME + "=?",
arrayOf(context.getString(code.name.monkey.retromusic.R.string.favorites))))
}
private fun getAllPlaylists(cursor: Cursor?): Observable<ArrayList<Playlist>> {
private fun getAllPlaylistsFlowable(cursor: Cursor?): Observable<ArrayList<Playlist>> {
return Observable.create { e ->
val playlists = ArrayList<Playlist>()
@ -99,15 +120,27 @@ object PlaylistLoader {
}
}
fun getAllPlaylists(context: Context): Observable<ArrayList<Playlist>> {
fun getAllPlaylists(context: Context): ArrayList<Playlist> {
return getAllPlaylists(makePlaylistCursor(context, null, null))
}
fun getFavoritePlaylist(context: Context): Observable<ArrayList<Playlist>> {
fun getFavoritePlaylist(context: Context): ArrayList<Playlist> {
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<Playlist> {
val playlists = ArrayList<Playlist>()
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<String>?
): 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)
}
}

View file

@ -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<ArrayList<Song>> {
fun getPlaylistSongListFlowable(
context: Context,
playlist: Playlist
): Observable<ArrayList<Song>> {
return (playlist as? AbsCustomPlaylist)?.getSongsFlowable(context)
?: getPlaylistSongListFlowable(context, playlist.id)
}
fun getPlaylistSongList(
context: Context,
playlist: Playlist
): ArrayList<Song> {
return (playlist as? AbsCustomPlaylist)?.getSongs(context)
?: getPlaylistSongList(context, playlist.id)
}
@NonNull
fun getPlaylistSongList(@NonNull context: Context, playlistId: Int): Observable<ArrayList<Song>> {
fun getPlaylistSongListFlowable(context: Context, playlistId: Int): Observable<ArrayList<Song>> {
return Observable.create { e ->
val songs = ArrayList<Song>()
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<Song> {
val songs = arrayListOf<Song>()
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()),

View file

@ -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<ArrayList<Any>> {
val results = ArrayList<Any>()
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<Any> {
val results = mutableListOf<Any>()
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
}
}

View file

@ -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<ArrayList<Song>> {
val cursor = makeSongCursor(context, null, null)
return getSongsFlowable(cursor)
}
fun getAllSongs(context: Context): Observable<ArrayList<Song>> {
fun getAllSongs(
context: Context
): ArrayList<Song> {
val cursor = makeSongCursor(context, null, null)
return getSongs(cursor)
}
fun getSongs(context: Context, query: String): Observable<ArrayList<Song>> {
val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%"))
return getSongs(cursor)
}
fun getSongs(cursor: Cursor?): Observable<ArrayList<Song>> {
fun getSongsFlowable(
cursor: Cursor?
): Observable<ArrayList<Song>> {
return Observable.create { e ->
val songs = ArrayList<Song>()
if (cursor != null && cursor.moveToFirst()) {
@ -59,7 +64,96 @@ object SongLoader {
}
}
private fun getSongFromCursorImpl(cursor: Cursor): Song {
fun getSongs(
cursor: Cursor?
): ArrayList<Song> {
val songs = arrayListOf<Song>()
if (cursor != null && cursor.moveToFirst()) {
do {
songs.add(getSongFromCursorImpl(cursor))
} while (cursor.moveToNext())
}
cursor?.close()
return songs
}
fun getSongsFlowable(
context: Context,
query: String
): Observable<ArrayList<Song>> {
val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%"))
return getSongsFlowable(cursor)
}
fun getSongs(
context: Context,
query: String
): ArrayList<Song> {
val cursor = makeSongCursor(context, AudioColumns.TITLE + " LIKE ?", arrayOf("%$query%"))
return getSongs(cursor)
}
private fun getSongFlowable(
cursor: Cursor?
): Observable<Song> {
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<Song> {
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<ArrayList<Song>> {
return SongLoader.getAllSongsFlowable(context)
.flatMap {
val list = ArrayList<Song>()
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<String>?, sortOrder: String = PreferenceUtil.getInstance().songSortOrder): Cursor? {
fun makeSongCursor(
context: Context,
selection: String?,
selectionValues: Array<String>?,
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<String>?,
paths: ArrayList<String>): Array<String>? {
private fun addBlacklistSelectionValues(
selectionValues: Array<String>?,
paths: ArrayList<String>
): Array<String>? {
var selectionValuesFinal = selectionValues
if (selectionValuesFinal == null) {
selectionValuesFinal = emptyArray()
@ -128,35 +232,4 @@ object SongLoader {
}
return newSelectionValues
}
private fun getSong(cursor: Cursor?): Observable<Song> {
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<Song> {
val cursor = makeSongCursor(context, AudioColumns._ID + "=?",
arrayOf(queryId.toString()))
return getSong(cursor)
}
fun suggestSongs(context: Context): Observable<ArrayList<Song>> {
return SongLoader.getAllSongs(context)
.flatMap {
val list = ArrayList<Song>()
ShuffleHelper.makeShuffleList(it, -1)
if (it.size >= 7) {
list.addAll(it.subList(0, 7))
}
return@flatMap Observable.just(list)
}
}
}

View file

@ -32,11 +32,19 @@ import java.util.*
object TopAndRecentlyPlayedTracksLoader {
fun getRecentlyPlayedTracks(context: Context): Observable<ArrayList<Song>> {
fun getRecentlyPlayedTracksFlowable(context: Context): Observable<ArrayList<Song>> {
return SongLoader.getSongsFlowable(makeRecentTracksCursorAndClearUpDatabase(context))
}
fun getRecentlyPlayedTracks(context: Context): ArrayList<Song> {
return SongLoader.getSongs(makeRecentTracksCursorAndClearUpDatabase(context))
}
fun getTopTracks(context: Context): Observable<ArrayList<Song>> {
fun getTopTracksFlowable(context: Context): Observable<ArrayList<Song>> {
return SongLoader.getSongsFlowable(makeTopTracksCursorAndClearUpDatabase(context))
}
fun getTopTracks(context: Context): ArrayList<Song> {
return SongLoader.getSongs(makeTopTracksCursorAndClearUpDatabase(context))
}
@ -130,9 +138,11 @@ object TopAndRecentlyPlayedTracksLoader {
return null
}
fun getTopAlbums(context: Context): Observable<ArrayList<Album>> {
fun getTopAlbumsFlowable(
context: Context
): Observable<ArrayList<Album>> {
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<ArrayList<Artist>> {
fun getTopAlbums(
context: Context
): ArrayList<Album> {
arrayListOf<Album>()
return AlbumLoader.splitIntoAlbums(getTopTracks(context))
}
fun getTopArtistsFlowable(context: Context): Observable<ArrayList<Artist>> {
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<Artist> {
return ArtistLoader.splitIntoArtists(getTopAlbums(context))
}
}

View file

@ -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<ArrayList<Song>> getSongs(Context context);
public abstract Observable<ArrayList<Song>> getSongsFlowable(@NotNull Context context);
@NonNull
public abstract ArrayList<Song> getSongs(@NotNull Context context);
}

View file

@ -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<Playlist> CREATOR = new Creator<Playlist>() {
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<ArrayList<Song>> getSongsFlowable(@NonNull Context context) {
// this default implementation covers static playlists
return PlaylistSongsLoader.INSTANCE.getPlaylistSongListFlowable(context, id);
}
@NonNull
public ArrayList<Song> 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<Playlist> CREATOR = new Creator<Playlist>() {
public Playlist createFromParcel(Parcel source) {
return new Playlist(source);
}
public Playlist[] newArray(int size) {
return new Playlist[size];
}
};
}

View file

@ -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<ArrayList<Song>> getSongs(@NonNull Context context) {
public Observable<ArrayList<Song>> getSongsFlowable(@NotNull @NonNull Context context) {
return TopAndRecentlyPlayedTracksLoader.INSTANCE.getRecentlyPlayedTracksFlowable(context);
}
@NonNull
@Override
public ArrayList<Song> getSongs(@NotNull @NonNull Context context) {
return TopAndRecentlyPlayedTracksLoader.INSTANCE.getRecentlyPlayedTracks(context);
}

View file

@ -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<ArrayList<Song>> getSongs(@NonNull Context context) {
public Observable<ArrayList<Song>> getSongsFlowable(@NotNull @NonNull Context context) {
return LastAddedSongsLoader.INSTANCE.getLastAddedSongsFlowable(context);
}
@NonNull
@Override
public ArrayList<Song> getSongs(@NotNull @NonNull Context context) {
return LastAddedSongsLoader.INSTANCE.getLastAddedSongs(context);
}

View file

@ -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<ArrayList<Song>> getSongs(@NonNull Context context) {
public Observable<ArrayList<Song>> getSongsFlowable(@NotNull @NonNull Context context) {
return TopAndRecentlyPlayedTracksLoader.INSTANCE.getTopTracksFlowable(context);
}
@NonNull
@Override
public ArrayList<Song> getSongs(@NotNull @NonNull Context context) {
return TopAndRecentlyPlayedTracksLoader.INSTANCE.getTopTracks(context);
}

View file

@ -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<ArrayList<Song>> getSongs(@NonNull Context context) {
public Observable<ArrayList<Song>> getSongsFlowable(@NotNull @NonNull Context context) {
return SongLoader.INSTANCE.getAllSongsFlowable(context);
}
@NonNull
@Override
public ArrayList<Song> getSongs(@NotNull Context context) {
return SongLoader.INSTANCE.getAllSongs(context);
}

View file

@ -24,7 +24,7 @@ import java.util.*
*/
interface SearchContract {
interface SearchView : BaseView<ArrayList<Any>>
interface SearchView : BaseView<MutableList<Any>>
interface SearchPresenter : BasePresenter<SearchView> {
fun search(query: String?)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
* <p/>
* This keeps track of the music playback and history state of the playback service
* <p/>
* 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<ArrayList<Song>> getSavedPlayingQueue() {
public Observable<ArrayList<Song>> getSavedPlayingQueueFlowable() {
return getQueueFlowable(PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
public Observable<ArrayList<Song>> getSavedOriginalPlayingQueueFlowable() {
return getQueueFlowable(ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
public ArrayList<Song> getSavedPlayingQueue() {
return getQueue(PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
public Observable<ArrayList<Song>> getSavedOriginalPlayingQueue() {
public ArrayList<Song> getSavedOriginalPlayingQueue() {
return getQueue(ORIGINAL_PLAYING_QUEUE_TABLE_NAME);
}
@NonNull
private Observable<ArrayList<Song>> getQueue(@NonNull final String tableName) {
private Observable<ArrayList<Song>> getQueueFlowable(@NonNull final String tableName) {
Cursor cursor = getReadableDatabase().query(tableName, null,
null, null, null, null, null);
return SongLoader.INSTANCE.getSongsFlowable(cursor);
}
@NonNull
private ArrayList<Song> getQueue(@NonNull final String tableName) {
Cursor cursor = getReadableDatabase().query(tableName, null,
null, null, null, null, null);
return SongLoader.INSTANCE.getSongs(cursor);

View file

@ -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<ArrayList<Playlist>>
override fun search(query: String?): MutableList<Any> {
return SearchLoader.searchAll(context, query)
}
override fun allAlbums(): ArrayList<Album> {
return AlbumLoader.getAllAlbums(context)
}
override fun recentAlbums(): ArrayList<Album> {
return LastAddedSongsLoader.getLastAddedAlbums(context)
}
override fun topAlbums(): ArrayList<Album> {
return TopAndRecentlyPlayedTracksLoader.getTopAlbums(context)
}
override fun allArtists(): ArrayList<Artist> {
return ArtistLoader.getAllArtists(context)
}
override fun recentArtists(): ArrayList<Artist> {
return LastAddedSongsLoader.getLastAddedArtists(context)
}
override fun topArtists(): ArrayList<Artist> {
return TopAndRecentlyPlayedTracksLoader.getTopArtists(context)
}
override fun allPlaylists(): ArrayList<Playlist> {
return PlaylistLoader.getAllPlaylists(context)
}
override fun allGenres(): ArrayList<Genre> {
return GenreLoader.getAllGenres(context)
}
override fun getSongFlowable(id: Int): Observable<Song> {
return SongLoader.getSongFlowable(context, id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getAlbumFlowable(albumId: Int): Observable<Album> {
return AlbumLoader.getAlbumFlowable(context, albumId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getArtistByIdFlowable(artistId: Int): Observable<Artist> {
return ArtistLoader.getArtistFlowable(context, artistId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlaylistSongsFlowable(playlist: Playlist): Observable<ArrayList<Song>> {
return PlaylistSongsLoader.getPlaylistSongListFlowable(context, playlist)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getGenreFlowable(genreId: Int): Observable<ArrayList<Song>> {
return GenreLoader.getSongsFlowable(context, genreId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override val favoritePlaylist: ArrayList<Playlist>
get() = PlaylistLoader.getFavoritePlaylist(context)
override fun allSongs(): ArrayList<Song> {
return SongLoader.getAllSongs(context)
}
override val favoritePlaylistFlowable: Observable<ArrayList<Playlist>>
get() = PlaylistLoader.getFavoritePlaylistFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allSongs: Observable<ArrayList<Song>>
get() = SongLoader.getAllSongs(context)
override val allSongsFlowable: Observable<ArrayList<Song>>
get() = SongLoader.getAllSongsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val suggestionSongs: Observable<ArrayList<Song>>
override val suggestionSongsFlowable: Observable<ArrayList<Song>>
get() = SongLoader.suggestSongs(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allAlbums: Observable<ArrayList<Album>>
get() = AlbumLoader.getAllAlbums(context)
override val allAlbumsFlowable: Observable<ArrayList<Album>>
get() = AlbumLoader.getAllAlbumsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val recentAlbums: Observable<ArrayList<Album>>
get() = LastAddedSongsLoader.getLastAddedAlbums(context)
override val recentAlbumsFlowable: Observable<ArrayList<Album>>
get() = LastAddedSongsLoader.getLastAddedAlbumsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val topAlbums: Observable<ArrayList<Album>>
get() = TopAndRecentlyPlayedTracksLoader.getTopAlbums(context)
override val topAlbumsFlowable: Observable<ArrayList<Album>>
get() = TopAndRecentlyPlayedTracksLoader.getTopAlbumsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allArtists: Observable<ArrayList<Artist>>
get() = ArtistLoader.getAllArtists(context)
override val allArtistsFlowable: Observable<ArrayList<Artist>>
get() = ArtistLoader.getAllArtistsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val recentArtists: Observable<ArrayList<Artist>>
get() = LastAddedSongsLoader.getLastAddedArtists(context)
override val recentArtistsFlowable: Observable<ArrayList<Artist>>
get() = LastAddedSongsLoader.getLastAddedArtistsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val topArtists: Observable<ArrayList<Artist>>
get() = TopAndRecentlyPlayedTracksLoader.getTopArtists(context)
override val topArtistsFlowable: Observable<ArrayList<Artist>>
get() = TopAndRecentlyPlayedTracksLoader.getTopArtistsFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allPlaylists: Observable<ArrayList<Playlist>>
get() = PlaylistLoader.getAllPlaylists(context)
override val allPlaylistsFlowable: Observable<ArrayList<Playlist>>
get() = PlaylistLoader.getAllPlaylistsFlowoable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val homeList: Observable<ArrayList<Playlist>>
get() = HomeLoader.getHomeLoader(context)
override val allGenresFlowable: Observable<ArrayList<Genre>>
get() = GenreLoader.getAllGenresFlowable(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allThings: Observable<ArrayList<AbsSmartPlaylist>>
get() = HomeLoader.getRecentAndTopThings(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override val allGenres: Observable<ArrayList<Genre>>
get() = GenreLoader.getAllGenres(context)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
override fun getSong(id: Int): Observable<Song> {
override fun getSong(id: Int): Song {
return SongLoader.getSong(context, id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getAlbum(albumId: Int): Observable<Album> {
override fun getAlbum(albumId: Int): Album {
return AlbumLoader.getAlbum(context, albumId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getArtistById(artistId: Long): Observable<Artist> {
override fun getArtistById(artistId: Long): Artist {
return ArtistLoader.getArtist(context, artistId.toInt())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun search(query: String?): Observable<ArrayList<Any>> {
return SearchLoader.searchAll(context, query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getPlaylistSongs(playlist: Playlist): Observable<ArrayList<Song>> {
override fun getPlaylistSongs(playlist: Playlist): ArrayList<Song> {
return PlaylistSongsLoader.getPlaylistSongList(context, playlist)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
override fun getGenre(genreId: Int): Observable<ArrayList<Song>> {
override fun getGenre(genreId: Int): ArrayList<Song> {
return GenreLoader.getSongs(context, genreId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}

View file

@ -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<ArrayList<Song>>
val allSongsFlowable: Observable<ArrayList<Song>>
val suggestionSongs: Observable<ArrayList<Song>>
fun allSongs(): ArrayList<Song>
val allAlbums: Observable<ArrayList<Album>>
val suggestionSongsFlowable: Observable<ArrayList<Song>>
val recentAlbums: Observable<ArrayList<Album>>
val allAlbumsFlowable: Observable<ArrayList<Album>>
val topAlbums: Observable<ArrayList<Album>>
fun allAlbums(): ArrayList<Album>
val allArtists: Observable<ArrayList<Artist>>
val recentAlbumsFlowable: Observable<ArrayList<Album>>
val recentArtists: Observable<ArrayList<Artist>>
fun recentAlbums(): ArrayList<Album>
val topArtists: Observable<ArrayList<Artist>>
val topAlbumsFlowable: Observable<ArrayList<Album>>
val allPlaylists: Observable<ArrayList<Playlist>>
fun topAlbums(): ArrayList<Album>
val homeList: Observable<ArrayList<Playlist>>
val allArtistsFlowable: Observable<ArrayList<Artist>>
val allThings: Observable<ArrayList<AbsSmartPlaylist>>
fun allArtists(): ArrayList<Artist>
val allGenres: Observable<ArrayList<Genre>>
val recentArtistsFlowable: Observable<ArrayList<Artist>>
fun getSong(id: Int): Observable<Song>
fun recentArtists(): ArrayList<Artist>
fun getAlbum(albumId: Int): Observable<Album>
val topArtistsFlowable: Observable<ArrayList<Artist>>
fun getArtistById(artistId: Long): Observable<Artist>
fun topArtists(): ArrayList<Artist>
fun search(query: String?): Observable<ArrayList<Any>>
val allPlaylistsFlowable: Observable<ArrayList<Playlist>>
fun getPlaylistSongs(playlist: Playlist): Observable<ArrayList<Song>>
fun allPlaylists(): ArrayList<Playlist>
fun getGenre(genreId: Int): Observable<ArrayList<Song>>
val allGenresFlowable: Observable<ArrayList<Genre>>
val favoritePlaylist: Observable<ArrayList<Playlist>>
fun allGenres(): ArrayList<Genre>
fun getSongFlowable(id: Int): Observable<Song>
fun getSong(id: Int): Song
fun getAlbumFlowable(albumId: Int): Observable<Album>
fun getAlbum(albumId: Int): Album
fun getArtistByIdFlowable(artistId: Int): Observable<Artist>
fun getArtistById(artistId: Long): Artist
fun search(query: String?): MutableList<Any>
fun getPlaylistSongsFlowable(playlist: Playlist): Observable<ArrayList<Song>>
fun getPlaylistSongs(playlist: Playlist): ArrayList<Song>
fun getGenreFlowable(genreId: Int): Observable<ArrayList<Song>>
fun getGenre(genreId: Int): ArrayList<Song>
val favoritePlaylistFlowable: Observable<ArrayList<Playlist>>
val favoritePlaylist: ArrayList<Playlist>
}

View file

@ -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.*
/**

View file

@ -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<Song>()
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<Song>
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<Song>, itemId: Int) {
var songIndex = MusicUtil.indexOfSongInList(songs, itemId)
if (songIndex == -1) {
songIndex = 0
}
openQueue(songs, songIndex)
}
private fun openQueue(songs: ArrayList<Song>, index: Int, startPlaying: Boolean = true) {
MusicPlayerRemote.openQueue(songs, index, startPlaying)
}
}

View file

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

View file

@ -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<Song> playingQueue = new ArrayList<>();
private ArrayList<Song> 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<Song> 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<List<MediaBrowserCompat.MediaItem>> 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<Song> restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue()
.blockingFirst();
ArrayList<Song> restoredOriginalQueue = MusicPlaybackQueueStore.getInstance(this).getSavedOriginalPlayingQueue()
.blockingFirst();
ArrayList<Song> restoredQueue = MusicPlaybackQueueStore.getInstance(this).getSavedPlayingQueue();
ArrayList<Song> 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<MusicService> 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<MusicService> 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
}
}
}

View file

@ -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<MusicService> 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;
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<Artist> artistList = ArtistLoader.INSTANCE.getAllArtists(mContext).blockingFirst();
List<Artist> 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<Album> artistAlbums = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getAlbums(); //ArtistAlbumLoader.getAlbumsForArtist(mContext, Long.parseLong(parentId.substring(1)));
List<Album> 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<Album> albumList = AlbumLoader.Companion.getAllAlbums(mContext).blockingFirst();
List<Album> 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<Song> songList = SongLoader.INSTANCE.getAllSongs(mContext).blockingFirst();
List<Song> 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<Song> albumSongList = AlbumLoader.Companion.getAlbum(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getSongs();
List<Song> 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<Song> artistSongs = ArtistLoader.INSTANCE.getArtist(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst().getSongs();
List<Song> 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<Playlist> playlistList = PlaylistLoader.INSTANCE.getAllPlaylists(mContext).blockingFirst();
List<Playlist> 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<Song> playlistSongs = PlaylistSongsLoader.INSTANCE.getPlaylistSongList(mContext, Integer.parseInt(parentId.substring(1))).blockingFirst();
List<Song> 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<Song> 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);
}

View file

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

View file

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

View file

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

View file

@ -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<ArrayList<Song>> matchFilesWithMediaStore(@NonNull Context context,
@Nullable List<File> files) {
public static ArrayList<Song> matchFilesWithMediaStore(@NonNull Context context,
@Nullable List<File> files) {
return SongLoader.INSTANCE.getSongs(makeSongCursor(context, files));
}

View file

@ -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}.
*/

View file

@ -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<Playlist> 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<Playlist> 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<Song> 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) : "-";

View file

@ -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<String, KnownCallerInfo>
private val platformSignature: String
private val callerChecked = mutableMapOf<String, Pair<Int, Boolean>>()
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<String>()
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<String, KnownCallerInfo> {
val certificateWhitelist = LinkedHashMap<String, KnownCallerInfo>()
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<KnownSignature>()
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<KnownSignature>
)
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<String>
)
}
private const val TAG = "PackageValidator"
private const val ANDROID_PLATFORM = "android"
private val WHITESPACE_REGEX = "\\s|\\n".toRegex()

View file

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

View file

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