Rx Java and Normal compatible for all query

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

View File

@ -121,7 +121,9 @@
android:name=".appshortcuts.AppShortcutLauncherActivity"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:name=".activities.saf.SAFGuideActivity" />
<activity
android:name=".activities.saf.SAFGuideActivity"
android:theme="@style/Theme.Intro" />
<meta-data
android:name="android.max_aspect"
@ -240,16 +242,23 @@
<service
android:name=".service.MusicService"
android:enabled="true" />
<service
android:name=".service.WearBrowserService"
android:exported="true">
android:enabled="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- Android Auto -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<meta-data
android:name="com.google.android.gms.car.application.theme"
android:resource="@style/CarTheme" />
<meta-data
android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/ic_notification" />
</application>
<uses-permission android:name="android.permission.WAKE_LOCK" />

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,

View File

@ -69,7 +69,7 @@
tools:ignore="MissingPrefix"
tools:text="Title" />
<code.name.monkey.appthemehelper.common.views.ATEPrimaryTextView
<code.name.monkey.appthemehelper.common.views.ATESecondaryTextView
android:id="@+id/text"
style="@style/TextAppearance.MaterialComponents.Subtitle2"
android:layout_width="match_parent"

View File

@ -77,7 +77,7 @@
tools:ignore="MissingPrefix"
tools:text="Title" />
<code.name.monkey.appthemehelper.common.views.ATEPrimaryTextView
<code.name.monkey.appthemehelper.common.views.ATESecondaryTextView
android:id="@+id/text"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="match_parent"

View File

@ -79,7 +79,7 @@
tools:ignore="MissingPrefix"
tools:text="Title" />
<code.name.monkey.appthemehelper.common.views.ATEPrimaryTextView
<code.name.monkey.appthemehelper.common.views.ATESecondaryTextView
android:id="@+id/text"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="match_parent"

View File

@ -80,7 +80,7 @@
tools:ignore="MissingPrefix"
tools:text="Title" />
<code.name.monkey.appthemehelper.common.views.ATEPrimaryTextView
<code.name.monkey.appthemehelper.common.views.ATESecondaryTextView
android:id="@+id/text"
style="@style/TextAppearance.MaterialComponents.Subtitle2"
android:layout_width="match_parent"

View File

@ -650,4 +650,14 @@
<string name="saf_guide_slide3_description">Do not open any sub-folders</string>
<string name="deleting_songs">Deleting songs</string>
<string name="hemanth_savarala">Hemanth Savarala</string>
<string name="albums_label">Albums</string>
<string name="artists_label">Artists</string>
<string name="playlists_label">Playlists</string>
<string name="history_label">History</string>
<string name="top_tracks_label">Top Tracks</string>
<string name="queue_label">Queue</string>
<string name="action_cycle_repeat">Cycle repeat mode</string>
<string name="action_toggle_shuffle">Toggle shuffle mode</string>
<string name="action_toggle_favorite">Toggle favorite</string>
</resources>

View File

@ -150,4 +150,12 @@
<style name="AppTextField" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<item name="boxBackgroundColor">@color/text_field_background</item>
</style>
<style name="CarTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- App bg, drawer right side, notification icon badge, overview "now playing" icon -->
<item name="colorPrimaryDark">@color/md_deep_purple_500</item>
<!-- Spinner, progress bar, FAB -->
<item name="colorAccent">@color/md_deep_orange_A400</item>
</style>
</resources>

View File

