Fixed lint errors

Added comments
This commit is contained in:
Nite 2021-07-18 13:17:29 +02:00
parent cf05d3c781
commit 982639d2c7
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
13 changed files with 385 additions and 249 deletions

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moire.ultrasonic" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"> package="org.moire.ultrasonic"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@ -60,6 +61,7 @@
</service> </service>
<service <service
tools:ignore="ExportedService"
android:name=".service.AutoMediaBrowserService" android:name=".service.AutoMediaBrowserService"
android:label="@string/common.appname" android:label="@string/common.appname"
android:exported="true"> android:exported="true">

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.di package org.moire.ultrasonic.di
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.service.AudioFocusHandler import org.moire.ultrasonic.service.AudioFocusHandler
import org.moire.ultrasonic.service.DownloadQueueSerializer import org.moire.ultrasonic.service.DownloadQueueSerializer

View File

@ -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 package org.moire.ultrasonic.service
import android.os.Bundle import android.os.Bundle
@ -24,44 +31,45 @@ import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID" private const val MEDIA_ROOT_ID = "MEDIA_ROOT_ID"
const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID" private const val MEDIA_ALBUM_ID = "MEDIA_ALBUM_ID"
const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID" private const val MEDIA_ALBUM_PAGE_ID = "MEDIA_ALBUM_PAGE_ID"
const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID" private const val MEDIA_ALBUM_NEWEST_ID = "MEDIA_ALBUM_NEWEST_ID"
const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID" private const val MEDIA_ALBUM_RECENT_ID = "MEDIA_ALBUM_RECENT_ID"
const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID" private const val MEDIA_ALBUM_FREQUENT_ID = "MEDIA_ALBUM_FREQUENT_ID"
const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID" private const val MEDIA_ALBUM_RANDOM_ID = "MEDIA_ALBUM_RANDOM_ID"
const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID" private const val MEDIA_ALBUM_STARRED_ID = "MEDIA_ALBUM_STARRED_ID"
const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID" private const val MEDIA_SONG_RANDOM_ID = "MEDIA_SONG_RANDOM_ID"
const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID" private const val MEDIA_SONG_STARRED_ID = "MEDIA_SONG_STARRED_ID"
const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID" private const val MEDIA_ARTIST_ID = "MEDIA_ARTIST_ID"
const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID" private const val MEDIA_LIBRARY_ID = "MEDIA_LIBRARY_ID"
const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID" private const val MEDIA_PLAYLIST_ID = "MEDIA_PLAYLIST_ID"
const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID" private const val MEDIA_SHARE_ID = "MEDIA_SHARE_ID"
const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID" private const val MEDIA_BOOKMARK_ID = "MEDIA_BOOKMARK_ID"
const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID" private const val MEDIA_PODCAST_ID = "MEDIA_PODCAST_ID"
const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM" private const val MEDIA_ALBUM_ITEM = "MEDIA_ALBUM_ITEM"
const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM" private const val MEDIA_PLAYLIST_SONG_ITEM = "MEDIA_PLAYLIST_SONG_ITEM"
const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM" private const val MEDIA_PLAYLIST_ITEM = "MEDIA_PLAYLIST_ITEM"
const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM" private const val MEDIA_ARTIST_ITEM = "MEDIA_ARTIST_ITEM"
const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION" private const val MEDIA_ARTIST_SECTION = "MEDIA_ARTIST_SECTION"
const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM" private const val MEDIA_ALBUM_SONG_ITEM = "MEDIA_ALBUM_SONG_ITEM"
const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM" private const val MEDIA_SONG_STARRED_ITEM = "MEDIA_SONG_STARRED_ITEM"
const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM" private const val MEDIA_SONG_RANDOM_ITEM = "MEDIA_SONG_RANDOM_ITEM"
const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM" private const val MEDIA_SHARE_ITEM = "MEDIA_SHARE_ITEM"
const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM" private const val MEDIA_SHARE_SONG_ITEM = "MEDIA_SHARE_SONG_ITEM"
const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM" private const val MEDIA_BOOKMARK_ITEM = "MEDIA_BOOKMARK_ITEM"
const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM" private const val MEDIA_PODCAST_ITEM = "MEDIA_PODCAST_ITEM"
const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM" private const val MEDIA_PODCAST_EPISODE_ITEM = "MEDIA_PODCAST_EPISODE_ITEM"
const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM" private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
// Currently the display limit for long lists is 100 items // Currently the display limit for long lists is 100 items
const val displayLimit = 100 private const val DISPLAY_LIMIT = 100
const val searchLimit = 10 private const val SEARCH_LIMIT = 10
/** /**
* MediaBrowserService implementation for e.g. Android Auto * MediaBrowserService implementation for e.g. Android Auto
*/ */
@Suppress("TooManyFunctions", "LargeClass")
class AutoMediaBrowserService : MediaBrowserServiceCompat() { class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private lateinit var mediaSessionEventListener: MediaSessionEventListener private lateinit var mediaSessionEventListener: MediaSessionEventListener
@ -84,6 +92,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private val useId3Tags get() = Util.getShouldUseId3Tags() private val useId3Tags get() = Util.getShouldUseId3Tags()
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
@Suppress("MagicNumber")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -95,16 +104,23 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
override fun onPlayFromMediaIdRequested(mediaId: String?, extras: Bundle?) { 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 if (mediaId == null) return
val mediaIdParts = mediaId.split('|') val mediaIdParts = mediaId.split('|')
when (mediaIdParts.first()) { when (mediaIdParts.first()) {
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2]) 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_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_ID -> playStarredSongs()
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1]) MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
MEDIA_SONG_RANDOM_ID -> playRandomSongs() MEDIA_SONG_RANDOM_ID -> playRandomSongs()
@ -113,7 +129,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2]) MEDIA_SHARE_SONG_ITEM -> playShareSong(mediaIdParts[1], mediaIdParts[2])
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1]) MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
MEDIA_PODCAST_ITEM -> playPodcast(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]) MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
} }
} }
@ -123,7 +141,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
if (query.isNullOrBlank()) playRandomSongs() if (query.isNullOrBlank()) playRandomSongs()
serviceScope.launch { serviceScope.launch {
val criteria = SearchCriteria(query!!, 0, 0, displayLimit) val criteria = SearchCriteria(query!!, 0, 0, DISPLAY_LIMIT)
val searchResult = callWithErrorHandling { musicService.search(criteria) } val searchResult = callWithErrorHandling { musicService.search(criteria) }
// Try to find the best match // Try to find the best match
@ -146,12 +164,17 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
mediaSessionHandler.initialize() mediaSessionHandler.initialize()
val handler = Handler() val handler = Handler()
handler.postDelayed({ handler.postDelayed(
// Ultrasonic may be started from Android Auto. This boots up the necessary components. {
Timber.d("AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService...") // Ultrasonic may be started from Android Auto. This boots up the necessary components.
lifecycleSupport.onCreate() Timber.d(
MediaPlayerService.getInstance() "AutoMediaBrowserService starting lifecycleSupport and MediaPlayerService..."
}, 100) )
lifecycleSupport.onCreate()
MediaPlayerService.getInstance()
},
100
)
Timber.i("AutoMediaBrowserService onCreate finished") Timber.i("AutoMediaBrowserService onCreate finished")
} }
@ -170,21 +193,28 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
clientUid: Int, clientUid: Int,
rootHints: Bundle? rootHints: Bundle?
): BrowserRoot { ): 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() val extras = Bundle()
extras.putInt( extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, 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( extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, 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( extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true) MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
)
return BrowserRoot(MEDIA_ROOT_ID, extras) return BrowserRoot(MEDIA_ROOT_ID, extras)
} }
@Suppress("ReturnCount", "ComplexMethod")
override fun onLoadChildren( override fun onLoadChildren(
parentId: String, parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>> result: Result<MutableList<MediaBrowserCompat.MediaItem>>
@ -199,7 +229,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
MEDIA_ARTIST_ID -> return getArtists(result) MEDIA_ARTIST_ID -> return getArtists(result)
MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1]) MEDIA_ARTIST_SECTION -> return getArtists(result, parentIdParts[1])
MEDIA_ALBUM_ID -> return getAlbums(result, AlbumListType.SORTED_BY_NAME) 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_PLAYLIST_ID -> return getPlaylists(result)
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT) MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(result, AlbumListType.FREQUENT)
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST) MEDIA_ALBUM_NEWEST_ID -> return getAlbums(result, AlbumListType.NEWEST)
@ -212,7 +244,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
MEDIA_BOOKMARK_ID -> return getBookmarks(result) MEDIA_BOOKMARK_ID -> return getBookmarks(result)
MEDIA_PODCAST_ID -> return getPodcasts(result) MEDIA_PODCAST_ID -> return getPodcasts(result)
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2], 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_ALBUM_ITEM -> return getSongsForAlbum(result, parentIdParts[1], parentIdParts[2])
MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1]) MEDIA_SHARE_ITEM -> return getSongsForShare(result, parentIdParts[1])
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1]) MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(result, parentIdParts[1])
@ -230,7 +264,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.detach() result.detach()
serviceScope.launch { 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) } val searchResult = callWithErrorHandling { musicService.search(criteria) }
// TODO Add More... button to categories // 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 { serviceScope.launch {
// If there is no cache, we can't play the selected song. // If there is no cache, we can't play the selected song.
if (searchSongsCache != null) { if (searchSongsCache != null) {
@ -380,7 +414,10 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.sendResult(mediaItems) 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() val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach() result.detach()
@ -404,7 +441,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
// If there are too many artists, create alphabetic index of them // 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>() val index = mutableListOf<String>()
// TODO This sort should use ignoredArticles somehow... // TODO This sort should use ignoredArticles somehow...
artists = artists.sortedBy { artist -> artist.name } artists = artists.sortedBy { artist -> artist.name }
@ -442,7 +479,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.detach() result.detach()
serviceScope.launch { serviceScope.launch {
val albums = if (!isOffline && useId3Tags) { val albums = if (!isOffline && useId3Tags) {
callWithErrorHandling { musicService.getArtist(id, name,false) } callWithErrorHandling { musicService.getArtist(id, name, false) }
} else { } else {
callWithErrorHandling { musicService.getMusicDirectory(id, name, false) } callWithErrorHandling { musicService.getMusicDirectory(id, name, false) }
} }
@ -477,7 +514,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all? // 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 -> items.map { item ->
if (item.isDirectory) if (item.isDirectory)
mediaItems.add( mediaItems.add(
@ -518,11 +555,19 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList() val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach() result.detach()
serviceScope.launch { serviceScope.launch {
val offset = (page ?: 0) * displayLimit val offset = (page ?: 0) * DISPLAY_LIMIT
val albums = if (useId3Tags) { val albums = if (useId3Tags) {
callWithErrorHandling { musicService.getAlbumList2(type.typeName, displayLimit, offset, null) } callWithErrorHandling {
musicService.getAlbumList2(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
} else { } else {
callWithErrorHandling { musicService.getAlbumList(type.typeName, displayLimit, offset, null) } callWithErrorHandling {
musicService.getAlbumList(
type.typeName, DISPLAY_LIMIT, offset, null
)
}
} }
albums?.getAllChild()?.map { album -> 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( mediaItems.add(
R.string.search_more, R.string.search_more,
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"), 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() val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
result.detach() result.detach()
@ -579,7 +628,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
// Playlist should be cached as it may contain random elements // Playlist should be cached as it may contain random elements
playlistCache = content.getAllChild() playlistCache = content.getAllChild()
playlistCache!!.take(displayLimit).map { item -> playlistCache!!.take(DISPLAY_LIMIT).map { item ->
mediaItems.add( mediaItems.add(
MediaBrowserCompat.MediaItem( MediaBrowserCompat.MediaItem(
Util.getMediaDescriptionForEntry( Util.getMediaDescriptionForEntry(
@ -618,7 +667,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val content = callWithErrorHandling { musicService.getPlaylist(id, name) } val content = callWithErrorHandling { musicService.getPlaylist(id, name) }
playlistCache = content?.getAllChild() playlistCache = content?.getAllChild()
} }
val song = playlistCache?.firstOrNull{x -> x.id == songId} val song = playlistCache?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song) if (song != null) playSong(song)
} }
} }
@ -633,7 +682,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun playAlbumSong(id: String, name: String, songId: String) { private fun playAlbumSong(id: String, name: String, songId: String) {
serviceScope.launch { serviceScope.launch {
val songs = listSongsInMusicService(id, name) 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) if (song != null) playSong(song)
} }
} }
@ -669,14 +718,16 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
episodes.getAllChild().map { episode -> episodes.getAllChild().map { episode ->
mediaItems.add(MediaBrowserCompat.MediaItem( mediaItems.add(
Util.getMediaDescriptionForEntry( MediaBrowserCompat.MediaItem(
episode, Util.getMediaDescriptionForEntry(
listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id) episode,
.joinToString("|") listOf(MEDIA_PODCAST_EPISODE_ITEM, id, episode.id)
), .joinToString("|")
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE ),
)) MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
} }
result.sendResult(mediaItems) result.sendResult(mediaItems)
} }
@ -713,13 +764,15 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val songs = Util.getSongsFromBookmarks(bookmarks) val songs = Util.getSongsFromBookmarks(bookmarks)
songs.getAllChild().map { song -> songs.getAllChild().map { song ->
mediaItems.add(MediaBrowserCompat.MediaItem( mediaItems.add(
Util.getMediaDescriptionForEntry( MediaBrowserCompat.MediaItem(
song, Util.getMediaDescriptionForEntry(
listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|") song,
), listOf(MEDIA_BOOKMARK_ITEM, song.id).joinToString("|")
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE ),
)) MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
} }
result.sendResult(mediaItems) result.sendResult(mediaItems)
} }
@ -731,7 +784,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val bookmarks = callWithErrorHandling { musicService.getBookmarks() } val bookmarks = callWithErrorHandling { musicService.getBookmarks() }
if (bookmarks != null) { if (bookmarks != null) {
val songs = Util.getSongsFromBookmarks(bookmarks) 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) if (song != null) playSong(song)
} }
} }
@ -766,20 +819,22 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
serviceScope.launch { serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) } 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 != null) {
if (selectedShare.getEntries().count() > 1) if (selectedShare.getEntries().count() > 1)
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
selectedShare.getEntries().map { song -> selectedShare.getEntries().map { song ->
mediaItems.add(MediaBrowserCompat.MediaItem( mediaItems.add(
Util.getMediaDescriptionForEntry( MediaBrowserCompat.MediaItem(
song, Util.getMediaDescriptionForEntry(
listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|") song,
), listOf(MEDIA_SHARE_SONG_ITEM, id, song.id).joinToString("|")
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE ),
)) MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
} }
} }
result.sendResult(mediaItems) result.sendResult(mediaItems)
@ -789,7 +844,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun playShare(id: String) { private fun playShare(id: String) {
serviceScope.launch { serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) } 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 != null) {
playSongs(selectedShare.getEntries()) playSongs(selectedShare.getEntries())
} }
@ -799,9 +854,9 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun playShareSong(id: String, songId: String) { private fun playShareSong(id: String, songId: String) {
serviceScope.launch { serviceScope.launch {
val shares = callWithErrorHandling { musicService.getShares(false) } 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 != null) {
val song = selectedShare.getEntries().firstOrNull{x -> x.id == songId} val song = selectedShare.getEntries().firstOrNull { x -> x.id == songId }
if (song != null) playSong(song) if (song != null) playSong(song)
} }
} }
@ -819,7 +874,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|")) mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
// TODO: Paging is not implemented for songs, is it necessary at all? // 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 starredSongsCache = items
items.map { song -> items.map { song ->
mediaItems.add( mediaItems.add(
@ -855,7 +910,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
val content = listStarredSongsInMusicService() val content = listStarredSongsInMusicService()
starredSongsCache = content?.songs starredSongsCache = content?.songs
} }
val song = starredSongsCache?.firstOrNull{x -> x.id == songId} val song = starredSongsCache?.firstOrNull { x -> x.id == songId }
if (song != null) playSong(song) if (song != null) playSong(song)
} }
} }
@ -865,7 +920,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
result.detach() result.detach()
serviceScope.launch { serviceScope.launch {
val songs = callWithErrorHandling { musicService.getRandomSongs(displayLimit) } val songs = callWithErrorHandling { musicService.getRandomSongs(DISPLAY_LIMIT) }
if (songs != null) { if (songs != null) {
if (songs.getAllChild().count() > 1) if (songs.getAllChild().count() > 1)
@ -895,7 +950,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
if (randomSongsCache == null) { if (randomSongsCache == null) {
// This can only happen if Android Auto cached items, but Ultrasonic has forgot them // 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 // 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() randomSongsCache = content?.getAllChild()
} }
if (randomSongsCache != null) playSongs(randomSongsCache) if (randomSongsCache != null) playSongs(randomSongsCache)
@ -942,10 +997,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
if (groupNameId != null) if (groupNameId != null)
builder.setExtras(Bundle().apply { putString( builder.setExtras(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, Bundle().apply {
getString(groupNameId) putString(
) }) MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
getString(groupNameId)
)
}
)
val mediaItem = MediaBrowserCompat.MediaItem( val mediaItem = MediaBrowserCompat.MediaItem(
builder.build(), builder.build(),
@ -970,10 +1029,14 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
builder.setIconUri(Util.getUriToDrawable(applicationContext, icon)) builder.setIconUri(Util.getUriToDrawable(applicationContext, icon))
if (groupNameId != null) if (groupNameId != null)
builder.setExtras(Bundle().apply { putString( builder.setExtras(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, Bundle().apply {
getString(groupNameId) putString(
) }) MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
getString(groupNameId)
)
}
)
val mediaItem = MediaBrowserCompat.MediaItem( val mediaItem = MediaBrowserCompat.MediaItem(
builder.build(), builder.build(),
@ -1034,4 +1097,4 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
null null
} }
} }
} }

View File

@ -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 package org.moire.ultrasonic.service
import android.content.Context 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob 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.FileUtil
import org.moire.ultrasonic.util.MediaSessionHandler import org.moire.ultrasonic.util.MediaSessionHandler
import timber.log.Timber 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 * This class is responsible for the serialization / deserialization
@ -102,4 +109,4 @@ class DownloadQueueSerializer : KoinComponent {
mediaSessionHandler.updateMediaSessionQueue(state.songs) mediaSessionHandler.updateMediaSessionQueue(state.songs)
afterDeserialized.accept(state) afterDeserialized.accept(state)
} }
} }

View File

@ -21,13 +21,13 @@ import android.os.PowerManager
import android.os.PowerManager.PARTIAL_WAKE_LOCK import android.os.PowerManager.PARTIAL_WAKE_LOCK
import android.os.PowerManager.WakeLock import android.os.PowerManager.WakeLock
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File import java.io.File
import java.net.URLEncoder import java.net.URLEncoder
import java.util.Locale import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max 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.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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 * 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 audioFocusHandler by inject<AudioFocusHandler>()
private val context by inject<Context>() private val context by inject<Context>()

View File

@ -1,21 +1,10 @@
/* /*
This file is part of Subsonic. * MediaPlayerLifecycleSupport.kt
* Copyright (C) 2009-2021 Ultrasonic developers
Subsonic is free software: you can redistribute it and/or modify *
it under the terms of the GNU General Public License as published by * Distributed under terms of the GNU GPLv3 license.
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
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -25,7 +14,6 @@ import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.view.KeyEvent import android.view.KeyEvent
import kotlinx.coroutines.newFixedThreadPoolContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -85,7 +73,8 @@ class MediaPlayerLifecycleSupport : KoinComponent {
false 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( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.downloadList,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
@ -179,14 +168,15 @@ class MediaPlayerLifecycleSupport : KoinComponent {
val headsetIntentFilter: IntentFilter = val headsetIntentFilter: IntentFilter =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
IntentFilter(AudioManager.ACTION_HEADSET_PLUG) IntentFilter(AudioManager.ACTION_HEADSET_PLUG)
} else { } else {
IntentFilter(Intent.ACTION_HEADSET_PLUG) IntentFilter(Intent.ACTION_HEADSET_PLUG)
} }
applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter) applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter)
} }
@Suppress("MagicNumber", "ComplexMethod")
private fun handleKeyEvent(event: KeyEvent) { private fun handleKeyEvent(event: KeyEvent) {
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return
@ -195,9 +185,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
val receivedKeyCode = event.keyCode val receivedKeyCode = event.keyCode
// Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices // Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices
keyCode = if (Util.getSingleButtonPlayPause() && keyCode = if (Util.getSingleButtonPlayPause() && (
(receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY || receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
)
) { ) {
Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE") Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE")
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
@ -221,10 +212,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
KeyEvent.KEYCODE_MEDIA_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY ->
if (mediaPlayerController.playerState === PlayerState.IDLE) { if (mediaPlayerController.playerState === PlayerState.IDLE) {
mediaPlayerController.play() mediaPlayerController.play()
} else if (mediaPlayerController.playerState !== PlayerState.STARTED) { } else if (mediaPlayerController.playerState !== PlayerState.STARTED) {
mediaPlayerController.start() mediaPlayerController.start()
} }
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1) KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1)
@ -242,13 +233,18 @@ class MediaPlayerLifecycleSupport : KoinComponent {
/** /**
* This function processes the intent that could come from other applications. * This function processes the intent that could come from other applications.
*/ */
@Suppress("ComplexMethod")
private fun handleUltrasonicIntent(intentAction: String) { private fun handleUltrasonicIntent(intentAction: String) {
val isRunning = created val isRunning = created
// If Ultrasonic is not running, do nothing to stop or pause // If Ultrasonic is not running, do nothing to stop or pause
if (!isRunning && (intentAction == Constants.CMD_PAUSE || if (
intentAction == Constants.CMD_STOP)) return !isRunning && (
intentAction == Constants.CMD_PAUSE ||
intentAction == Constants.CMD_STOP
)
) return
val autoStart = val autoStart =
intentAction == Constants.CMD_PLAY || intentAction == Constants.CMD_PLAY ||
@ -261,7 +257,9 @@ class MediaPlayerLifecycleSupport : KoinComponent {
onCreate(autoStart) { onCreate(autoStart) {
when (intentAction) { when (intentAction) {
Constants.CMD_PLAY -> mediaPlayerController.play() 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() if (isRunning) mediaPlayerController.resumeOrPlay()
Constants.CMD_NEXT -> mediaPlayerController.next() Constants.CMD_NEXT -> mediaPlayerController.next()
@ -277,4 +275,4 @@ class MediaPlayerLifecycleSupport : KoinComponent {
} }
} }
} }
} }

View File

@ -7,7 +7,11 @@
package org.moire.ultrasonic.service 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.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@ -93,7 +97,7 @@ class MediaPlayerService : Service() {
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() } localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
mediaSessionEventListener = object:MediaSessionEventListener { mediaSessionEventListener = object : MediaSessionEventListener {
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) { override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
mediaSessionToken = token mediaSessionToken = token
} }
@ -383,7 +387,11 @@ class MediaPlayerService : Service() {
val context = this@MediaPlayerService val context = this@MediaPlayerService
// Notify MediaSession // Notify MediaSession
mediaSessionHandler.updateMediaSession(currentPlaying, downloader.currentPlayingIndex.toLong(), playerState) mediaSessionHandler.updateMediaSession(
currentPlaying,
downloader.currentPlayingIndex.toLong(),
playerState
)
if (playerState === PlayerState.PAUSED) { if (playerState === PlayerState.PAUSED) {
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
@ -535,7 +543,11 @@ class MediaPlayerService : Service() {
// Init // Init
val context = applicationContext val context = applicationContext
val song = currentPlaying?.song 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 // We should use a single notification builder, otherwise the notification may not be updated
if (notificationBuilder == null) { if (notificationBuilder == null) {

View File

@ -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 package org.moire.ultrasonic.util
import android.os.Bundle import android.os.Bundle
@ -41,7 +48,10 @@ class MediaSessionEventDistributor {
} }
fun raisePlayFromMediaIdRequestedEvent(mediaId: String?, extras: Bundle?) { 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?) { fun raisePlayFromSearchRequestedEvent(query: String?, extras: Bundle?) {

View File

@ -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 package org.moire.ultrasonic.util
import android.os.Bundle import android.os.Bundle

View File

@ -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 package org.moire.ultrasonic.util
import android.app.PendingIntent import android.app.PendingIntent
@ -21,7 +28,7 @@ import org.moire.ultrasonic.service.DownloadFile
import timber.log.Timber import timber.log.Timber
private const val INTENT_CODE_MEDIA_BUTTON = 161 private const val INTENT_CODE_MEDIA_BUTTON = 161
private const val CALL_DIVIDE = 10
/** /**
* Central place to handle the state of the MediaSession * Central place to handle the state of the MediaSession
*/ */
@ -157,7 +164,12 @@ class MediaSessionHandler : KoinComponent {
Timber.i("MediaSessionHandler.initialize Media Session created") 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") Timber.d("Updating the MediaSession")
// Set Metadata // Set Metadata
@ -240,18 +252,20 @@ class MediaSessionHandler : KoinComponent {
mediaSession!!.setPlaybackState(playbackStateBuilder.build()) 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 // This call is cached because Downloader may initialize earlier than the MediaSession
cachedPlaylist = playlist cachedPlaylist = playlist
if (mediaSession == null) return if (mediaSession == null) return
mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing)) mediaSession!!.setQueueTitle(applicationContext.getString(R.string.button_bar_now_playing))
mediaSession!!.setQueue(playlist.mapIndexed { id, song -> mediaSession!!.setQueue(
MediaSessionCompat.QueueItem( playlist.mapIndexed { id, song ->
Util.getMediaDescriptionForEntry(song), MediaSessionCompat.QueueItem(
id.toLong()) Util.getMediaDescriptionForEntry(song),
}) id.toLong()
)
}
)
} }
fun updateMediaSessionPlaybackPosition(playbackPosition: Long) { fun updateMediaSessionPlaybackPosition(playbackPosition: Long) {
@ -264,7 +278,7 @@ class MediaSessionHandler : KoinComponent {
// Playback position is updated too frequently in the player. // Playback position is updated too frequently in the player.
// This counter makes sure that the MediaSession is updated ~ at every second // This counter makes sure that the MediaSession is updated ~ at every second
playbackPositionDelayCount++ playbackPositionDelayCount++
if (playbackPositionDelayCount < 10) return if (playbackPositionDelayCount < CALL_DIVIDE) return
playbackPositionDelayCount = 0 playbackPositionDelayCount = 0
val playbackStateBuilder = PlaybackStateCompat.Builder() val playbackStateBuilder = PlaybackStateCompat.Builder()
@ -286,7 +300,10 @@ class MediaSessionHandler : KoinComponent {
} }
private fun registerMediaButtonEventReceiver() { 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) val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
mediaButtonIntent.component = component mediaButtonIntent.component = component
@ -303,4 +320,4 @@ class MediaSessionHandler : KoinComponent {
private fun unregisterMediaButtonEventReceiver() { private fun unregisterMediaButtonEventReceiver() {
mediaSession?.setMediaButtonReceiver(null) mediaSession?.setMediaButtonReceiver(null)
} }
} }

View File

@ -1,21 +1,10 @@
/* /*
This file is part of Subsonic. * Util.kt
* Copyright (C) 2009-2021 Ultrasonic developers
Subsonic is free software: you can redistribute it and/or modify *
it under the terms of the GNU General Public License as published by * Distributed under terms of the GNU GPLv3 license.
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
*/ */
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -51,16 +40,6 @@ import android.widget.Toast
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import androidx.preference.PreferenceManager 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.ByteArrayOutputStream
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
@ -72,17 +51,32 @@ import java.io.OutputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.* import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt 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 * Contains various utility functions
* @version $Id$
*/ */
@Suppress("TooManyFunctions", "LargeClass")
object Util { object Util {
private val GIGA_BYTE_FORMAT = DecimalFormat("0.00 GB") private val GIGA_BYTE_FORMAT = DecimalFormat("0.00 GB")
@ -171,17 +165,17 @@ object Util {
fun applyTheme(context: Context?) { fun applyTheme(context: Context?) {
val theme = getTheme() val theme = getTheme()
if (Constants.PREFERENCES_KEY_THEME_DARK.equals( if (Constants.PREFERENCES_KEY_THEME_DARK.equals(
theme, theme,
ignoreCase = true ignoreCase = true
) || "fullscreen".equals(theme, ignoreCase = true) ) || "fullscreen".equals(theme, ignoreCase = true)
) { ) {
context!!.setTheme(R.style.UltrasonicTheme) context!!.setTheme(R.style.UltrasonicTheme)
} else if (Constants.PREFERENCES_KEY_THEME_BLACK.equals(theme, ignoreCase = true)) { } else if (Constants.PREFERENCES_KEY_THEME_BLACK.equals(theme, ignoreCase = true)) {
context!!.setTheme(R.style.UltrasonicTheme_Black) context!!.setTheme(R.style.UltrasonicTheme_Black)
} else if (Constants.PREFERENCES_KEY_THEME_LIGHT.equals( } else if (Constants.PREFERENCES_KEY_THEME_LIGHT.equals(
theme, theme,
ignoreCase = true ignoreCase = true
) || "fullscreenlight".equals(theme, ignoreCase = true) ) || "fullscreenlight".equals(theme, ignoreCase = true)
) { ) {
context!!.setTheme(R.style.UltrasonicTheme_Light) context!!.setTheme(R.style.UltrasonicTheme_Light)
} }
@ -248,8 +242,9 @@ object Util {
} }
@Throws(IOException::class) @Throws(IOException::class)
@Suppress("MagicNumber")
fun copy(input: InputStream, output: OutputStream): Long { fun copy(input: InputStream, output: OutputStream): Long {
val buffer = ByteArray(1024 * 4) val buffer = ByteArray(KBYTE * 4)
var count: Long = 0 var count: Long = 0
var n: Int var n: Int
while (-1 != input.read(buffer).also { n = it }) { while (-1 != input.read(buffer).also { n = it }) {
@ -261,14 +256,16 @@ object Util {
@Throws(IOException::class) @Throws(IOException::class)
fun atomicCopy(from: File, to: File) { fun atomicCopy(from: File, to: File) {
val tmp = File(String.format("%s.tmp", to.path)) val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
val `in` = FileInputStream(from) val input = FileInputStream(from)
val out = FileOutputStream(tmp) val out = FileOutputStream(tmp)
try { try {
`in`.channel.transferTo(0, from.length(), out.channel) input.channel.transferTo(0, from.length(), out.channel)
out.close() out.close()
if (!tmp.renameTo(to)) { 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) Timber.i("Copied %s to %s", from, to)
} catch (x: IOException) { } catch (x: IOException) {
@ -276,7 +273,7 @@ object Util {
delete(to) delete(to)
throw x throw x
} finally { } finally {
close(`in`) close(input)
close(out) close(out)
delete(tmp) delete(tmp)
} }
@ -296,7 +293,7 @@ object Util {
fun close(closeable: Closeable?) { fun close(closeable: Closeable?) {
try { try {
closeable?.close() closeable?.close()
} catch (x: Throwable) { } catch (_: Throwable) {
// Ignored // Ignored
} }
} }
@ -376,18 +373,18 @@ object Util {
fun formatBytes(byteCount: Long): String { fun formatBytes(byteCount: Long): String {
// More than 1 GB? // More than 1 GB?
if (byteCount >= 1024 * 1024 * 1024) { if (byteCount >= KBYTE * KBYTE * KBYTE) {
return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (1024 * 1024 * 1024)) return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
} }
// More than 1 MB? // More than 1 MB?
if (byteCount >= 1024 * 1024) { if (byteCount >= KBYTE * KBYTE) {
return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (1024 * 1024)) return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE))
} }
// More than 1 KB? // More than 1 KB?
return if (byteCount >= 1024) { return if (byteCount >= KBYTE) {
KILO_BYTE_FORMAT.format(byteCount.toDouble() / 1024) KILO_BYTE_FORMAT.format(byteCount.toDouble() / KBYTE)
} else "$byteCount B" } else "$byteCount B"
} }
@ -406,35 +403,36 @@ object Util {
* @return The formatted string. * @return The formatted string.
*/ */
@Synchronized @Synchronized
@Suppress("ReturnCount")
fun formatLocalizedBytes(byteCount: Long, context: Context): String { fun formatLocalizedBytes(byteCount: Long, context: Context): String {
// More than 1 GB? // More than 1 GB?
if (byteCount >= 1024 * 1024 * 1024) { if (byteCount >= KBYTE * KBYTE * KBYTE) {
if (GIGA_BYTE_LOCALIZED_FORMAT == null) { if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
GIGA_BYTE_LOCALIZED_FORMAT = GIGA_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_gigabyte)) DecimalFormat(context.resources.getString(R.string.util_bytes_format_gigabyte))
} }
return GIGA_BYTE_LOCALIZED_FORMAT!! return GIGA_BYTE_LOCALIZED_FORMAT!!
.format(byteCount.toDouble() / (1024 * 1024 * 1024)) .format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
} }
// More than 1 MB? // More than 1 MB?
if (byteCount >= 1024 * 1024) { if (byteCount >= KBYTE * KBYTE) {
if (MEGA_BYTE_LOCALIZED_FORMAT == null) { if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
MEGA_BYTE_LOCALIZED_FORMAT = MEGA_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_megabyte)) DecimalFormat(context.resources.getString(R.string.util_bytes_format_megabyte))
} }
return MEGA_BYTE_LOCALIZED_FORMAT!! return MEGA_BYTE_LOCALIZED_FORMAT!!
.format(byteCount.toDouble() / (1024 * 1024)) .format(byteCount.toDouble() / (KBYTE * KBYTE))
} }
// More than 1 KB? // More than 1 KB?
if (byteCount >= 1024) { if (byteCount >= KBYTE) {
if (KILO_BYTE_LOCALIZED_FORMAT == null) { if (KILO_BYTE_LOCALIZED_FORMAT == null) {
KILO_BYTE_LOCALIZED_FORMAT = KILO_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_kilobyte)) 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) { if (BYTE_LOCALIZED_FORMAT == null) {
BYTE_LOCALIZED_FORMAT = BYTE_LOCALIZED_FORMAT =
@ -453,6 +451,7 @@ object Util {
* @param s The string to encode. * @param s The string to encode.
* @return The encoded string. * @return The encoded string.
*/ */
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
fun utf8HexEncode(s: String?): String? { fun utf8HexEncode(s: String?): String? {
if (s == null) { if (s == null) {
return null return null
@ -473,6 +472,7 @@ object Util {
* @param data Bytes to convert to hexadecimal characters. * @param data Bytes to convert to hexadecimal characters.
* @return A string containing hexadecimal characters. * @return A string containing hexadecimal characters.
*/ */
@Suppress("MagicNumber")
fun hexEncode(data: ByteArray): String { fun hexEncode(data: ByteArray): String {
val length = data.size val length = data.size
val out = CharArray(length shl 1) val out = CharArray(length shl 1)
@ -493,6 +493,7 @@ object Util {
* @return MD5 digest as a hex string. * @return MD5 digest as a hex string.
*/ */
@JvmStatic @JvmStatic
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
fun md5Hex(s: String?): String? { fun md5Hex(s: String?): String? {
return if (s == null) { return if (s == null) {
null null
@ -567,7 +568,11 @@ object Util {
.setIcon(icon) .setIcon(icon)
.setTitle(titleId) .setTitle(titleId)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.common_ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } .setPositiveButton(R.string.common_ok) {
dialog: DialogInterface,
_: Int ->
dialog.dismiss()
}
.show() .show()
} }
@ -746,6 +751,7 @@ object Util {
context.sendBroadcast(avrcpIntent) context.sendBroadcast(avrcpIntent)
} }
@Suppress("LongParameterList")
fun broadcastA2dpPlayStatusChange( fun broadcastA2dpPlayStatusChange(
context: Context, context: Context,
state: PlayerState?, state: PlayerState?,
@ -763,7 +769,7 @@ object Util {
return return
} }
// FIXME: This is probably a bug. // FIXME This is probably a bug.
if (currentSong !== currentSong) { if (currentSong !== currentSong) {
Util.currentSong = currentSong Util.currentSong = currentSong
} }
@ -797,11 +803,12 @@ object Util {
when (state) { when (state) {
PlayerState.STARTED -> avrcpIntent.putExtra("playing", true) PlayerState.STARTED -> avrcpIntent.putExtra("playing", true)
PlayerState.STOPPED, PlayerState.PAUSED, PlayerState.COMPLETED -> avrcpIntent.putExtra( PlayerState.STOPPED, PlayerState.PAUSED,
PlayerState.COMPLETED -> avrcpIntent.putExtra(
"playing", "playing",
false false
) )
else -> return // No need to broadcast. else -> return // No need to broadcast.
} }
context.sendBroadcast(avrcpIntent) context.sendBroadcast(avrcpIntent)
@ -819,12 +826,13 @@ object Util {
PlayerState.STOPPED -> intent.putExtra("state", "stop") PlayerState.STOPPED -> intent.putExtra("state", "stop")
PlayerState.PAUSED -> intent.putExtra("state", "pause") PlayerState.PAUSED -> intent.putExtra("state", "pause")
PlayerState.COMPLETED -> intent.putExtra("state", "complete") PlayerState.COMPLETED -> intent.putExtra("state", "complete")
else -> return // No need to broadcast. else -> return // No need to broadcast.
} }
context.sendBroadcast(intent) context.sendBroadcast(intent)
} }
@JvmStatic @JvmStatic
@Suppress("MagicNumber")
fun getNotificationImageSize(context: Context): Int { fun getNotificationImageSize(context: Context): Int {
val metrics = context.resources.displayMetrics val metrics = context.resources.displayMetrics
val imageSizeLarge = val imageSizeLarge =
@ -838,6 +846,7 @@ object Util {
} }
} }
@Suppress("MagicNumber")
fun getAlbumImageSize(context: Context?): Int { fun getAlbumImageSize(context: Context?): Int {
val metrics = context!!.resources.displayMetrics val metrics = context!!.resources.displayMetrics
val imageSizeLarge = val imageSizeLarge =
@ -1027,11 +1036,11 @@ object Util {
} }
val hours = TimeUnit.MILLISECONDS.toHours(millis) val hours = TimeUnit.MILLISECONDS.toHours(millis)
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours) val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours)
val seconds = val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(hours * 60 + minutes) TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
return when { return when {
hours >= 10 -> { hours >= DEGRADE_PRECISION_AFTER -> {
String.format( String.format(
Locale.getDefault(), Locale.getDefault(),
"%02d:%02d:%02d", "%02d:%02d:%02d",
@ -1043,7 +1052,7 @@ object Util {
hours > 0 -> { hours > 0 -> {
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds) String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
} }
minutes >= 10 -> { minutes >= DEGRADE_PRECISION_AFTER -> {
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
} }
minutes > 0 -> String.format( minutes > 0 -> String.format(
@ -1254,12 +1263,13 @@ object Util {
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri { fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse( return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE + ContentResolver.SCHEME_ANDROID_RESOURCE +
"://" + context.resources.getResourcePackageName(drawableId) "://" + context.resources.getResourcePackageName(drawableId) +
+ '/' + context.resources.getResourceTypeName(drawableId) '/' + context.resources.getResourceTypeName(drawableId) +
+ '/' + context.resources.getResourceEntryName(drawableId) '/' + context.resources.getResourceEntryName(drawableId)
) )
} }
@Suppress("ComplexMethod")
fun getMediaDescriptionForEntry( fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry, song: MusicDirectory.Entry,
mediaId: String? = null, mediaId: String? = null,
@ -1267,12 +1277,14 @@ object Util {
): MediaDescriptionCompat { ): MediaDescriptionCompat {
val descriptionBuilder = MediaDescriptionCompat.Builder() val descriptionBuilder = MediaDescriptionCompat.Builder()
val artist = StringBuilder(60) val artist = StringBuilder(LINE_LENGTH)
var bitRate: String? = null var bitRate: String? = null
val duration = song.duration val duration = song.duration
if (duration != null) { 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) if (song.bitRate != null && song.bitRate!! > 0)
@ -1286,16 +1298,21 @@ object Util {
fileFormat = if ( fileFormat = if (
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo 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 val artistName = song.artist
if (artistName != null) { if (artistName != null) {
if (shouldDisplayBitrateWithArtist() && (!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank())) { if (shouldDisplayBitrateWithArtist() && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
) {
artist.append(artistName).append(" (").append( artist.append(artistName).append(" (").append(
String.format( String.format(
appContext().getString(R.string.song_details_all), 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(')') ).append(')')
} else { } else {
@ -1305,9 +1322,9 @@ object Util {
val trackNumber = song.track ?: 0 val trackNumber = song.track ?: 0
val title = StringBuilder(60) val title = StringBuilder(LINE_LENGTH)
if (shouldShowTrackNumber() && trackNumber > 0) if (shouldShowTrackNumber() && trackNumber > 0)
title.append(String.format("%02d - ", trackNumber)) title.append(String.format(Locale.ROOT, "%02d - ", trackNumber))
title.append(song.title) title.append(song.title)
@ -1315,16 +1332,22 @@ object Util {
title.append(" (").append( title.append(" (").append(
String.format( String.format(
appContext().getString(R.string.song_details_all), 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(')') ).append(')')
} }
if (groupNameId != null) if (groupNameId != null)
descriptionBuilder.setExtras(Bundle().apply { putString( descriptionBuilder.setExtras(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, Bundle().apply {
appContext().getString(groupNameId) putString(
) }) MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
)
}
)
descriptionBuilder.setTitle(title) descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(artist) descriptionBuilder.setSubtitle(artist)
@ -1344,4 +1367,4 @@ object Util {
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode)) intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
return PendingIntent.getBroadcast(context, requestCode, intent, flags) return PendingIntent.getBroadcast(context, requestCode, intent, flags)
} }
} }

View File

@ -2,9 +2,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24">
android:tint="?attr/colorControlNormal">
<path <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"/> 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> </vector>

View File

@ -2,10 +2,8 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24">
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path <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"/> 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> </vector>