Initial Test of Android Auto

This commit is contained in:
James Wells 2021-05-28 20:30:36 -04:00
parent bf013c02d6
commit e666498f13
No known key found for this signature in database
GPG Key ID: DB1528F6EED16127
4 changed files with 166 additions and 14 deletions

View File

@ -3,6 +3,8 @@
package="org.moire.ultrasonic" package="org.moire.ultrasonic"
android:installLocation="auto"> android:installLocation="auto">
<uses-sdk android:minSdkVersion="20" android:targetSdkVersion="29" />
<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"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
@ -27,6 +29,14 @@
android:name=".app.UApp" android:name=".app.UApp"
android:label="@string/common.appname" android:label="@string/common.appname"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<!--Used by Android Auto-->
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@mipmap/ic_launcher" />
<activity android:name=".activity.NavigationActivity" <activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname" android:label="@string/common.appname"
@ -48,7 +58,11 @@
<service <service
android:name=".service.MediaPlayerService" android:name=".service.MediaPlayerService"
android:label="Ultrasonic Media Player Service" android:label="Ultrasonic Media Player Service"
android:exported="false"> android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service> </service>
<receiver android:name=".receiver.MediaButtonIntentReceiver"> <receiver android:name=".receiver.MediaButtonIntentReceiver">

View File

