parent
cf05d3c781
commit
982639d2c7
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.moire.ultrasonic"
|
||||
android:installLocation="auto">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.moire.ultrasonic"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
@ -60,6 +61,7 @@
|
|||
</service>
|
||||
|
||||
<service
|
||||
tools:ignore="ExportedService"
|
||||
android:name=".service.AutoMediaBrowserService"
|
||||
android:label="@string/common.appname"
|
||||
android:exported="true">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.moire.ultrasonic.di
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.service.AudioFocusHandler
|
||||
import org.moire.ultrasonic.service.DownloadQueueSerializer
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
/*
|
||||
* AutoMediaBrowserService.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.os.Bundle
|
||||
|
@ -24,44 +31,45 @@ import org.moire.ultrasonic.util.MediaSessionHandler
|
|||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
|
||||
const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
|
||||
const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
|
||||
const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
|
||||
const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
|
||||
const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
|
||||
const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
|
||||
const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
|
||||
const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
|
||||
const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
|
||||
const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
|
||||
const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
|
||||
const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
|
||||
const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
|
||||
const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
|
||||
const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
|
||||
const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
|
||||
const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
|
||||
const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
|
||||
const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
|
||||
const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
|
||||
const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
|
||||
const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
|
||||
const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
|
||||
const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
|
||||
const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
|
||||
const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
|
||||
const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
|
||||
const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
|
||||
const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
|
||||
private const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
|
||||
private const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
|
||||
private const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
|
||||
private const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
|
||||
private const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
|
||||
private const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
|
||||
private const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
|
||||
private const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
|
||||
private const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
|
||||
private const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
|
||||
private const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
|
||||
private const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
|
||||
private const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
|
||||
private const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
|
||||
private const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
|
||||
private const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
|
||||
private const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
|
||||
private const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
|
||||
private const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
|
||||
private const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
|
||||
private const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
|
||||
private const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
|
||||
private const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
|
||||
private const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
|
||||
private const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
|
||||
private const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
|
||||
private const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
|
||||
private const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
|
||||
private const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
|
||||
private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
|
||||
|
||||
// Currently the display limit for long lists is 100 items
|
||||
const val displayLimit = 100
|
||||
const val searchLimit = 10
|
||||
private const val DISPLAY_LIMIT = 100
|
||||
private const val SEARCH_LIMIT = 10
|
||||
|
||||
/**
|
||||
* MediaBrowserService implementation for e.g. Android Auto
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
|
||||
private lateinit var mediaSessionEventListener: MediaSessionEventListener
|
||||
|
@ -84,6 +92,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
private val useId3Tags get() = Util.getShouldUseId3Tags()
|
||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
@ -95,16 +104,23 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) {
|
||||
Timber.d("AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s", mediaId)
|
||||
Timber.d(
|
||||
"AutoMediaBrowserService onPlayFromMediaIdRequested called. mediaId: %s",
|
||||
mediaId
|
||||
)
|
||||
|
||||
if (mediaId == null) return
|
||||
val mediaIdParts = mediaId.split('|')
|
||||
|
||||
when (mediaIdParts.first()) {
|
||||
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
|
||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(mediaIdParts[1], mediaIdParts[2], mediaIdParts[3])
|
||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
||||
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
||||
|
@ -113,7 +129,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
|
||||
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
|
||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||
mediaIdParts[1], mediaIdParts[2]
|
||||
)
|
||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +141,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
if (query.isNullOrBlank()) playRandomSongs()
|
||||
|
||||
serviceScope.launch {
|
||||
val criteria = SearchCriteria(query!!, 0, 0, displayLimit)
|
||||
val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT)
|
||||
val searchResult = callWithErrorHandling { musicService.search(criteria) }
|
||||
|
||||
// Try to find the best match
|
||||
|
@ -146,12 +164,17 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
mediaSessionHandler.initialize()
|
||||
|
||||
val handler = Handler()
|
||||
handler.postDelayed({
|
||||
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
|
||||
Timber.d("AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService...")
|
||||
lifecycleSupport.onCreate()
|
||||
MediaPlayerService.getInstance()
|
||||
}, 100)
|
||||
handler.postDelayed(
|
||||
{
|
||||
// Ultrasonic may be started from Android Auto. This boots up the necessary components.
|
||||
Timber.d(
|
||||
"AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..."
|
||||
)
|
||||
lifecycleSupport.onCreate()
|
||||
MediaPlayerService.getInstance()
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
Timber.i("AutoMediaBrowserService onCreate finished")
|
||||
}
|
||||
|
@ -170,21 +193,28 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): BrowserRoot {
|
||||
Timber.d("AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d", clientPackageName, clientUid)
|
||||
Timber.d(
|
||||
"AutoMediaBrowserService onGetRoot called. clientPackageName: %s; clientUid: %d",
|
||||
clientPackageName, clientUid
|
||||
)
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
)
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
)
|
||||
extras.putBoolean(
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
|
||||
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
|
||||
)
|
||||
|
||||
return BrowserRoot(MEDIA_ROOT_ID, extras)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount", "ComplexMethod")
|
||||
override fun onLoadChildren(
|
||||
parentId: String,
|
||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||
|
@ -199,7 +229,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
MEDIA_ARTIST_ID -> return getArtists(result)
|
||||
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
|
||||
MEDIA_ALBUM_ID -> return getAlbums(result, AlbumListType.SORTED_BY_NAME)
|
||||
MEDIA_ALBUM_PAGE_ID -> return getAlbums(result, AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt())
|
||||
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
|
||||
result, AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
||||
)
|
||||
MEDIA_PLAYLIST_ID -> return getPlaylists(result)
|
||||
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT)
|
||||
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST)
|
||||
|
@ -212,7 +244,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
MEDIA_BOOKMARK_ID -> return getBookmarks(result)
|
||||
MEDIA_PODCAST_ID -> return getPodcasts(result)
|
||||
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], result)
|
||||
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(result, parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
|
||||
result, parentIdParts[1], parentIdParts[2]
|
||||
)
|
||||
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(result, parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1])
|
||||
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1])
|
||||
|
@ -230,7 +264,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
result.detach()
|
||||
|
||||
serviceScope.launch {
|
||||
val criteria = SearchCriteria(query, searchLimit, searchLimit, searchLimit)
|
||||
val criteria = SearchCriteria(query, SEARCH_LIMIT, SEARCH_LIMIT, SEARCH_LIMIT)
|
||||
val searchResult = callWithErrorHandling { musicService.search(criteria) }
|
||||
|
||||
// TODO Add More... button to categories
|
||||
|
@ -272,7 +306,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun playSearch(id : String) {
|
||||
private fun playSearch(id: String) {
|
||||
serviceScope.launch {
|
||||
// If there is no cache, we can't play the selected song.
|
||||
if (searchSongsCache != null) {
|
||||
|
@ -380,7 +414,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
result.sendResult(mediaItems)
|
||||
}
|
||||
|
||||
private fun getArtists(result: Result<MutableList<MediaBrowserCompat.MediaItem>>, section: String? = null) {
|
||||
private fun getArtists(
|
||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>,
|
||||
section: String? = null
|
||||
) {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
result.detach()
|
||||
|
||||
|
@ -404,7 +441,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
// If there are too many artists, create alphabetic index of them
|
||||
if (section == null && artists.count() > displayLimit) {
|
||||
if (section == null && artists.count() > DISPLAY_LIMIT) {
|
||||
val index = mutableListOf<String>()
|
||||
// TODO This sort should use ignoredArticles somehow...
|
||||
artists = artists.sortedBy { artist -> artist.name }
|
||||
|
@ -442,7 +479,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
result.detach()
|
||||
serviceScope.launch {
|
||||
val albums = if (!isOffline && useId3Tags) {
|
||||
callWithErrorHandling { musicService.getArtist(id, name,false) }
|
||||
callWithErrorHandling { musicService.getArtist(id, name, false) }
|
||||
} else {
|
||||
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
|
||||
}
|
||||
|
@ -477,7 +514,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
||||
|
||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||
val items = songs.getChildren().take(displayLimit)
|
||||
val items = songs.getChildren().take(DISPLAY_LIMIT)
|
||||
items.map { item ->
|
||||
if (item.isDirectory)
|
||||
mediaItems.add(
|
||||
|
@ -518,11 +555,19 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
result.detach()
|
||||
serviceScope.launch {
|
||||
val offset = (page ?: 0) * displayLimit
|
||||
val offset = (page ?: 0) * DISPLAY_LIMIT
|
||||
val albums = if (useId3Tags) {
|
||||
callWithErrorHandling { musicService.getAlbumList2(type.typeName, displayLimit, offset, null) }
|
||||
callWithErrorHandling {
|
||||
musicService.getAlbumList2(
|
||||
type.typeName, DISPLAY_LIMIT, offset, null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
callWithErrorHandling { musicService.getAlbumList(type.typeName, displayLimit, offset, null) }
|
||||
callWithErrorHandling {
|
||||
musicService.getAlbumList(
|
||||
type.typeName, DISPLAY_LIMIT, offset, null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
albums?.getAllChild()?.map { album ->
|
||||
|
@ -534,7 +579,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
)
|
||||
}
|
||||
|
||||
if (albums?.getAllChild()?.count() ?: 0 >= displayLimit)
|
||||
if (albums?.getAllChild()?.count() ?: 0 >= DISPLAY_LIMIT)
|
||||
mediaItems.add(
|
||||
R.string.search_more,
|
||||
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"),
|
||||
|
@ -564,7 +609,11 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getPlaylist(id: String, name: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
private fun getPlaylist(
|
||||
id: String,
|
||||
name: String,
|
||||
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
result.detach()
|
||||
|
||||
|
@ -579,7 +628,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
|
||||
// Playlist should be cached as it may contain random elements
|
||||
playlistCache = content.getAllChild()
|
||||
playlistCache!!.take(displayLimit).map { item ->
|
||||
playlistCache!!.take(DISPLAY_LIMIT).map { item ->
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
|
@ -618,7 +667,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
|
||||
playlistCache = content?.getAllChild()
|
||||
}
|
||||
val song = playlistCache?.firstOrNull{x -> x.id == songId}
|
||||
val song = playlistCache?.firstOrNull { x -> x.id == songId }
|
||||
if (song != null) playSong(song)
|
||||
}
|
||||
}
|
||||
|
@ -633,7 +682,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
private fun playAlbumSong(id: String, name: String, songId: String) {
|
||||
serviceScope.launch {
|
||||
val songs = listSongsInMusicService(id, name)
|
||||
val song = songs?.getAllChild()?.firstOrNull{x -> x.id == songId}
|
||||
val song = songs?.getAllChild()?.firstOrNull { x -> x.id == songId }
|
||||
if (song != null) playSong(song)
|
||||
}
|
||||
}
|
||||
|
@ -669,14 +718,16 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
|
||||
|
||||
episodes.getAllChild().map { episode ->
|
||||
mediaItems.add(MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
episode,
|
||||
listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id)
|
||||
.joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
))
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
episode,
|
||||
listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id)
|
||||
.joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
result.sendResult(mediaItems)
|
||||
}
|
||||
|
@ -713,13 +764,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
val songs = Util.getSongsFromBookmarks(bookmarks)
|
||||
|
||||
songs.getAllChild().map { song ->
|
||||
mediaItems.add(MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
song,
|
||||
listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
))
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
song,
|
||||
listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
result.sendResult(mediaItems)
|
||||
}
|
||||
|
@ -731,7 +784,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
|
||||
if (bookmarks != null) {
|
||||
val songs = Util.getSongsFromBookmarks(bookmarks)
|
||||
val song = songs.getAllChild().firstOrNull{song -> song.id == id}
|
||||
val song = songs.getAllChild().firstOrNull { song -> song.id == id }
|
||||
if (song != null) playSong(song)
|
||||
}
|
||||
}
|
||||
|
@ -766,20 +819,22 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
serviceScope.launch {
|
||||
val shares = callWithErrorHandling { musicService.getShares(false) }
|
||||
|
||||
val selectedShare = shares?.firstOrNull{share -> share.id == id }
|
||||
val selectedShare = shares?.firstOrNull { share -> share.id == id }
|
||||
if (selectedShare != null) {
|
||||
|
||||
if (selectedShare.getEntries().count() > 1)
|
||||
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
|
||||
|
||||
selectedShare.getEntries().map { song ->
|
||||
mediaItems.add(MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
song,
|
||||
listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
))
|
||||
mediaItems.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
Util.getMediaDescriptionForEntry(
|
||||
song,
|
||||
listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|")
|
||||
),
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
result.sendResult(mediaItems)
|
||||
|
@ -789,7 +844,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
private fun playShare(id: String) {
|
||||
serviceScope.launch {
|
||||
val shares = callWithErrorHandling { musicService.getShares(false) }
|
||||
val selectedShare = shares?.firstOrNull{share -> share.id == id }
|
||||
val selectedShare = shares?.firstOrNull { share -> share.id == id }
|
||||
if (selectedShare != null) {
|
||||
playSongs(selectedShare.getEntries())
|
||||
}
|
||||
|
@ -799,9 +854,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
private fun playShareSong(id: String, songId: String) {
|
||||
serviceScope.launch {
|
||||
val shares = callWithErrorHandling { musicService.getShares(false) }
|
||||
val selectedShare = shares?.firstOrNull{share -> share.id == id }
|
||||
val selectedShare = shares?.firstOrNull { share -> share.id == id }
|
||||
if (selectedShare != null) {
|
||||
val song = selectedShare.getEntries().firstOrNull{x -> x.id == songId}
|
||||
val song = selectedShare.getEntries().firstOrNull { x -> x.id == songId }
|
||||
if (song != null) playSong(song)
|
||||
}
|
||||
}
|
||||
|
@ -819,7 +874,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
|
||||
|
||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||
val items = songs.songs.take(displayLimit)
|
||||
val items = songs.songs.take(DISPLAY_LIMIT)
|
||||
starredSongsCache = items
|
||||
items.map { song ->
|
||||
mediaItems.add(
|
||||
|
@ -855,7 +910,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
val content = listStarredSongsInMusicService()
|
||||
starredSongsCache = content?.songs
|
||||
}
|
||||
val song = starredSongsCache?.firstOrNull{x -> x.id == songId}
|
||||
val song = starredSongsCache?.firstOrNull { x -> x.id == songId }
|
||||
if (song != null) playSong(song)
|
||||
}
|
||||
}
|
||||
|
@ -865,7 +920,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
result.detach()
|
||||
|
||||
serviceScope.launch {
|
||||
val songs = callWithErrorHandling { musicService.getRandomSongs(displayLimit) }
|
||||
val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
|
||||
|
||||
if (songs != null) {
|
||||
if (songs.getAllChild().count() > 1)
|
||||
|
@ -895,7 +950,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
if (randomSongsCache == null) {
|
||||
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them
|
||||
// In this case we request a new set of random songs
|
||||
val content = callWithErrorHandling { musicService.getRandomSongs(displayLimit) }
|
||||
val content = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
|
||||
randomSongsCache = content?.getAllChild()
|
||||
}
|
||||
if (randomSongsCache != null) playSongs(randomSongsCache)
|
||||
|
@ -942,10 +997,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
|
||||
|
||||
if (groupNameId != null)
|
||||
builder.setExtras(Bundle().apply { putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
getString(groupNameId)
|
||||
) })
|
||||
builder.setExtras(
|
||||
Bundle().apply {
|
||||
putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
getString(groupNameId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val mediaItem = MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
|
@ -970,10 +1029,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
|
||||
|
||||
if (groupNameId != null)
|
||||
builder.setExtras(Bundle().apply { putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
getString(groupNameId)
|
||||
) })
|
||||
builder.setExtras(
|
||||
Bundle().apply {
|
||||
putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
getString(groupNameId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val mediaItem = MediaBrowserCompat.MediaItem(
|
||||
builder.build(),
|
||||
|
@ -1034,4 +1097,4 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
|
|||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
/*
|
||||
* DownloadQueueSerializer.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.content.Context
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
@ -11,9 +21,6 @@ import org.moire.ultrasonic.util.Constants
|
|||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
/**
|
||||
* This class is responsible for the serialization / deserialization
|
||||
|
@ -102,4 +109,4 @@ class DownloadQueueSerializer : KoinComponent {
|
|||
mediaSessionHandler.updateMediaSessionQueue(state.songs)
|
||||
afterDeserialized.accept(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,13 @@ import android.os.PowerManager
|
|||
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
||||
import android.os.PowerManager.WakeLock
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
|
@ -42,7 +42,8 @@ import timber.log.Timber
|
|||
/**
|
||||
* Represents a Media Player which uses the mobile's resources for playback
|
||||
*/
|
||||
class LocalMediaPlayer: KoinComponent {
|
||||
@Suppress("TooManyFunctions")
|
||||
class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
private val audioFocusHandler by inject<AudioFocusHandler>()
|
||||
private val context by inject<Context>()
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic 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.
|
||||
|
||||
Subsonic 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
* MediaPlayerLifecycleSupport.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -25,7 +14,6 @@ import android.content.IntentFilter
|
|||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import kotlinx.coroutines.newFixedThreadPoolContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
|
@ -85,7 +73,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
false
|
||||
)
|
||||
|
||||
// Work-around: Serialize again, as the restore() method creates a serialization without current playing info.
|
||||
// Work-around: Serialize again, as the restore() method creates a
|
||||
// serialization without current playing info.
|
||||
downloadQueueSerializer.serializeDownloadQueue(
|
||||
downloader.downloadList,
|
||||
downloader.currentPlayingIndex,
|
||||
|
@ -179,14 +168,15 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
|
||||
val headsetIntentFilter: IntentFilter =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
IntentFilter(AudioManager.ACTION_HEADSET_PLUG)
|
||||
} else {
|
||||
IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||
}
|
||||
IntentFilter(AudioManager.ACTION_HEADSET_PLUG)
|
||||
} else {
|
||||
IntentFilter(Intent.ACTION_HEADSET_PLUG)
|
||||
}
|
||||
|
||||
applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "ComplexMethod")
|
||||
private fun handleKeyEvent(event: KeyEvent) {
|
||||
|
||||
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return
|
||||
|
@ -195,9 +185,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
val receivedKeyCode = event.keyCode
|
||||
|
||||
// Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices
|
||||
keyCode = if (Util.getSingleButtonPlayPause() &&
|
||||
(receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE)
|
||||
keyCode = if (Util.getSingleButtonPlayPause() && (
|
||||
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|
||||
)
|
||||
) {
|
||||
Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE")
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
|
@ -221,10 +212,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY ->
|
||||
if (mediaPlayerController.playerState === PlayerState.IDLE) {
|
||||
mediaPlayerController.play()
|
||||
} else if (mediaPlayerController.playerState !== PlayerState.STARTED) {
|
||||
mediaPlayerController.start()
|
||||
}
|
||||
mediaPlayerController.play()
|
||||
} else if (mediaPlayerController.playerState !== PlayerState.STARTED) {
|
||||
mediaPlayerController.start()
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||
KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1)
|
||||
|
@ -242,13 +233,18 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
/**
|
||||
* This function processes the intent that could come from other applications.
|
||||
*/
|
||||
@Suppress("ComplexMethod")
|
||||
private fun handleUltrasonicIntent(intentAction: String) {
|
||||
|
||||
val isRunning = created
|
||||
|
||||
// If Ultrasonic is not running, do nothing to stop or pause
|
||||
if (!isRunning && (intentAction == Constants.CMD_PAUSE ||
|
||||
intentAction == Constants.CMD_STOP)) return
|
||||
if (
|
||||
!isRunning && (
|
||||
intentAction == Constants.CMD_PAUSE ||
|
||||
intentAction == Constants.CMD_STOP
|
||||
)
|
||||
) return
|
||||
|
||||
val autoStart =
|
||||
intentAction == Constants.CMD_PLAY ||
|
||||
|
@ -261,7 +257,9 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
onCreate(autoStart) {
|
||||
when (intentAction) {
|
||||
Constants.CMD_PLAY -> mediaPlayerController.play()
|
||||
Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, no need to call anything
|
||||
Constants.CMD_RESUME_OR_PLAY ->
|
||||
// If Ultrasonic wasn't running, the autoStart is enough to resume,
|
||||
// no need to call anything
|
||||
if (isRunning) mediaPlayerController.resumeOrPlay()
|
||||
|
||||
Constants.CMD_NEXT -> mediaPlayerController.next()
|
||||
|
@ -277,4 +275,4 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.app.*
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
|
@ -93,7 +97,7 @@ class MediaPlayerService : Service() {
|
|||
|
||||
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
|
||||
|
||||
mediaSessionEventListener = object:MediaSessionEventListener {
|
||||
mediaSessionEventListener = object : MediaSessionEventListener {
|
||||
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
|
||||
mediaSessionToken = token
|
||||
}
|
||||
|
@ -383,7 +387,11 @@ class MediaPlayerService : Service() {
|
|||
val context = this@MediaPlayerService
|
||||
|
||||
// Notify MediaSession
|
||||
mediaSessionHandler.updateMediaSession(currentPlaying, downloader.currentPlayingIndex.toLong(), playerState)
|
||||
mediaSessionHandler.updateMediaSession(
|
||||
currentPlaying,
|
||||
downloader.currentPlayingIndex.toLong(),
|
||||
playerState
|
||||
)
|
||||
|
||||
if (playerState === PlayerState.PAUSED) {
|
||||
downloadQueueSerializer.serializeDownloadQueue(
|
||||
|
@ -535,7 +543,11 @@ class MediaPlayerService : Service() {
|
|||
// Init
|
||||
val context = applicationContext
|
||||
val song = currentPlaying?.song
|
||||
val stopIntent = Util.getPendingIntentForMediaAction(context, KeyEvent.KEYCODE_MEDIA_STOP, 100)
|
||||
val stopIntent = Util.getPendingIntentForMediaAction(
|
||||
context,
|
||||
KeyEvent.KEYCODE_MEDIA_STOP,
|
||||
100
|
||||
)
|
||||
|
||||
// We should use a single notification builder, otherwise the notification may not be updated
|
||||
if (notificationBuilder == null) {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
/*
|
||||
* MediaSessionEventDistributor.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Bundle
|
||||
|
@ -41,7 +48,10 @@ class MediaSessionEventDistributor {
|
|||
}
|
||||
|
||||
fun raisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) {
|
||||
eventListenerList.forEach { listener -> listener.onPlayFromMediaIdRequested(mediaId, extras) }
|
||||
eventListenerList.forEach {
|
||||
listener ->
|
||||
listener.onPlayFromMediaIdRequested(mediaId, extras)
|
||||
}
|
||||
}
|
||||
|
||||
fun raisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
/*
|
||||
* MediaSessionEventListener.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Bundle
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
/*
|
||||
* MediaSessionHandler.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
|
@ -21,7 +28,7 @@ import org.moire.ultrasonic.service.DownloadFile
|
|||
import timber.log.Timber
|
||||
|
||||
private const val INTENT_CODE_MEDIA_BUTTON = 161
|
||||
|
||||
private const val CALL_DIVIDE = 10
|
||||
/**
|
||||
* Central place to handle the state of the MediaSession
|
||||
*/
|
||||
|
@ -157,7 +164,12 @@ class MediaSessionHandler : KoinComponent {
|
|||
Timber.i("MediaSessionHandler.initialize Media Session created")
|
||||
}
|
||||
|
||||
fun updateMediaSession(currentPlaying: DownloadFile?, currentPlayingIndex: Long?, playerState: PlayerState) {
|
||||
@Suppress("TooGenericExceptionCaught", "LongMethod")
|
||||
fun updateMediaSession(
|
||||
currentPlaying: DownloadFile?,
|
||||
currentPlayingIndex: Long?,
|
||||
playerState: PlayerState
|
||||
) {
|
||||
Timber.d("Updating the MediaSession")
|
||||
|
||||
// Set Metadata
|
||||
|
@ -240,18 +252,20 @@ class MediaSessionHandler : KoinComponent {
|
|||
mediaSession!!.setPlaybackState(playbackStateBuilder.build())
|
||||
}
|
||||
|
||||
fun updateMediaSessionQueue(playlist: Iterable<MusicDirectory.Entry>)
|
||||
{
|
||||
fun updateMediaSessionQueue(playlist: Iterable<MusicDirectory.Entry>) {
|
||||
// This call is cached because Downloader may initialize earlier than the MediaSession
|
||||
cachedPlaylist = playlist
|
||||
if (mediaSession == null) return
|
||||
|
||||
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
|
||||
mediaSession!!.setQueue(playlist.mapIndexed { id, song ->
|
||||
MediaSessionCompat.QueueItem(
|
||||
Util.getMediaDescriptionForEntry(song),
|
||||
id.toLong())
|
||||
})
|
||||
mediaSession!!.setQueue(
|
||||
playlist.mapIndexed { id, song ->
|
||||
MediaSessionCompat.QueueItem(
|
||||
Util.getMediaDescriptionForEntry(song),
|
||||
id.toLong()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) {
|
||||
|
@ -264,7 +278,7 @@ class MediaSessionHandler : KoinComponent {
|
|||
// Playback position is updated too frequently in the player.
|
||||
// This counter makes sure that the MediaSession is updated ~ at every second
|
||||
playbackPositionDelayCount++
|
||||
if (playbackPositionDelayCount < 10) return
|
||||
if (playbackPositionDelayCount < CALL_DIVIDE) return
|
||||
|
||||
playbackPositionDelayCount = 0
|
||||
val playbackStateBuilder = PlaybackStateCompat.Builder()
|
||||
|
@ -286,7 +300,10 @@ class MediaSessionHandler : KoinComponent {
|
|||
}
|
||||
|
||||
private fun registerMediaButtonEventReceiver() {
|
||||
val component = ComponentName(applicationContext.packageName, MediaButtonIntentReceiver::class.java.name)
|
||||
val component = ComponentName(
|
||||
applicationContext.packageName,
|
||||
MediaButtonIntentReceiver::class.java.name
|
||||
)
|
||||
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
||||
mediaButtonIntent.component = component
|
||||
|
||||
|
@ -303,4 +320,4 @@ class MediaSessionHandler : KoinComponent {
|
|||
private fun unregisterMediaButtonEventReceiver() {
|
||||
mediaSession?.setMediaButtonReceiver(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic 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.
|
||||
|
||||
Subsonic 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.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
* Util.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
@ -51,16 +40,6 @@ import android.widget.Toast
|
|||
import androidx.annotation.AnyRes
|
||||
import androidx.media.utils.MediaConstants
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.domain.RepeatMode
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
@ -72,17 +51,32 @@ import java.io.OutputStream
|
|||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.domain.RepeatMode
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import timber.log.Timber
|
||||
|
||||
private const val LINE_LENGTH = 60
|
||||
private const val DEGRADE_PRECISION_AFTER = 10
|
||||
private const val MINUTES_IN_HOUR = 60
|
||||
private const val KBYTE = 1024
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
* Contains various utility functions
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
object Util {
|
||||
|
||||
private val GIGA_BYTE_FORMAT = DecimalFormat("0.00 GB")
|
||||
|
@ -171,17 +165,17 @@ object Util {
|
|||
fun applyTheme(context: Context?) {
|
||||
val theme = getTheme()
|
||||
if (Constants.PREFERENCES_KEY_THEME_DARK.equals(
|
||||
theme,
|
||||
ignoreCase = true
|
||||
) || "fullscreen".equals(theme, ignoreCase = true)
|
||||
theme,
|
||||
ignoreCase = true
|
||||
) || "fullscreen".equals(theme, ignoreCase = true)
|
||||
) {
|
||||
context!!.setTheme(R.style.UltrasonicTheme)
|
||||
} else if (Constants.PREFERENCES_KEY_THEME_BLACK.equals(theme, ignoreCase = true)) {
|
||||
context!!.setTheme(R.style.UltrasonicTheme_Black)
|
||||
} else if (Constants.PREFERENCES_KEY_THEME_LIGHT.equals(
|
||||
theme,
|
||||
ignoreCase = true
|
||||
) || "fullscreenlight".equals(theme, ignoreCase = true)
|
||||
theme,
|
||||
ignoreCase = true
|
||||
) || "fullscreenlight".equals(theme, ignoreCase = true)
|
||||
) {
|
||||
context!!.setTheme(R.style.UltrasonicTheme_Light)
|
||||
}
|
||||
|
@ -248,8 +242,9 @@ object Util {
|
|||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@Suppress("MagicNumber")
|
||||
fun copy(input: InputStream, output: OutputStream): Long {
|
||||
val buffer = ByteArray(1024 * 4)
|
||||
val buffer = ByteArray(KBYTE * 4)
|
||||
var count: Long = 0
|
||||
var n: Int
|
||||
while (-1 != input.read(buffer).also { n = it }) {
|
||||
|
@ -261,14 +256,16 @@ object Util {
|
|||
|
||||
@Throws(IOException::class)
|
||||
fun atomicCopy(from: File, to: File) {
|
||||
val tmp = File(String.format("%s.tmp", to.path))
|
||||
val `in` = FileInputStream(from)
|
||||
val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
|
||||
val input = FileInputStream(from)
|
||||
val out = FileOutputStream(tmp)
|
||||
try {
|
||||
`in`.channel.transferTo(0, from.length(), out.channel)
|
||||
input.channel.transferTo(0, from.length(), out.channel)
|
||||
out.close()
|
||||
if (!tmp.renameTo(to)) {
|
||||
throw IOException(String.format("Failed to rename %s to %s", tmp, to))
|
||||
throw IOException(
|
||||
String.format(Locale.ROOT, "Failed to rename %s to %s", tmp, to)
|
||||
)
|
||||
}
|
||||
Timber.i("Copied %s to %s", from, to)
|
||||
} catch (x: IOException) {
|
||||
|
@ -276,7 +273,7 @@ object Util {
|
|||
delete(to)
|
||||
throw x
|
||||
} finally {
|
||||
close(`in`)
|
||||
close(input)
|
||||
close(out)
|
||||
delete(tmp)
|
||||
}
|
||||
|
@ -296,7 +293,7 @@ object Util {
|
|||
fun close(closeable: Closeable?) {
|
||||
try {
|
||||
closeable?.close()
|
||||
} catch (x: Throwable) {
|
||||
} catch (_: Throwable) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
@ -376,18 +373,18 @@ object Util {
|
|||
fun formatBytes(byteCount: Long): String {
|
||||
|
||||
// More than 1 GB?
|
||||
if (byteCount >= 1024 * 1024 * 1024) {
|
||||
return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (1024 * 1024 * 1024))
|
||||
if (byteCount >= KBYTE * KBYTE * KBYTE) {
|
||||
return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
|
||||
}
|
||||
|
||||
// More than 1 MB?
|
||||
if (byteCount >= 1024 * 1024) {
|
||||
return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (1024 * 1024))
|
||||
if (byteCount >= KBYTE * KBYTE) {
|
||||
return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE))
|
||||
}
|
||||
|
||||
// More than 1 KB?
|
||||
return if (byteCount >= 1024) {
|
||||
KILO_BYTE_FORMAT.format(byteCount.toDouble() / 1024)
|
||||
return if (byteCount >= KBYTE) {
|
||||
KILO_BYTE_FORMAT.format(byteCount.toDouble() / KBYTE)
|
||||
} else "$byteCount B"
|
||||
}
|
||||
|
||||
|
@ -406,35 +403,36 @@ object Util {
|
|||
* @return The formatted string.
|
||||
*/
|
||||
@Synchronized
|
||||
@Suppress("ReturnCount")
|
||||
fun formatLocalizedBytes(byteCount: Long, context: Context): String {
|
||||
|
||||
// More than 1 GB?
|
||||
if (byteCount >= 1024 * 1024 * 1024) {
|
||||
if (byteCount >= KBYTE * KBYTE * KBYTE) {
|
||||
if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
|
||||
GIGA_BYTE_LOCALIZED_FORMAT =
|
||||
DecimalFormat(context.resources.getString(R.string.util_bytes_format_gigabyte))
|
||||
}
|
||||
return GIGA_BYTE_LOCALIZED_FORMAT!!
|
||||
.format(byteCount.toDouble() / (1024 * 1024 * 1024))
|
||||
.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
|
||||
}
|
||||
|
||||
// More than 1 MB?
|
||||
if (byteCount >= 1024 * 1024) {
|
||||
if (byteCount >= KBYTE * KBYTE) {
|
||||
if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
|
||||
MEGA_BYTE_LOCALIZED_FORMAT =
|
||||
DecimalFormat(context.resources.getString(R.string.util_bytes_format_megabyte))
|
||||
}
|
||||
return MEGA_BYTE_LOCALIZED_FORMAT!!
|
||||
.format(byteCount.toDouble() / (1024 * 1024))
|
||||
.format(byteCount.toDouble() / (KBYTE * KBYTE))
|
||||
}
|
||||
|
||||
// More than 1 KB?
|
||||
if (byteCount >= 1024) {
|
||||
if (byteCount >= KBYTE) {
|
||||
if (KILO_BYTE_LOCALIZED_FORMAT == null) {
|
||||
KILO_BYTE_LOCALIZED_FORMAT =
|
||||
DecimalFormat(context.resources.getString(R.string.util_bytes_format_kilobyte))
|
||||
}
|
||||
return KILO_BYTE_LOCALIZED_FORMAT!!.format(byteCount.toDouble() / 1024)
|
||||
return KILO_BYTE_LOCALIZED_FORMAT!!.format(byteCount.toDouble() / KBYTE)
|
||||
}
|
||||
if (BYTE_LOCALIZED_FORMAT == null) {
|
||||
BYTE_LOCALIZED_FORMAT =
|
||||
|
@ -453,6 +451,7 @@ object Util {
|
|||
* @param s The string to encode.
|
||||
* @return The encoded string.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
|
||||
fun utf8HexEncode(s: String?): String? {
|
||||
if (s == null) {
|
||||
return null
|
||||
|
@ -473,6 +472,7 @@ object Util {
|
|||
* @param data Bytes to convert to hexadecimal characters.
|
||||
* @return A string containing hexadecimal characters.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun hexEncode(data: ByteArray): String {
|
||||
val length = data.size
|
||||
val out = CharArray(length shl 1)
|
||||
|
@ -493,6 +493,7 @@ object Util {
|
|||
* @return MD5 digest as a hex string.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
|
||||
fun md5Hex(s: String?): String? {
|
||||
return if (s == null) {
|
||||
null
|
||||
|
@ -567,7 +568,11 @@ object Util {
|
|||
.setIcon(icon)
|
||||
.setTitle(titleId)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.common_ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
|
||||
.setPositiveButton(R.string.common_ok) {
|
||||
dialog: DialogInterface,
|
||||
_: Int ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -746,6 +751,7 @@ object Util {
|
|||
context.sendBroadcast(avrcpIntent)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun broadcastA2dpPlayStatusChange(
|
||||
context: Context,
|
||||
state: PlayerState?,
|
||||
|
@ -763,7 +769,7 @@ object Util {
|
|||
return
|
||||
}
|
||||
|
||||
// FIXME: This is probably a bug.
|
||||
// FIXME This is probably a bug.
|
||||
if (currentSong !== currentSong) {
|
||||
Util.currentSong = currentSong
|
||||
}
|
||||
|
@ -797,11 +803,12 @@ object Util {
|
|||
|
||||
when (state) {
|
||||
PlayerState.STARTED -> avrcpIntent.putExtra("playing", true)
|
||||
PlayerState.STOPPED, PlayerState.PAUSED, PlayerState.COMPLETED -> avrcpIntent.putExtra(
|
||||
PlayerState.STOPPED, PlayerState.PAUSED,
|
||||
PlayerState.COMPLETED -> avrcpIntent.putExtra(
|
||||
"playing",
|
||||
false
|
||||
)
|
||||
else -> return // No need to broadcast.
|
||||
else -> return // No need to broadcast.
|
||||
}
|
||||
|
||||
context.sendBroadcast(avrcpIntent)
|
||||
|
@ -819,12 +826,13 @@ object Util {
|
|||
PlayerState.STOPPED -> intent.putExtra("state", "stop")
|
||||
PlayerState.PAUSED -> intent.putExtra("state", "pause")
|
||||
PlayerState.COMPLETED -> intent.putExtra("state", "complete")
|
||||
else -> return // No need to broadcast.
|
||||
else -> return // No need to broadcast.
|
||||
}
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("MagicNumber")
|
||||
fun getNotificationImageSize(context: Context): Int {
|
||||
val metrics = context.resources.displayMetrics
|
||||
val imageSizeLarge =
|
||||
|
@ -838,6 +846,7 @@ object Util {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getAlbumImageSize(context: Context?): Int {
|
||||
val metrics = context!!.resources.displayMetrics
|
||||
val imageSizeLarge =
|
||||
|
@ -1027,11 +1036,11 @@ object Util {
|
|||
}
|
||||
val hours = TimeUnit.MILLISECONDS.toHours(millis)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours)
|
||||
val seconds =
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(hours * 60 + minutes)
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
|
||||
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
|
||||
|
||||
return when {
|
||||
hours >= 10 -> {
|
||||
hours >= DEGRADE_PRECISION_AFTER -> {
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d:%02d:%02d",
|
||||
|
@ -1043,7 +1052,7 @@ object Util {
|
|||
hours > 0 -> {
|
||||
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
minutes >= 10 -> {
|
||||
minutes >= DEGRADE_PRECISION_AFTER -> {
|
||||
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
minutes > 0 -> String.format(
|
||||
|
@ -1254,12 +1263,13 @@ object Util {
|
|||
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(
|
||||
ContentResolver.SCHEME_ANDROID_RESOURCE +
|
||||
"://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId)
|
||||
"://" + context.resources.getResourcePackageName(drawableId) +
|
||||
'/' + context.resources.getResourceTypeName(drawableId) +
|
||||
'/' + context.resources.getResourceEntryName(drawableId)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun getMediaDescriptionForEntry(
|
||||
song: MusicDirectory.Entry,
|
||||
mediaId: String? = null,
|
||||
|
@ -1267,12 +1277,14 @@ object Util {
|
|||
): MediaDescriptionCompat {
|
||||
|
||||
val descriptionBuilder = MediaDescriptionCompat.Builder()
|
||||
val artist = StringBuilder(60)
|
||||
val artist = StringBuilder(LINE_LENGTH)
|
||||
var bitRate: String? = null
|
||||
|
||||
val duration = song.duration
|
||||
if (duration != null) {
|
||||
artist.append(String.format("%s ", formatTotalDuration(duration.toLong())))
|
||||
artist.append(
|
||||
String.format(Locale.ROOT, "%s ", formatTotalDuration(duration.toLong()))
|
||||
)
|
||||
}
|
||||
|
||||
if (song.bitRate != null && song.bitRate!! > 0)
|
||||
|
@ -1286,16 +1298,21 @@ object Util {
|
|||
|
||||
fileFormat = if (
|
||||
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo
|
||||
) suffix else String.format("%s > %s", suffix, transcodedSuffix)
|
||||
) suffix else String.format(Locale.ROOT, "%s > %s", suffix, transcodedSuffix)
|
||||
|
||||
val artistName = song.artist
|
||||
|
||||
if (artistName != null) {
|
||||
if (shouldDisplayBitrateWithArtist() && (!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank())) {
|
||||
if (shouldDisplayBitrateWithArtist() && (
|
||||
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
|
||||
)
|
||||
) {
|
||||
artist.append(artistName).append(" (").append(
|
||||
String.format(
|
||||
appContext().getString(R.string.song_details_all),
|
||||
if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat
|
||||
if (bitRate == null) ""
|
||||
else String.format(Locale.ROOT, "%s ", bitRate),
|
||||
fileFormat
|
||||
)
|
||||
).append(')')
|
||||
} else {
|
||||
|
@ -1305,9 +1322,9 @@ object Util {
|
|||
|
||||
val trackNumber = song.track ?: 0
|
||||
|
||||
val title = StringBuilder(60)
|
||||
val title = StringBuilder(LINE_LENGTH)
|
||||
if (shouldShowTrackNumber() && trackNumber > 0)
|
||||
title.append(String.format("%02d - ", trackNumber))
|
||||
title.append(String.format(Locale.ROOT, "%02d - ", trackNumber))
|
||||
|
||||
title.append(song.title)
|
||||
|
||||
|
@ -1315,16 +1332,22 @@ object Util {
|
|||
title.append(" (").append(
|
||||
String.format(
|
||||
appContext().getString(R.string.song_details_all),
|
||||
if (bitRate == null) "" else String.format("%s ", bitRate), fileFormat
|
||||
if (bitRate == null) ""
|
||||
else String.format(Locale.ROOT, "%s ", bitRate),
|
||||
fileFormat
|
||||
)
|
||||
).append(')')
|
||||
}
|
||||
|
||||
if (groupNameId != null)
|
||||
descriptionBuilder.setExtras(Bundle().apply { putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
appContext().getString(groupNameId)
|
||||
) })
|
||||
descriptionBuilder.setExtras(
|
||||
Bundle().apply {
|
||||
putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
appContext().getString(groupNameId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
descriptionBuilder.setTitle(title)
|
||||
descriptionBuilder.setSubtitle(artist)
|
||||
|
@ -1344,4 +1367,4 @@ object Util {
|
|||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M9,13.75c-2.34,0 -7,1.17 -7,3.5L2,19h14v-1.75c0,-2.33 -4.66,-3.5 -7,-3.5zM4.34,17c0.84,-0.58 2.87,-1.25 4.66,-1.25s3.82,0.67 4.66,1.25L4.34,17zM9,12c1.93,0 3.5,-1.57 3.5,-3.5S10.93,5 9,5 5.5,6.57 5.5,8.5 7.07,12 9,12zM9,7c0.83,0 1.5,0.67 1.5,1.5S9.83,10 9,10s-1.5,-0.67 -1.5,-1.5S8.17,7 9,7zM16.04,13.81c1.16,0.84 1.96,1.96 1.96,3.44L18,19h4v-1.75c0,-2.02 -3.5,-3.17 -5.96,-3.44zM15,12c1.93,0 3.5,-1.57 3.5,-3.5S16.93,5 15,5c-0.54,0 -1.04,0.13 -1.5,0.35 0.63,0.89 1,1.98 1,3.15s-0.37,2.26 -1,3.15c0.46,0.22 0.96,0.35 1.5,0.35z"/>
|
||||
</vector>
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="M22,6h-5v8.18C16.69,14.07 16.35,14 16,14c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3V8h3V6zM15,6H3v2h12V6zM15,10H3v2h12V10zM11,14H3v2h8V14z"/>
|
||||
</vector>
|
||||
|
|
Loading…
Reference in New Issue