@ -0,0 +1,309 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
If you add a new signing_certificate to allowed_callers and you don't know its signature,
PackageValidator by default will print to logcat (INFO level) a message with the proper base64
version of the caller's certificate that has not been validated. You can copy from logcat and
paste it here.
Spaces and newlines are ignored.
-->
<allowed_callers>
<signing_certificate
name="Android Auto"
package="com.google.android.projection.gearhead"
release="false">
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</signing_certificate>
<signing_certificate
name="Android Auto"
package="com.google.android.projection.gearhead"
release="false">
MIIDvTCCAqWgAwIBAgIJAOfkBvDXw5bzMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwMjUxWhcNNDExMDEyMjMwMjUxWjB1
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAou7wwBKFyznqpRretJ3EVp55/Yr049Ag5wlGvrCnjIP8DrMrU+skfKe1
DmwpsLNtnhhiNH+J000Lok3hc8jdWKeKOopzKGDNvL/HvnS70Zyk26gj9jtMMHz9
2aZdpmwD67FNmTlG2FERr+TwMD5agaPnsFR2zla6ugUvHGzz65YDxpCZsQ/TowyD
LnxgMagvhvS+Oex3yh2FN7pJfwS03KdGdkWPbLqf9Fem09s5jjeZW/O3RgnKoRPI
J4QLK70efjAZqJyBGcDZyQMwOs+8HIknraf8+cRZJDzqOx7rttl8M3KGB2EFljTp
6/FyxJLnAo6QlXn7GrYalTI0yLU9dQIDAQABo1AwTjAdBgNVHQ4EFgQU9QPJ5xJE
DA8MDQMrj0hm2/A2BRkwHwYDVR0jBBgwFoAU9QPJ5xJEDA8MDQMrj0hm2/A2BRkw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEADcr5h1FR8IpmN4hSsUA9
SnCQVyXa1GQhzpQgRbF+npkgOn2Mebp8bd28VpfgooD2OBNQXCUcZkn7pWj++ut9
HhObHVaV5FNg0pdDqLna9QZ9Y4oS+ZrijK70XZ/EjlYUHvhu0pIjZAbD8CmCFlow
SR55qCSjM5iS37LZB32SMr1BBiYrNAvncKjYQVK8ctTRzhpNQQPBgXBA98Xl+d1D
Py00JWQuF0ssmhKcJuvfdEnFF7Hvaxz/gCQ9nzarQI3CJB8dOXVwF8mcyDRBz4JR
+YDpXo6BD+fGt15ov+zmqC8xaT9P1/JgoDXiMhy/6rwgdi9WxPf8mb7TnBC+CksX
0A==
</signing_certificate>
<signing_certificate
name="Android Auto"
package="com.google.android.projection.gearhead"
release="true">
MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
IQ==
</signing_certificate>
<signing_certificate
name="Android Wear"
package="com.google.android.wearable.app"
release="false">
MIIDvzCCAqegAwIBAgIJANqYw9kVc9PvMA0GCSqGSIb3DQEBBQUAMHYxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDESMBAG
A1UEAwwJQ2xvY2tXb3JrMB4XDTE0MDMwNzIyMDExOFoXDTQxMDcyMzIyMDExOFow
djELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1v
dW50YWluIFZpZXcxFDASBgNVBAoMC0dvb2dsZSBJbmMuMRAwDgYDVQQLDAdBbmRy
b2lkMRIwEAYDVQQDDAlDbG9ja1dvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQDcHW9LKO04MBSynIL22v/THd57jB5jCEBlC1ixZaNqrrYscVOVLgRF
Ca+CH5S6n08YZMOntdZTzAAVnQAQ4eVm+jeq/xg2Xa57SoXdsfODzEdwoj6VYpH+
tXLBaTFar0706qWuhh/N1ufl6tQxE3RGRgx8KPsyLJKVXFx6qJV3w3A/l+CYt362
oG6sa3LqoK0hCrAqH9z8dmJ0dEGpPzzqihb0jJciweMyQTJ+wsn3MDEujRvv7ikL
RRo0iSys71sUctbZfvlUKMyK1e8EuMTx9Q3SQtVdclhmhVBbXksbHlmtjB2FL6CC
SBVnO8bmQynsxOrU24RkqWsxg5+f28kHAgMBAAGjUDBOMB0GA1UdDgQWBBSEhUcQ
hKQ8s+r4P6shYqCVAM4sejAfBgNVHSMEGDAWgBSEhUcQhKQ8s+r4P6shYqCVAM4s
ejAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAHOWILon0qD1SIQ60b
YI4cKdkBKIHq/D/WKF9fYmqXPvBX5pfusqxcouYFyj0z9ZCZa4sAMsRH5lAPJb0X
yvmVAzmDQMjubNy1O+3ksfJI59AgmZ6B58rqpTLP2pn+SqXtQEBORPdb79J/yts1
uLIblHhGXhci8nr7KwtuFY5ExKsMT2V7Gdd9j1PJz7nuLU9FtlTgEryN6YHkwuLD
055RkwPYrk0swchijXhXrnU/HXsCo6cFeMYF4wUcbB2pSRrOE7uI0H2BfdSUJlGX
hK6WlaRHNQ2J60BekPKr82auL8pY0va/Hb9LHEie4KABVN/PAiUS9aHHIp5zHePw
R9b4
</signing_certificate>
<signing_certificate
name="Android Wear"
package="com.google.android.wearable.app"
release="true">
MIIFYTCCA0sCBgFEnpGW0zALBgkqhkiG9w0BAQUwdjELMAkGA1UEBhMCVVMxEzAR
BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNV
BAoTC0dvb2dsZSBJbmMuMRAwDgYDVQQLEwdBbmRyb2lkMRIwEAYDVQQDEwlDbG9j
a1dvcmswHhcNMTQwMzA3MjIwMjI1WhcNMzgwMTE5MDMxNDA3WjB2MQswCQYDVQQG
EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmll
dzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEjAQBgNV
BAMTCUNsb2NrV29yazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALo8
fzkL/lmKYrz8izyUxidamRXt3N03OlVqmQvi/UP3nxizAdJAJ+NyfwnO/eKcfCaw
iiDeNn0a3+NOp4+uN/OQ9eAmcnpOCCg773b49kO8FSc2oEg/ybeRq1I8872Ge2Yt
KidMANiQ550R6LAmX+2pddzI5UKZiY6QE2picYwuCy85eVHbJLFXob/nxWvOSjgL
Jfq5JmM+qJQEOOC2lAu5nol+LvoFPDIpm9lhb6S9loIhezdDH83Ygu0hp/LwRn/g
lRy8Wphi40oVa+FaF/8CF5hkRNYTsR7XX4OAGO60/ZTkj1rjHOSvpDY4tpcshVzS
2woBxDJhKOTFGXq+rMxtwuitpEJfD5DVpaVYJGG/eBHhLs4O6gYDP5ZUOe3gcf/E
bCDy374jIzp/ZMHOCa2hy85r9ryiLpuYnErAyWqdbHVP7Bhx2HsQmMGg3mC8fXfe
MNVOuEfOaxJ8GR6nk28KRsFG8za5NOq6Wl8cA2S3UpZVRDJQ/WOq5Xvrq+AmPwkI
TRlEBgw62bu6f3n03jwrLTe6sw1LuRHcUWngr5VS9NOOPbPyy4AcUgJScGG/AbBC
0H6J5I8RqaqgJ/BElZ7aKMXd2FNXpx45u4JRs1frb3IY/MwXGIGmMGdGMeBVlDka
emea8lqgYgHWIrjQCd0R1QaiAw8kJ65n2Bs0N3l5AgMBAAEwCwYJKoZIhvcNAQEF
A4ICAQCkxJaWNGHIlTWlsQrNASQ3aonaJ0OdrDADSGcLICut4z8vuioHZAO1C+hx
yiqym769u8QG2wk5QcmMF2oORv+Q/wAWFgREgG7cguEw/hCGHuMFnbd+PZ2poq00
qdK02hsm/VpbcBzVbP7pZHrkFDuXpnwCgGWxf54U8jKl7xfhZKFJF5KWlBwwvVo2
q/jzQsjjr7xvSUNzB31qnBHXOSINWte1GS+bHP6Wj0pysbhUdeDpiL5ocohmZbEr
9O7DIlZU9eHyK4vrVY6+ZneL1l8JkS35XoCd/u8Px+rKXQ6+HUEEH+cgyzKbMH45
LhOX8SA5VGkwhIt/AhdAiS32x6By5984usPXIjVv5lR/anxXit9nyT0rNYiTVDXw
+aETzi3szW2hncNLQYLsrtYg61KFMCXF4ATstFG8ReFIWsw2f7ZJkq9ZTFUbC8k4
y9Ya1WdZkCD3OmXhqcikiNusgx7rkY8MkikJXt5BBXs8rupOFsW5RUuS4lmKEbSU
oh8/er+DwGf0GC6YQZuk5JOKNIDwhi/tr1dySlUzV4/aX7PN/PlUgH//2MlRd+d1
BKZCvlzboOEAZfx8aBKc7SezqATXpM3ZDNPsywWoyIpgmtBWoE60ih4Flf05XB+n
e7MdpSQ0Xgq9TgG1BoJP6rpC0y3Ukmc+z8AXnYYdJunNXEbv0A==
</signing_certificate>
<signing_certificate
name="Media Browser Service Simulator"
package="com.google.android.mediasimulator"
release="true">
MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
IQ==
</signing_certificate>
<signing_certificate
name="Android Auto Simulator"
package="com.google.android.autosimulator"
release="true">
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</signing_certificate>
<signing_certificate
name="Media Browser Simulator"
package="com.google.android.mediasimulator"
release="true">
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</signing_certificate>
<signing_certificate
name="Google"
package="com.google.android.googlequicksearchbox"
release="false">
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</signing_certificate>
<signing_certificate
name="Google"
package="com.google.android.googlequicksearchbox"
release="true">
MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
IQ==
</signing_certificate>
</allowed_callers>