@ -125,6 +125,10 @@ class LocalMediaPlayer(
} }
fun release() { fun release() {
// Calling reset() will result in changing this player's state. If we allow
// the onPlayerStateChanged callback, then the state change will cause this
// to resurrect the media session which has just been destroyed.
onPlayerStateChanged = null
reset() reset()
try { try {
val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION) val i = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)

View File

@ -11,18 +11,27 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import android.view.KeyEvent import android.view.KeyEvent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.collections.ArrayList
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
@ -40,7 +49,6 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.NowPlayingEventDistributor import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.ShufflePlayBuffer import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.SimpleServiceBinder
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -49,8 +57,7 @@ import timber.log.Timber
* while the rest of the Ultrasonic App is in the background. * while the rest of the Ultrasonic App is in the background.
*/ */
@Suppress("LargeClass") @Suppress("LargeClass")
class MediaPlayerService : Service() { class MediaPlayerService : MediaBrowserServiceCompat() {
private val binder: IBinder = SimpleServiceBinder(this)
private val scrobbler = Scrobbler() private val scrobbler = Scrobbler()
private val jukeboxMediaPlayer by inject<JukeboxMediaPlayer>() private val jukeboxMediaPlayer by inject<JukeboxMediaPlayer>()
@ -62,20 +69,25 @@ class MediaPlayerService : Service() {
private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>() private val mediaPlayerLifecycleSupport by inject<MediaPlayerLifecycleSupport>()
private var mediaSession: MediaSessionCompat? = null private var mediaSession: MediaSessionCompat? = null
private var mediaSessionToken: MediaSessionCompat.Token? = null
private var isInForeground = false private var isInForeground = false
private var notificationBuilder: NotificationCompat.Builder? = null private var notificationBuilder: NotificationCompat.Builder? = null
val executorService: ExecutorService = Executors.newFixedThreadPool(4)
private val MEDIA_BROWSER_ROOT_ID = "_Ultrasonice_mb_root_"
private val MEDIA_BROWSER_ALBUM_LIST_ROOT = "_Ultrasonic_mb_album_list_root_"
private val MEDIA_BROWSER_ALBUM_PREFIX = "_Ultrasonic_mb_album_prefix_"
private val MEDIA_BROWSER_EXTRA_ENTRY_BYTES = "_Ultrasonic_mb_extra_entry_bytes_"
private val repeatMode: RepeatMode private val repeatMode: RepeatMode
get() = Util.getRepeatMode() get() = Util.getRepeatMode()
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
updateMediaSession(null, PlayerState.IDLE)
mediaSession!!.isActive = true
downloader.onCreate() downloader.onCreate()
shufflePlayBuffer.onCreate() shufflePlayBuffer.onCreate()
localMediaPlayer.init() localMediaPlayer.init()
@ -136,6 +148,99 @@ class MediaPlayerService : Service() {
} }
} }
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot {
// Returns a root ID that clients can use with onLoadChildren() to retrieve
// the content hierarchy. Note that this root isn't actually displayed.
return MediaBrowserServiceCompat.BrowserRoot(MEDIA_BROWSER_ROOT_ID, null)
}
override fun onLoadChildren(
parentMediaId: String,
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
if (MEDIA_BROWSER_ROOT_ID == parentMediaId) {
// Build the MediaItem objects for the top level,
// and put them in the mediaItems list...
var albumList: MediaDescriptionCompat.Builder = MediaDescriptionCompat.Builder()
albumList.setTitle("Browse Albums").setMediaId(MEDIA_BROWSER_ALBUM_LIST_ROOT)
mediaItems.add(
MediaBrowserCompat.MediaItem(
albumList.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
} else if (MEDIA_BROWSER_ALBUM_LIST_ROOT == parentMediaId) {
executorService.execute {
val musicService = getMusicService()
val musicDirectory: MusicDirectory = musicService.getAlbumList2(
"alphabeticalByName", 10, 0, null
)
for (item in musicDirectory.getAllChild()) {
var entryBuilder: MediaDescriptionCompat.Builder =
MediaDescriptionCompat.Builder()
entryBuilder
.setTitle(item.title)
.setMediaId(MEDIA_BROWSER_ALBUM_PREFIX + item.id)
mediaItems.add(
MediaBrowserCompat.MediaItem(
entryBuilder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
}
result.sendResult(mediaItems)
}
result.detach()
return
} else if (parentMediaId.startsWith(MEDIA_BROWSER_ALBUM_PREFIX)) {
executorService.execute {
val musicService = getMusicService()
val id = parentMediaId.substring(MEDIA_BROWSER_ALBUM_PREFIX.length)
val albumDirectory = musicService.getAlbum(
id, "", false
)
for (item in albumDirectory.getAllChild()) {
var extras = Bundle()
var baos = ByteArrayOutputStream()
var oos = ObjectOutputStream(baos)
oos.writeObject(item)
oos.close()
extras.putByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES, baos.toByteArray())
var entryBuilder: MediaDescriptionCompat.Builder =
MediaDescriptionCompat.Builder()
entryBuilder.setTitle(item.title).setMediaId(item.id).setExtras(extras)
mediaItems.add(
MediaBrowserCompat.MediaItem(
entryBuilder.build(),
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
)
}
result.sendResult(mediaItems)
}
result.detach()
return
} else {
// Examine the passed parentMediaId to see which submenu we're at,
// and put the children of that menu in the mediaItems list...
}
result.sendResult(mediaItems)
}
@Synchronized @Synchronized
fun seekTo(position: Int) { fun seekTo(position: Int) {
if (jukeboxMediaPlayer.isEnabled) { if (jukeboxMediaPlayer.isEnabled) {
@ -631,8 +736,8 @@ class MediaPlayerService : Service() {
// Use the Media Style, to enable native Android support for playback notification // Use the Media Style, to enable native Android support for playback notification
val style = androidx.media.app.NotificationCompat.MediaStyle() val style = androidx.media.app.NotificationCompat.MediaStyle()
if (mediaSessionToken != null) { if (getSessionToken() != null) {
style.setMediaSession(mediaSessionToken) style.setMediaSession(getSessionToken())
} }
// Clear old actions // Clear old actions
@ -799,7 +904,7 @@ class MediaPlayerService : Service() {
Timber.w("Creating media session") Timber.w("Creating media session")
mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService") mediaSession = MediaSessionCompat(applicationContext, "UltrasonicService")
mediaSessionToken = mediaSession!!.sessionToken setSessionToken(mediaSession!!.sessionToken)
updateMediaButtonReceiver() updateMediaButtonReceiver()
@ -816,6 +921,32 @@ class MediaPlayerService : Service() {
Timber.v("Media Session Callback: onPlay") Timber.v("Media Session Callback: onPlay")
} }
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras)
if (extras!!.containsKey(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)) {
resetPlayback()
var bytes = extras.getByteArray(MEDIA_BROWSER_EXTRA_ENTRY_BYTES)
var bais = ByteArrayInputStream(bytes)
var ois = ObjectInputStream(bais)
var item: MusicDirectory.Entry = ois.readObject() as MusicDirectory.Entry
val songs: MutableList<MusicDirectory.Entry> = mutableListOf()
songs.add(item)
downloader.download(songs, false, false, false, true)
getPendingIntentForMediaAction(
applicationContext,
KeyEvent.KEYCODE_MEDIA_PLAY,
keycode
).send()
}
Timber.v("Media Session Callback: onPlayFromMediaId")
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
getPendingIntentForMediaAction( getPendingIntentForMediaAction(

View File

@ -0,0 +1,3 @@
<automotiveApp>
<uses name="media"/>
</automotiveApp>