Support android auto (#47)
This commit is contained in:
parent
fae96d1114
commit
d0cffe466b
11 changed files with 420 additions and 1 deletions
|
@ -9,7 +9,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId = "it.vfsfitvnm.vimusic"
|
||||
minSdk = 21
|
||||
minSdk = 23
|
||||
targetSdk = 32
|
||||
versionCode = 16
|
||||
versionName = "0.5.0"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="32" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -93,8 +95,22 @@
|
|||
android:foregroundServiceType="mediaPlayback">
|
||||
</service>
|
||||
|
||||
<service android:name=".service.PlayerMediaBrowserService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".service.PlayerService$NotificationDismissReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@drawable/app_icon" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -127,6 +127,10 @@ interface Database {
|
|||
@Query("SELECT * FROM Song WHERE id = :id")
|
||||
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")
|
||||
fun likedAt(songId: String): Flow<Long?>
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
package it.vfsfitvnm.vimusic.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.media.MediaDescription
|
||||
import android.media.browse.MediaBrowser.MediaItem
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Process
|
||||
import android.service.media.MediaBrowserService
|
||||
import it.vfsfitvnm.vimusic.BuildConfig
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class PlayerMediaBrowserService : MediaBrowserService() {
|
||||
|
||||
var playerServiceBinder: PlayerService.Binder? = null
|
||||
var isBound = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val intent = Intent(this, PlayerService::class.java)
|
||||
bindService(intent, playerConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onGetRoot(
|
||||
clientPackageName: String,
|
||||
clientUid: Int,
|
||||
rootHints: Bundle?
|
||||
): BrowserRoot? {
|
||||
if (!isCallerAllowed(clientPackageName, clientUid)) {
|
||||
return null
|
||||
}
|
||||
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
|
||||
isBound = true
|
||||
sessionToken = playerServiceBinder?.mediaSession?.sessionToken
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
isBound = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCallerAllowed(
|
||||
clientPackageName: String,
|
||||
clientUid: Int
|
||||
): Boolean {
|
||||
return when {
|
||||
clientUid == Process.myUid() -> true
|
||||
clientUid == Process.SYSTEM_UID -> true
|
||||
ANDROID_AUTO_PACKAGE_NAME == clientPackageName -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead"
|
||||
const val CONTENT_STYLE_BROWSABLE_HINT = "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"
|
||||
const val CONTENT_STYLE_LIST_ITEM_HINT_VALUE = 1
|
||||
const val MEDIA_ROOT_ID = "VIMUSIC_MEDIA_ROOT_ID"
|
||||
const val MEDIA_PLAYLISTS_ID = "VIMUSIC_MEDIA_PLAYLISTS_ID"
|
||||
const val MEDIA_FAVORITES_ID = "VIMUSIC_MEDIA_FAVORITES_ID"
|
||||
const val MEDIA_SONGS_ID = "VIMUSIC_MEDIA_SONGS_ID"
|
||||
}
|
||||
|
||||
}
|
|
@ -13,12 +13,14 @@ import android.content.SharedPreferences
|
|||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.media.MediaDescription
|
||||
import android.media.MediaMetadata
|
||||
import android.media.audiofx.AudioEffect
|
||||
import android.media.session.MediaSession
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -70,6 +72,7 @@ import it.vfsfitvnm.vimusic.models.Event
|
|||
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
||||
import it.vfsfitvnm.vimusic.utils.MediaIDHelper
|
||||
import it.vfsfitvnm.vimusic.utils.RingBuffer
|
||||
import it.vfsfitvnm.vimusic.utils.TimerJob
|
||||
import it.vfsfitvnm.vimusic.utils.YouTubeRadio
|
||||
|
@ -322,6 +325,20 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
} else if (mediaItem.mediaMetadata.artworkUri == bitmapProvider.lastUri) {
|
||||
bitmapProvider.listener?.invoke(bitmapProvider.lastBitmap)
|
||||
}
|
||||
|
||||
// On playlist changed, we refresh the mediaSession queue
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
|
||||
mediaSession.setQueue(player.currentTimeline.mediaItems.mapIndexed { index, it ->
|
||||
MediaSession.QueueItem(
|
||||
MediaDescription.Builder()
|
||||
.setMediaId(it.mediaId)
|
||||
.setTitle(it.mediaMetadata.title)
|
||||
.setSubtitle(it.mediaMetadata.artist)
|
||||
.setIconUri(it.mediaMetadata.artworkUri)
|
||||
.build(), index.toLong()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeRecoverPlaybackError() {
|
||||
|
@ -469,6 +486,10 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
MediaMetadata.METADATA_KEY_ALBUM,
|
||||
player.currentMediaItem?.mediaMetadata?.albumTitle
|
||||
)
|
||||
.putBitmap(
|
||||
MediaMetadata.METADATA_KEY_ALBUM_ART,
|
||||
if (isShowingThumbnailInLockscreen) bitmapProvider.bitmap else null
|
||||
)
|
||||
.putLong(MediaMetadata.METADATA_KEY_DURATION, player.duration)
|
||||
.build().let(mediaSession::setMetadata)
|
||||
}
|
||||
|
@ -534,6 +555,7 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun notification(): Notification? {
|
||||
if (player.currentMediaItem == null) return null
|
||||
|
||||
|
@ -758,6 +780,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
val cache: Cache
|
||||
get() = this@PlayerService.cache
|
||||
|
||||
val mediaSession: MediaSession
|
||||
get() = this@PlayerService.mediaSession
|
||||
|
||||
val sleepTimerMillisLeft: StateFlow<Long?>?
|
||||
get() = timerJob?.millisLeft
|
||||
|
||||
|
@ -837,6 +862,11 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
override fun onSkipToPrevious() = player.forceSeekToPrevious()
|
||||
override fun onSkipToNext() = player.forceSeekToNext()
|
||||
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() {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
12
app/src/main/res/drawable/disc_white.xml
Normal file
12
app/src/main/res/drawable/disc_white.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<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>
|
9
app/src/main/res/drawable/heart_white.xml
Normal file
9
app/src/main/res/drawable/heart_white.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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>
|
46
app/src/main/res/drawable/playlist_white.xml
Normal file
46
app/src/main/res/drawable/playlist_white.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<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>
|
3
app/src/main/res/xml/automotive_app_desc.xml
Normal file
3
app/src/main/res/xml/automotive_app_desc.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<automotiveApp>
|
||||
<uses name="media"/>
|
||||
</automotiveApp>
|
Loading…
Reference in a new issue