mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-08 21:08:43 +02:00
Initial android auto support, fix bug saving user settings
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 15
|
||||
versionName "0.9.0-beta"
|
||||
versionCode 16
|
||||
versionName "0.9.1-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -31,18 +30,25 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<!--Used by Android Auto-->
|
||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@drawable/icon" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<!-- TODO: Can remove in future -->
|
||||
<!-- <provider-->
|
||||
<!-- android:name="androidx.core.content.FileProvider"-->
|
||||
<!-- android:authorities="${applicationId}.fileprovider"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:grantUriPermissions="true">-->
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="android.support.FILE_PROVIDER_PATHS"-->
|
||||
<!-- android:resource="@xml/file_paths" />-->
|
||||
<!-- </provider>-->
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
@@ -51,8 +57,12 @@
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:exported="true"
|
||||
android:enabled="true"
|
||||
android:name=".PlayerNotificationService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
@@ -34,9 +34,19 @@ class Audiobook {
|
||||
cover = jsondata.getString("cover", "").toString()
|
||||
playlistUrl = jsondata.getString("playlistUrl", "").toString()
|
||||
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
|
||||
startTime = jsondata.getString("startTime", "0")!!.toLong()
|
||||
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||
|
||||
if (jsondata.has("startTime")) {
|
||||
startTime = jsondata.getString("startTime", "0")!!.toLong()
|
||||
}
|
||||
|
||||
if (jsondata.has("duration")) {
|
||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||
}
|
||||
|
||||
if (jsondata.has("playbackSpeed")) {
|
||||
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||
}
|
||||
|
||||
|
||||
// Local data
|
||||
isLocal = jsondata.getBoolean("isLocal", false) == true
|
||||
|
||||
@@ -60,12 +60,12 @@ class MainActivity : BridgeActivity() {
|
||||
|
||||
mConnection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
Log.w(tag, "Service Disconnected")
|
||||
Log.w(tag, "Service Disconnected $name")
|
||||
mBounded = false
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Log.d(tag, "Service Connected")
|
||||
Log.d(tag, "Service Connected $name")
|
||||
|
||||
|
||||
mBounded = true
|
||||
|
||||
@@ -5,11 +5,9 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import org.json.JSONObject
|
||||
|
||||
@CapacitorPlugin(name = "MyNativeAudio")
|
||||
class MyNativeAudio : Plugin() {
|
||||
@@ -31,6 +29,13 @@ class MyNativeAudio : Plugin() {
|
||||
override fun onMetadata(metadata:JSObject) {
|
||||
notifyListeners("onMetadata", metadata)
|
||||
}
|
||||
override fun onPrepare(audiobookId:String, playWhenReady:Boolean) {
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("playWhenReady", playWhenReady)
|
||||
notifyListeners("onPrepareMedia", jsobj)
|
||||
}
|
||||
override fun onCar() {}
|
||||
})
|
||||
}
|
||||
mainActivity.pluginCallback = foregroundServiceReady
|
||||
@@ -141,4 +146,37 @@ class MyNativeAudio : Plugin() {
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun setAudiobooks(call: PluginCall) {
|
||||
var audiobooks = call.getArray("audiobooks", JSArray())
|
||||
if (audiobooks == null) {
|
||||
Log.w(tag, "setAudiobooks IS NULL")
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
var audiobookObjs = mutableListOf<Audiobook>()
|
||||
|
||||
var len = audiobooks.length()
|
||||
(0 until len).forEach { _it ->
|
||||
var jsonobj = audiobooks.get(_it) as JSONObject
|
||||
|
||||
var _names = Array(jsonobj.names().length()) {
|
||||
jsonobj.names().getString(it)
|
||||
}
|
||||
var jsobj = JSObject(jsonobj, _names)
|
||||
|
||||
if (jsobj.has("duration")) {
|
||||
var dur = jsobj.getDouble("duration")
|
||||
var duration = Math.floor(dur * 1000L).toLong()
|
||||
jsobj.put("duration", duration)
|
||||
}
|
||||
|
||||
var audiobook = Audiobook(jsobj)
|
||||
audiobookObjs.add(audiobook)
|
||||
}
|
||||
Log.d(tag, "Setting Audiobooks ${audiobookObjs.size}")
|
||||
playerNotificationService.setAudiobooks(audiobookObjs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@ import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.provider.MediaStore
|
||||
import android.os.*
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaControllerCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
@@ -25,20 +26,17 @@ import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaExtractor
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import com.google.android.exoplayer2.upstream.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
|
||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||
|
||||
class PlayerNotificationService : Service() {
|
||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
companion object {
|
||||
var isStarted = false
|
||||
@@ -47,6 +45,8 @@ class PlayerNotificationService : Service() {
|
||||
interface MyCustomObjectListener {
|
||||
fun onPlayingUpdate(isPlaying: Boolean)
|
||||
fun onMetadata(metadata: JSObject)
|
||||
fun onPrepare(audiobookId:String, playWhenReady:Boolean)
|
||||
fun onCar()
|
||||
}
|
||||
|
||||
private val tag = "PlayerService"
|
||||
@@ -71,6 +71,8 @@ class PlayerNotificationService : Service() {
|
||||
|
||||
private var currentAudiobook:Audiobook? = null
|
||||
|
||||
private var audiobooks = mutableListOf<Audiobook>()
|
||||
|
||||
fun setCustomObjectListener(mylistener: MyCustomObjectListener) {
|
||||
listener = mylistener
|
||||
}
|
||||
@@ -78,8 +80,14 @@ class PlayerNotificationService : Service() {
|
||||
/*
|
||||
Service related stuff
|
||||
*/
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
Log.d(tag, "onBind")
|
||||
|
||||
// Android Auto Media Browser Service
|
||||
if (SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
Log.d(tag, "Is Media Browser Service")
|
||||
return super.onBind(intent);
|
||||
}
|
||||
return binder
|
||||
}
|
||||
|
||||
@@ -97,11 +105,13 @@ class PlayerNotificationService : Service() {
|
||||
Log.d(tag, "onStartCommand $startId")
|
||||
isStarted = true
|
||||
|
||||
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onStart(intent: Intent?, startId: Int) {
|
||||
Log.d(tag, "onStart $startId" )
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
||||
val chan = NotificationChannel(channelId,
|
||||
@@ -166,6 +176,9 @@ class PlayerNotificationService : Service() {
|
||||
|
||||
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
|
||||
|
||||
// This is for Media Browser
|
||||
sessionToken = mediaSession.sessionToken
|
||||
|
||||
val builder = PlayerNotificationManager.Builder(
|
||||
ctx,
|
||||
notificationId,
|
||||
@@ -232,7 +245,53 @@ class PlayerNotificationService : Service() {
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
val myPlaybackPreparer:MediaSessionConnector.PlaybackPreparer = object :MediaSessionConnector.PlaybackPreparer {
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
|
||||
Log.d(tag, "ON COMMAND $command")
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getSupportedPrepareActions(): Long {
|
||||
Log.d(tag, "GET SUPORTED ACITONS")
|
||||
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||
}
|
||||
|
||||
override fun onPrepare(playWhenReady: Boolean) {
|
||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||
|
||||
var audiobook = audiobooks[0]
|
||||
if (audiobook == null) {
|
||||
Log.e(tag, "Audiobook NOT FOUND")
|
||||
return
|
||||
}
|
||||
listener.onPrepare(audiobook.id, playWhenReady)
|
||||
}
|
||||
|
||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
||||
var audiobook = audiobooks.find { it.id == mediaId }
|
||||
if (audiobook == null) {
|
||||
Log.e(tag, "Audiobook NOT FOUND")
|
||||
return
|
||||
}
|
||||
listener.onPrepare(audiobook.id, playWhenReady)
|
||||
}
|
||||
|
||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||
}
|
||||
|
||||
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
|
||||
Log.d(tag, "ON PREPARE FROM URI $uri")
|
||||
}
|
||||
|
||||
}
|
||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||
mediaSessionConnector.setPlaybackPreparer(myPlaybackPreparer)
|
||||
mediaSessionConnector.setPlayer(mPlayer)
|
||||
|
||||
//attach player to playerNotificationManager
|
||||
@@ -286,9 +345,6 @@ class PlayerNotificationService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private fun setPlayerListeners() {
|
||||
mPlayer.addListener(object : Player.Listener {
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
@@ -451,4 +507,81 @@ class PlayerNotificationService : Service() {
|
||||
metadata.put("stateName", stateName)
|
||||
if (listener != null) listener.onMetadata(metadata)
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||
//
|
||||
private val MY_MEDIA_ROOT_ID = "audiobookshelf"
|
||||
|
||||
fun setAudiobooks(_audiobooks:MutableList<Audiobook>) {
|
||||
audiobooks = _audiobooks
|
||||
}
|
||||
|
||||
private fun isValid(packageName:String, uid:Int) : Boolean {
|
||||
Log.d(tag, "Check package $packageName is valid with uid $uid")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
||||
// Verify that the specified package is allowed to access your
|
||||
// content! You'll need to write your own logic to do this.
|
||||
return if (!isValid(clientPackageName, clientUid)) {
|
||||
// If the request comes from an untrusted package, return null.
|
||||
// No further calls will be made to other media browsing methods.
|
||||
null
|
||||
} else {
|
||||
listener.onCar()
|
||||
|
||||
val extras = Bundle()
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
|
||||
MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, extras)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
if (audiobooks.size == 0) {
|
||||
result.sendResult(mediaItems)
|
||||
return
|
||||
}
|
||||
|
||||
audiobooks.forEach {
|
||||
var builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(it.id)
|
||||
.setTitle(it.title)
|
||||
.setSubtitle(it.author)
|
||||
.setMediaUri(it.playlistUri)
|
||||
.setIconUri(it.coverUri)
|
||||
|
||||
// val extras = Bundle()
|
||||
// var startsWithA = it.title.toLowerCase().startsWith("a")
|
||||
// var groupTitle = "test group
|
||||
// extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
// builder.setExtras(extras)\
|
||||
// Log.d(tag, "Load Media Item for AUTO ${it.title} - ${it.author}")
|
||||
|
||||
var mediaDescription = builder.build()
|
||||
var newMediaItem = MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
mediaItems.add(newMediaItem)
|
||||
}
|
||||
|
||||
// Check if this is the root menu:
|
||||
if (MY_MEDIA_ROOT_ID == parentMediaId) {
|
||||
// build the MediaItem objects for the top level,
|
||||
// and put them in the mediaItems list
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<automotiveApp>
|
||||
<uses name="media" />
|
||||
</automotiveApp>
|
||||
Reference in New Issue
Block a user