Complete android auto support (#47)
This commit is contained in:
parent
6fb8e41a04
commit
270986215c
|
@ -51,7 +51,6 @@ import it.vfsfitvnm.vimusic.models.SongArtistMap
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface Database {
|
interface Database {
|
||||||
|
@ -127,10 +126,6 @@ interface Database {
|
||||||
@Query("SELECT * FROM Song WHERE id = :id")
|
@Query("SELECT * FROM Song WHERE id = :id")
|
||||||
fun song(id: String): Flow<Song?>
|
fun song(id: String): Flow<Song?>
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM Song WHERE id = :id")
|
|
||||||
fun songById(id: String): Flow<DetailedSong?>
|
|
||||||
|
|
||||||
@Query("SELECT likedAt FROM Song WHERE id = :songId")
|
@Query("SELECT likedAt FROM Song WHERE id = :songId")
|
||||||
fun likedAt(songId: String): Flow<Long?>
|
fun likedAt(songId: String): Flow<Long?>
|
||||||
|
|
||||||
|
@ -238,28 +233,44 @@ interface Database {
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC")
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC")
|
||||||
fun playlistPreviewsByName(): Flow<List<PlaylistPreview>>
|
fun playlistPreviewsByNameAsc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC")
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC")
|
||||||
fun playlistPreviewsByDateAdded(): Flow<List<PlaylistPreview>>
|
fun playlistPreviewsByDateAddedAsc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC")
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC")
|
||||||
fun playlistPreviewsByDateSongCount(): Flow<List<PlaylistPreview>>
|
fun playlistPreviewsByDateSongCountAsc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name DESC")
|
||||||
|
fun playlistPreviewsByNameDesc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID DESC")
|
||||||
|
fun playlistPreviewsByDateAddedDesc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount DESC")
|
||||||
|
fun playlistPreviewsByDateSongCountDesc(): Flow<List<PlaylistPreview>>
|
||||||
|
|
||||||
fun playlistPreviews(
|
fun playlistPreviews(
|
||||||
sortBy: PlaylistSortBy,
|
sortBy: PlaylistSortBy,
|
||||||
sortOrder: SortOrder
|
sortOrder: SortOrder
|
||||||
): Flow<List<PlaylistPreview>> {
|
): Flow<List<PlaylistPreview>> {
|
||||||
return when (sortBy) {
|
return when (sortBy) {
|
||||||
PlaylistSortBy.Name -> playlistPreviewsByName()
|
PlaylistSortBy.Name -> when (sortOrder) {
|
||||||
PlaylistSortBy.DateAdded -> playlistPreviewsByDateAdded()
|
SortOrder.Ascending -> playlistPreviewsByNameAsc()
|
||||||
PlaylistSortBy.SongCount -> playlistPreviewsByDateSongCount()
|
SortOrder.Descending -> playlistPreviewsByNameDesc()
|
||||||
}.map {
|
}
|
||||||
when (sortOrder) {
|
PlaylistSortBy.SongCount -> when (sortOrder) {
|
||||||
SortOrder.Ascending -> it
|
SortOrder.Ascending -> playlistPreviewsByDateSongCountAsc()
|
||||||
SortOrder.Descending -> it.reversed()
|
SortOrder.Descending -> playlistPreviewsByDateSongCountDesc()
|
||||||
|
}
|
||||||
|
PlaylistSortBy.DateAdded -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> playlistPreviewsByDateAddedAsc()
|
||||||
|
SortOrder.Descending -> playlistPreviewsByDateAddedDesc()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.enums
|
|
||||||
|
|
||||||
enum class MediaIDType {
|
|
||||||
Playlist,
|
|
||||||
RandomFavorites,
|
|
||||||
RandomSongs,
|
|
||||||
Song;
|
|
||||||
|
|
||||||
val prefix: String
|
|
||||||
get() = when (this) {
|
|
||||||
Song -> "VIMUSIC_SONG_ID_"
|
|
||||||
Playlist -> "VIMUSIC_PLAYLIST_ID_"
|
|
||||||
RandomSongs -> "VIMUSIC_RANDOM_SONGS"
|
|
||||||
RandomFavorites -> "VIMUSIC_RANDOM_FAVORITES"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +1,307 @@
|
||||||
package it.vfsfitvnm.vimusic.service
|
package it.vfsfitvnm.vimusic.service
|
||||||
|
|
||||||
|
import android.media.MediaDescription as BrowserMediaDescription
|
||||||
|
import android.media.browse.MediaBrowser.MediaItem as BrowserMediaItem
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.media.MediaDescription
|
import android.media.session.MediaSession
|
||||||
import android.media.browse.MediaBrowser.MediaItem
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import android.service.media.MediaBrowserService
|
import android.service.media.MediaBrowserService
|
||||||
import it.vfsfitvnm.vimusic.BuildConfig
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.datasource.cache.Cache
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
||||||
|
import it.vfsfitvnm.vimusic.utils.intent
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class PlayerMediaBrowserService : MediaBrowserService() {
|
class PlayerMediaBrowserService : MediaBrowserService(), ServiceConnection {
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private var lastSongs = emptyList<DetailedSong>()
|
||||||
|
|
||||||
var playerServiceBinder: PlayerService.Binder? = null
|
private var bound = false
|
||||||
var isBound = false
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onDestroy() {
|
||||||
super.onCreate()
|
if (bound) {
|
||||||
val intent = Intent(this, PlayerService::class.java)
|
unbindService(this)
|
||||||
bindService(intent, playerConnection, Context.BIND_AUTO_CREATE)
|
}
|
||||||
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
if (service is PlayerService.Binder) {
|
||||||
|
bound = true
|
||||||
|
sessionToken = service.mediaSession.sessionToken
|
||||||
|
service.mediaSession.setCallback(SessionCallback(service.player, service.cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) = Unit
|
||||||
|
|
||||||
override fun onGetRoot(
|
override fun onGetRoot(
|
||||||
clientPackageName: String,
|
clientPackageName: String,
|
||||||
clientUid: Int,
|
clientUid: Int,
|
||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot? {
|
): BrowserRoot? {
|
||||||
if (!isCallerAllowed(clientPackageName, clientUid)) {
|
return if (clientUid == Process.myUid()
|
||||||
return null
|
|| clientUid == Process.SYSTEM_UID
|
||||||
}
|
|| clientPackageName == "com.google.android.projection.gearhead"
|
||||||
val extras = Bundle()
|
|
||||||
extras.putInt(CONTENT_STYLE_BROWSABLE_HINT, CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
|
|
||||||
return BrowserRoot(MEDIA_ROOT_ID, extras)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadChildren(
|
|
||||||
parentId: String,
|
|
||||||
result: Result<MutableList<MediaItem>>
|
|
||||||
) {
|
|
||||||
when (parentId) {
|
|
||||||
MEDIA_ROOT_ID -> result.sendResult(createMenuMediaItem())
|
|
||||||
MEDIA_PLAYLISTS_ID -> result.sendResult(createPlaylistsMediaItem())
|
|
||||||
MEDIA_FAVORITES_ID -> result.sendResult(createFavoritesMediaItem())
|
|
||||||
MEDIA_SONGS_ID -> result.sendResult(createSongsMediaItem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createFavoritesMediaItem(): MutableList<MediaItem> {
|
|
||||||
val favorites = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.favorites().first()
|
|
||||||
}.map { entry ->
|
|
||||||
MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MediaIDHelper.createMediaIdForSong(entry.id))
|
|
||||||
.setTitle(entry.title)
|
|
||||||
.setSubtitle(entry.artistsText)
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse(entry.thumbnailUrl)
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
}.toCollection(mutableListOf())
|
|
||||||
if (favorites.isNotEmpty()) {
|
|
||||||
favorites.add(
|
|
||||||
0, MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MediaIDHelper.createMediaIdForRandomFavorites())
|
|
||||||
.setTitle("Play all random")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/shuffle")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return favorites
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSongsMediaItem(): MutableList<MediaItem> {
|
|
||||||
val songs = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.songs(SongSortBy.DateAdded, SortOrder.Descending).first()
|
|
||||||
}.map { entry ->
|
|
||||||
MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MediaIDHelper.createMediaIdForSong(entry.id))
|
|
||||||
.setTitle(entry.title)
|
|
||||||
.setSubtitle(entry.artistsText)
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse(entry.thumbnailUrl)
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
}.toCollection(mutableListOf())
|
|
||||||
if (songs.isNotEmpty()) {
|
|
||||||
songs.add(
|
|
||||||
0, MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MediaIDHelper.createMediaIdForRandomSongs())
|
|
||||||
.setTitle("Play all random")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/shuffle")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return songs
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPlaylistsMediaItem(): MutableList<MediaItem> {
|
|
||||||
return runBlocking(Dispatchers.IO) {
|
|
||||||
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending).first()
|
|
||||||
}.map { entry ->
|
|
||||||
MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MediaIDHelper.createMediaIdForPlaylist(entry.playlist.id))
|
|
||||||
.setTitle(entry.playlist.name)
|
|
||||||
.setSubtitle("${entry.songCount} songs")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/playlist")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_PLAYABLE
|
|
||||||
)
|
|
||||||
}.toCollection(mutableListOf())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMenuMediaItem(): MutableList<MediaItem> {
|
|
||||||
return mutableListOf(
|
|
||||||
MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MEDIA_PLAYLISTS_ID)
|
|
||||||
.setTitle("Playlists")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/playlist_white")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_BROWSABLE
|
|
||||||
), MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MEDIA_FAVORITES_ID)
|
|
||||||
.setTitle("Favorites")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/heart_white")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_BROWSABLE
|
|
||||||
), MediaItem(
|
|
||||||
MediaDescription.Builder()
|
|
||||||
.setMediaId(MEDIA_SONGS_ID)
|
|
||||||
.setTitle("Songs")
|
|
||||||
.setIconUri(
|
|
||||||
Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/drawable/disc_white")
|
|
||||||
)
|
|
||||||
.build(), MediaItem.FLAG_BROWSABLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val playerConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(
|
|
||||||
className: ComponentName,
|
|
||||||
service: IBinder
|
|
||||||
) {
|
) {
|
||||||
playerServiceBinder = service as PlayerService.Binder
|
bindService(intent<PlayerService>(), this, Context.BIND_AUTO_CREATE)
|
||||||
isBound = true
|
BrowserRoot(
|
||||||
sessionToken = playerServiceBinder?.mediaSession?.sessionToken
|
MediaId.root,
|
||||||
}
|
bundleOf("android.media.browse.CONTENT_STYLE_BROWSABLE_HINT" to 1)
|
||||||
|
)
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
} else {
|
||||||
isBound = false
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isCallerAllowed(
|
override fun onLoadChildren(parentId: String, result: Result<MutableList<BrowserMediaItem>>) {
|
||||||
clientPackageName: String,
|
runBlocking(Dispatchers.IO) {
|
||||||
clientUid: Int
|
result.sendResult(
|
||||||
): Boolean {
|
when (parentId) {
|
||||||
return when {
|
MediaId.root -> mutableListOf(
|
||||||
clientUid == Process.myUid() -> true
|
songsBrowserMediaItem,
|
||||||
clientUid == Process.SYSTEM_UID -> true
|
playlistsBrowserMediaItem,
|
||||||
ANDROID_AUTO_PACKAGE_NAME == clientPackageName -> true
|
albumsBrowserMediaItem
|
||||||
else -> false
|
)
|
||||||
|
|
||||||
|
MediaId.songs -> Database
|
||||||
|
.songsByPlayTimeDesc()
|
||||||
|
.first()
|
||||||
|
.take(30)
|
||||||
|
.also { lastSongs = it }
|
||||||
|
.map { it.asBrowserMediaItem }
|
||||||
|
.toMutableList()
|
||||||
|
.apply {
|
||||||
|
if (isNotEmpty()) add(0, shuffleBrowserMediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaId.playlists -> Database
|
||||||
|
.playlistPreviewsByDateAddedDesc()
|
||||||
|
.first()
|
||||||
|
.map { it.asBrowserMediaItem }
|
||||||
|
.toMutableList()
|
||||||
|
.apply {
|
||||||
|
add(0, favoritesBrowserMediaItem)
|
||||||
|
add(1, offlineBrowserMediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaId.albums -> Database
|
||||||
|
.albumsByRowIdDesc()
|
||||||
|
.first()
|
||||||
|
.map { it.asBrowserMediaItem }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
else -> mutableListOf()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun uriFor(@DrawableRes id: Int) = Uri.Builder()
|
||||||
const val ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"
|
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||||
const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"
|
.authority(resources.getResourcePackageName(id))
|
||||||
const val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1
|
.appendPath(resources.getResourceTypeName(id))
|
||||||
const val MEDIA_ROOT_ID = "VIMUSIC_MEDIA_ROOT_ID"
|
.appendPath(resources.getResourceEntryName(id))
|
||||||
const val MEDIA_PLAYLISTS_ID = "VIMUSIC_MEDIA_PLAYLISTS_ID"
|
.build()
|
||||||
const val MEDIA_FAVORITES_ID = "VIMUSIC_MEDIA_FAVORITES_ID"
|
|
||||||
const val MEDIA_SONGS_ID = "VIMUSIC_MEDIA_SONGS_ID"
|
private val shuffleBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.shuffle)
|
||||||
|
.setTitle("Shuffle")
|
||||||
|
.setIconUri(uriFor(R.drawable.shuffle))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val songsBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.songs)
|
||||||
|
.setTitle("Songs")
|
||||||
|
.setIconUri(uriFor(R.drawable.musical_notes))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private val playlistsBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.playlists)
|
||||||
|
.setTitle("Playlists")
|
||||||
|
.setIconUri(uriFor(R.drawable.playlist))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val albumsBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.albums)
|
||||||
|
.setTitle("Albums")
|
||||||
|
.setIconUri(uriFor(R.drawable.disc))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val favoritesBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.favorites)
|
||||||
|
.setTitle("Favorites")
|
||||||
|
.setIconUri(uriFor(R.drawable.heart))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val offlineBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.offline)
|
||||||
|
.setTitle("Offline")
|
||||||
|
.setIconUri(uriFor(R.drawable.airplane))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DetailedSong.asBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.forSong(id))
|
||||||
|
.setTitle(title)
|
||||||
|
.setSubtitle(artistsText)
|
||||||
|
.setIconUri(thumbnailUrl?.toUri())
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val PlaylistPreview.asBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.forPlaylist(playlist.id))
|
||||||
|
.setTitle(playlist.name)
|
||||||
|
.setSubtitle("$songCount songs")
|
||||||
|
.setIconUri(uriFor(R.drawable.playlist))
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val Album.asBrowserMediaItem
|
||||||
|
inline get() = BrowserMediaItem(
|
||||||
|
BrowserMediaDescription.Builder()
|
||||||
|
.setMediaId(MediaId.forAlbum(id))
|
||||||
|
.setTitle(title)
|
||||||
|
.setSubtitle(authorsText)
|
||||||
|
.setIconUri(thumbnailUrl?.toUri())
|
||||||
|
.build(),
|
||||||
|
BrowserMediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private inner class SessionCallback(private val player: Player, private val cache: Cache) :
|
||||||
|
MediaSession.Callback() {
|
||||||
|
override fun onPlay() = player.play()
|
||||||
|
override fun onPause() = player.pause()
|
||||||
|
override fun onSkipToPrevious() = player.forceSeekToPrevious()
|
||||||
|
override fun onSkipToNext() = player.forceSeekToNext()
|
||||||
|
override fun onSeekTo(pos: Long) = player.seekTo(pos)
|
||||||
|
override fun onSkipToQueueItem(id: Long) = player.seekToDefaultPosition(id.toInt())
|
||||||
|
|
||||||
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
|
val data = mediaId?.split('/') ?: return
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
val mediaItems = when (data.getOrNull(0)) {
|
||||||
|
MediaId.shuffle -> lastSongs
|
||||||
|
|
||||||
|
MediaId.songs -> data
|
||||||
|
.getOrNull(1)
|
||||||
|
?.let { songId ->
|
||||||
|
val index = lastSongs.indexOfFirst { it.id == songId }
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
val mediaItems = lastSongs.map(DetailedSong::asMediaItem)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
player.forcePlayAtIndex(mediaItems, index)
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyList()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
MediaId.favorites -> Database
|
||||||
|
.favorites()
|
||||||
|
.first()
|
||||||
|
|
||||||
|
MediaId.offline -> Database
|
||||||
|
.songsWithContentLength()
|
||||||
|
.first()
|
||||||
|
.filter { song ->
|
||||||
|
song.contentLength?.let {
|
||||||
|
cache.isCached(song.id, 0, song.contentLength)
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaId.playlists -> data
|
||||||
|
.getOrNull(1)
|
||||||
|
?.toLongOrNull()
|
||||||
|
?.let { playlistId ->
|
||||||
|
Database.playlistWithSongs(playlistId).first()?.songs
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
MediaId.albums -> data
|
||||||
|
.getOrNull(1)
|
||||||
|
?.let { albumId ->
|
||||||
|
Database.albumSongs(albumId).first()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
else -> emptyList()
|
||||||
|
}.map(DetailedSong::asMediaItem).shuffled()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
player.forcePlayFromBeginning(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object MediaId {
|
||||||
|
const val root = "root"
|
||||||
|
const val songs = "songs"
|
||||||
|
const val playlists = "playlists"
|
||||||
|
const val albums = "albums"
|
||||||
|
|
||||||
|
const val favorites = "favorites"
|
||||||
|
const val offline = "offline"
|
||||||
|
const val shuffle = "shuffle"
|
||||||
|
|
||||||
|
fun forSong(id: String) = "songs/$id"
|
||||||
|
fun forPlaylist(id: Long) = "playlists/$id"
|
||||||
|
fun forAlbum(id: String) = "albums/$id"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -13,14 +13,12 @@ import android.content.SharedPreferences
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.media.MediaDescription
|
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
import android.media.session.MediaSession
|
import android.media.session.MediaSession
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -72,7 +70,6 @@ import it.vfsfitvnm.vimusic.models.Event
|
||||||
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
||||||
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
|
|
||||||
import it.vfsfitvnm.vimusic.utils.RingBuffer
|
import it.vfsfitvnm.vimusic.utils.RingBuffer
|
||||||
import it.vfsfitvnm.vimusic.utils.TimerJob
|
import it.vfsfitvnm.vimusic.utils.TimerJob
|
||||||
import it.vfsfitvnm.vimusic.utils.YouTubeRadio
|
import it.vfsfitvnm.vimusic.utils.YouTubeRadio
|
||||||
|
@ -327,18 +324,18 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
}
|
}
|
||||||
|
|
||||||
// On playlist changed, we refresh the mediaSession queue
|
// On playlist changed, we refresh the mediaSession queue
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
|
// if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
|
||||||
mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
|
// mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
|
||||||
MediaSession.QueueItem(
|
// MediaSession.QueueItem(
|
||||||
MediaDescription.Builder()
|
// MediaDescription.Builder()
|
||||||
.setMediaId(it.mediaId)
|
// .setMediaId(it.mediaId)
|
||||||
.setTitle(it.mediaMetadata.title)
|
// .setTitle(it.mediaMetadata.title)
|
||||||
.setSubtitle(it.mediaMetadata.artist)
|
// .setSubtitle(it.mediaMetadata.artist)
|
||||||
.setIconUri(it.mediaMetadata.artworkUri)
|
// .setIconUri(it.mediaMetadata.artworkUri)
|
||||||
.build(), index.toLong()
|
// .build(), index.toLong()
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun maybeRecoverPlaybackError() {
|
private fun maybeRecoverPlaybackError() {
|
||||||
|
@ -486,10 +483,10 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
MediaMetadata.METADATA_KEY_ALBUM,
|
MediaMetadata.METADATA_KEY_ALBUM,
|
||||||
player.currentMediaItem?.mediaMetadata?.albumTitle
|
player.currentMediaItem?.mediaMetadata?.albumTitle
|
||||||
)
|
)
|
||||||
.putBitmap(
|
// .putBitmap(
|
||||||
MediaMetadata.METADATA_KEY_ALBUM_ART,
|
// MediaMetadata.METADATA_KEY_ALBUM_ART,
|
||||||
if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
|
// if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
|
||||||
)
|
// )
|
||||||
.putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
|
.putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
|
||||||
.build().let(mediaSession::setMetadata)
|
.build().let(mediaSession::setMetadata)
|
||||||
}
|
}
|
||||||
|
@ -555,7 +552,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun notification(): Notification? {
|
override fun notification(): Notification? {
|
||||||
if (player.currentMediaItem == null) return null
|
if (player.currentMediaItem == null) return null
|
||||||
|
|
||||||
|
@ -780,7 +776,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
val cache: Cache
|
val cache: Cache
|
||||||
get() = this@PlayerService.cache
|
get() = this@PlayerService.cache
|
||||||
|
|
||||||
val mediaSession: MediaSession
|
val mediaSession
|
||||||
get() = this@PlayerService.mediaSession
|
get() = this@PlayerService.mediaSession
|
||||||
|
|
||||||
val sleepTimerMillisLeft: StateFlow<Long?>?
|
val sleepTimerMillisLeft: StateFlow<Long?>?
|
||||||
|
@ -862,11 +858,6 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
override fun onSkipToPrevious() = player.forceSeekToPrevious()
|
override fun onSkipToPrevious() = player.forceSeekToPrevious()
|
||||||
override fun onSkipToNext() = player.forceSeekToNext()
|
override fun onSkipToNext() = player.forceSeekToNext()
|
||||||
override fun onSeekTo(pos: Long) = player.seekTo(pos)
|
override fun onSeekTo(pos: Long) = player.seekTo(pos)
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
|
||||||
player.forcePlayFromBeginning(MediaIDHelper.extractMusicQueueFromMediaId(mediaId))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSkipToQueueItem(id: Long) = player.seekToDefaultPosition(id.toInt())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NotificationActionReceiver(private val player: Player) : BroadcastReceiver() {
|
private class NotificationActionReceiver(private val player: Player) : BroadcastReceiver() {
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.utils
|
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.enums.MediaIDType
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
class MediaIDHelper {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun createMediaIdForSong(id: String): String {
|
|
||||||
return MediaIDType.Song.prefix.plus(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createMediaIdForPlaylist(id: Long): String {
|
|
||||||
return MediaIDType.Playlist.prefix.plus(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createMediaIdForRandomFavorites(): String {
|
|
||||||
return MediaIDType.RandomFavorites.prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createMediaIdForRandomSongs(): String {
|
|
||||||
return MediaIDType.RandomSongs.prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extractMusicQueueFromMediaId(mediaID: String?): List<MediaItem> {
|
|
||||||
val result = mutableListOf<MediaItem>()
|
|
||||||
mediaID?.apply {
|
|
||||||
with(mediaID) {
|
|
||||||
when {
|
|
||||||
startsWith(MediaIDType.Song.prefix) -> {
|
|
||||||
val id = mediaID.removePrefix(MediaIDType.Song.prefix)
|
|
||||||
val song = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.songById(id).first()
|
|
||||||
}
|
|
||||||
song?.apply {
|
|
||||||
result.add(song.asMediaItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startsWith(MediaIDType.Playlist.prefix) -> {
|
|
||||||
val id = mediaID.removePrefix(MediaIDType.Playlist.prefix).toLong()
|
|
||||||
val playlist = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.playlistWithSongs(id).first()
|
|
||||||
}
|
|
||||||
playlist?.apply {
|
|
||||||
if (playlist.songs.isNotEmpty()) {
|
|
||||||
playlist.songs.map { it.asMediaItem }.forEach(result::add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startsWith(MediaIDType.RandomFavorites.prefix) -> {
|
|
||||||
val favorites = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.favorites().first()
|
|
||||||
}
|
|
||||||
favorites.map { it.asMediaItem }.forEach(result::add)
|
|
||||||
result.shuffle()
|
|
||||||
}
|
|
||||||
startsWith(MediaIDType.RandomSongs.prefix) -> {
|
|
||||||
val favorites = runBlocking(Dispatchers.IO) {
|
|
||||||
Database.songs(SongSortBy.DateAdded, SortOrder.Descending).first()
|
|
||||||
}
|
|
||||||
favorites.map { it.asMediaItem }.forEach(result::add)
|
|
||||||
result.shuffle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="512">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M186.62,464H160a16,16 0,0 1,-14.57 -22.6l64.46,-142.25L113.1,297 77.8,339.77C71.07,348.23 65.7,352 52,352H34.08a17.66,17.66 0,0 1,-14.7 -7.06c-2.38,-3.21 -4.72,-8.65 -2.44,-16.41l19.82,-71c0.15,-0.53 0.33,-1.06 0.53,-1.58a0.38,0.38 0,0 0,0 -0.15,14.82 14.82,0 0,1 -0.53,-1.59L16.92,182.76c-2.15,-7.61 0.2,-12.93 2.56,-16.06a16.83,16.83 0,0 1,13.6 -6.7H52c10.23,0 20.16,4.59 26,12l34.57,42.05 97.32,-1.44 -64.44,-142A16,16 0,0 1,160 48h26.91a25,25 0,0 1,19.35 9.8l125.05,152 57.77,-1.52c4.23,-0.23 15.95,-0.31 18.66,-0.31C463,208 496,225.94 496,256c0,9.46 -3.78,27 -29.07,38.16 -14.93,6.6 -34.85,9.94 -59.21,9.94 -2.68,0 -14.37,-0.08 -18.66,-0.31l-57.76,-1.54 -125.36,152A25,25 0,0 1,186.62 464Z"/>
|
android:pathData="M186.62,464H160a16,16 0,0 1,-14.57 -22.6l64.46,-142.25L113.1,297 77.8,339.77C71.07,348.23 65.7,352 52,352H34.08a17.66,17.66 0,0 1,-14.7 -7.06c-2.38,-3.21 -4.72,-8.65 -2.44,-16.41l19.82,-71c0.15,-0.53 0.33,-1.06 0.53,-1.58a0.38,0.38 0,0 0,0 -0.15,14.82 14.82,0 0,1 -0.53,-1.59L16.92,182.76c-2.15,-7.61 0.2,-12.93 2.56,-16.06a16.83,16.83 0,0 1,13.6 -6.7H52c10.23,0 20.16,4.59 26,12l34.57,42.05 97.32,-1.44 -64.44,-142A16,16 0,0 1,160 48h26.91a25,25 0,0 1,19.35 9.8l125.05,152 57.77,-1.52c4.23,-0.23 15.95,-0.31 18.66,-0.31C463,208 496,225.94 496,256c0,9.46 -3.78,27 -29.07,38.16 -14.93,6.6 -34.85,9.94 -59.21,9.94 -2.68,0 -14.37,-0.08 -18.66,-0.31l-57.76,-1.54 -125.36,152A25,25 0,0 1,186.62 464Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="512">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M256,176a80,80 0,1 0,80 80A80.09,80.09 0,0 0,256 176ZM256,288a32,32 0,1 1,32 -32A32,32 0,0 1,256 288Z"/>
|
android:pathData="M256,176a80,80 0,1 0,80 80A80.09,80.09 0,0 0,256 176ZM256,288a32,32 0,1 1,32 -32A32,32 0,0 1,256 288Z"/>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM256,368A112,112 0,1 1,368 256,112.12 112.12,0 0,1 256,368Z"/>
|
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM256,368A112,112 0,1 1,368 256,112.12 112.12,0 0,1 256,368Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="512"
|
|
||||||
android:viewportHeight="512">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M256,176a80,80 0,1 0,80 80A80.09,80.09 0,0 0,256 176ZM256,288a32,32 0,1 1,32 -32A32,32 0,0 1,256 288Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M414.39,97.61A224,224 0,1 0,97.61 414.39,224 224,0 1,0 414.39,97.61ZM256,368A112,112 0,1 1,368 256,112.12 112.12,0 0,1 256,368Z"/>
|
|
||||||
</vector>
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="512">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M256,448a32,32 0,0 1,-18 -5.57c-78.59,-53.35 -112.62,-89.93 -131.39,-112.8 -40,-48.75 -59.15,-98.8 -58.61,-153C48.63,114.52 98.46,64 159.08,64c44.08,0 74.61,24.83 92.39,45.51a6,6 0,0 0,9.06 0C278.31,88.81 308.84,64 352.92,64 413.54,64 463.37,114.52 464,176.64c0.54,54.21 -18.63,104.26 -58.61,153 -18.77,22.87 -52.8,59.45 -131.39,112.8A32,32 0,0 1,256 448Z"/>
|
android:pathData="M256,448a32,32 0,0 1,-18 -5.57c-78.59,-53.35 -112.62,-89.93 -131.39,-112.8 -40,-48.75 -59.15,-98.8 -58.61,-153C48.63,114.52 98.46,64 159.08,64c44.08,0 74.61,24.83 92.39,45.51a6,6 0,0 0,9.06 0C278.31,88.81 308.84,64 352.92,64 413.54,64 463.37,114.52 464,176.64c0.54,54.21 -18.63,104.26 -58.61,153 -18.77,22.87 -52.8,59.45 -131.39,112.8A32,32 0,0 1,256 448Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="512"
|
|
||||||
android:viewportHeight="512">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M256,448a32,32 0,0 1,-18 -5.57c-78.59,-53.35 -112.62,-89.93 -131.39,-112.8 -40,-48.75 -59.15,-98.8 -58.61,-153C48.63,114.52 98.46,64 159.08,64c44.08,0 74.61,24.83 92.39,45.51a6,6 0,0 0,9.06 0C278.31,88.81 308.84,64 352.92,64 413.54,64 463.37,114.52 464,176.64c0.54,54.21 -18.63,104.26 -58.61,153 -18.77,22.87 -52.8,59.45 -131.39,112.8A32,32 0,0 1,256 448Z"/>
|
|
||||||
</vector>
|
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="512">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFFFF"
|
||||||
android:pathData="M421.84,37.37a25.86,25.86 0,0 0,-22.6 -4.46L199.92,86.49A32.3,32.3 0,0 0,176 118v226c0,6.74 -4.36,12.56 -11.11,14.83l-0.12,0.05 -52,18C92.88,383.53 80,402 80,423.91a55.54,55.54 0,0 0,23.23 45.63A54.78,54.78 0,0 0,135.34 480a55.82,55.82 0,0 0,17.75 -2.93l0.38,-0.13L175.31,469A47.84,47.84 0,0 0,208 423.91v-212c0,-7.29 4.77,-13.21 12.16,-15.07l0.21,-0.06L395,150.14a4,4 0,0 1,5 3.86V295.93c0,6.75 -4.25,12.38 -11.11,14.68l-0.25,0.09 -50.89,18.11A49.09,49.09 0,0 0,304 375.92a55.67,55.67 0,0 0,23.23 45.8,54.63 54.63,0 0,0 49.88,7.35l0.36,-0.12L399.31,421A47.83,47.83 0,0 0,432 375.92V58A25.74,25.74 0,0 0,421.84 37.37Z"/>
|
android:pathData="M421.84,37.37a25.86,25.86 0,0 0,-22.6 -4.46L199.92,86.49A32.3,32.3 0,0 0,176 118v226c0,6.74 -4.36,12.56 -11.11,14.83l-0.12,0.05 -52,18C92.88,383.53 80,402 80,423.91a55.54,55.54 0,0 0,23.23 45.63A54.78,54.78 0,0 0,135.34 480a55.82,55.82 0,0 0,17.75 -2.93l0.38,-0.13L175.31,469A47.84,47.84 0,0 0,208 423.91v-212c0,-7.29 4.77,-13.21 12.16,-15.07l0.21,-0.06L395,150.14a4,4 0,0 1,5 3.86V295.93c0,6.75 -4.25,12.38 -11.11,14.68l-0.25,0.09 -50.89,18.11A49.09,49.09 0,0 0,304 375.92a55.67,55.67 0,0 0,23.23 45.8,54.63 54.63,0 0,0 49.88,7.35l0.36,-0.12L399.31,421A47.83,47.83 0,0 0,432 375.92V58A25.74,25.74 0,0 0,421.84 37.37Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -8,39 +8,39 @@
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="48"
|
android:strokeWidth="48"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#000000"
|
android:strokeColor="#FFFFFF"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="32"
|
android:strokeWidth="32"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#000000"
|
android:strokeColor="#FFFFFF"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="32"
|
android:strokeWidth="32"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#000000"
|
android:strokeColor="#FFFFFF"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:strokeWidth="32"
|
android:strokeWidth="32"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#000000"
|
android:strokeColor="#FFFFFF"
|
||||||
android:strokeLineCap="round"/>
|
android:strokeLineCap="round"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M160.98,312L218.65,312A24,24 0,0 1,242.65 336L242.65,336A24,24 0,0 1,218.65 360L160.98,360A24,24 0,0 1,136.98 336L136.98,336A24,24 0,0 1,160.98 312z"
|
android:pathData="M160.98,312L218.65,312A24,24 0,0 1,242.65 336L242.65,336A24,24 0,0 1,218.65 360L160.98,360A24,24 0,0 1,136.98 336L136.98,336A24,24 0,0 1,160.98 312z"
|
||||||
android:strokeWidth="8.97186"
|
android:strokeWidth="8.97186"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="#FFFFFF"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M160.98,200L300.29,200A24,24 0,0 1,324.29 224L324.29,224A24,24 0,0 1,300.29 248L160.98,248A24,24 0,0 1,136.98 224L136.98,224A24,24 0,0 1,160.98 200z"
|
android:pathData="M160.98,200L300.29,200A24,24 0,0 1,324.29 224L324.29,224A24,24 0,0 1,300.29 248L160.98,248A24,24 0,0 1,136.98 224L136.98,224A24,24 0,0 1,160.98 200z"
|
||||||
android:strokeWidth="11.9451"
|
android:strokeWidth="11.9451"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="#FFFFFF"/>
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="m341.62,486a36.22,36.22 0,0 1,-21.24 -6.92,37.17 37.17,0 0,1 -15.4,-30.1 32.98,32.98 0,0 1,22.4 -31.32l33.38,-11.23a10.66,10.66 0,0 0,7.22 -10.17V231.37a21.07,21.07 0,0 1,15.81 -20.44l71.13,-18.47a14.44,14.44 0,0 1,18.06 13.97v37.9a21.06,21.06 0,0 1,-15.88 20.47l-60.15,15.18a10.66,10.66 0,0 0,-7.97 10.4v158.87a31.64,31.64 0,0 1,-21.51 30.06l-14.09,4.74a36.75,36.75 0,0 1,-11.76 1.94z"
|
android:pathData="m341.62,486a36.22,36.22 0,0 1,-21.24 -6.92,37.17 37.17,0 0,1 -15.4,-30.1 32.98,32.98 0,0 1,22.4 -31.32l33.38,-11.23a10.66,10.66 0,0 0,7.22 -10.17V231.37a21.07,21.07 0,0 1,15.81 -20.44l71.13,-18.47a14.44,14.44 0,0 1,18.06 13.97v37.9a21.06,21.06 0,0 1,-15.88 20.47l-60.15,15.18a10.66,10.66 0,0 0,-7.97 10.4v158.87a31.64,31.64 0,0 1,-21.51 30.06l-14.09,4.74a36.75,36.75 0,0 1,-11.76 1.94z"
|
||||||
android:strokeWidth="0.656249"/>
|
android:strokeWidth="0.656249"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="512"
|
|
||||||
android:viewportHeight="512">
|
|
||||||
<path
|
|
||||||
android:pathData="M160.98,112L448.98,112"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="48"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M80.98,112m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="32"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M80.98,224m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="32"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M80.98,336m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="32"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#FFFFFF"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M160.98,312L218.65,312A24,24 0,0 1,242.65 336L242.65,336A24,24 0,0 1,218.65 360L160.98,360A24,24 0,0 1,136.98 336L136.98,336A24,24 0,0 1,160.98 312z"
|
|
||||||
android:strokeWidth="8.97186"
|
|
||||||
android:fillColor="#FFFFFF"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M160.98,200L300.29,200A24,24 0,0 1,324.29 224L324.29,224A24,24 0,0 1,300.29 248L160.98,248A24,24 0,0 1,136.98 224L136.98,224A24,24 0,0 1,160.98 200z"
|
|
||||||
android:strokeWidth="11.9451"
|
|
||||||
android:fillColor="#FFFFFF"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="m341.62,486a36.22,36.22 0,0 1,-21.24 -6.92,37.17 37.17,0 0,1 -15.4,-30.1 32.98,32.98 0,0 1,22.4 -31.32l33.38,-11.23a10.66,10.66 0,0 0,7.22 -10.17V231.37a21.07,21.07 0,0 1,15.81 -20.44l71.13,-18.47a14.44,14.44 0,0 1,18.06 13.97v37.9a21.06,21.06 0,0 1,-15.88 20.47l-60.15,15.18a10.66,10.66 0,0 0,-7.97 10.4v158.87a31.64,31.64 0,0 1,-21.51 30.06l-14.09,4.74a36.75,36.75 0,0 1,-11.76 1.94z"
|
|
||||||
android:strokeWidth="0.656249"/>
|
|
||||||
</vector>
|
|
Loading…
Reference in a new issue