parent
cf05d3c781
commit
982639d2c7
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue