From 76882780a4de97aae44b121e28e6a241b30a8036 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 23 Jul 2022 18:09:10 -0500 Subject: [PATCH] Add:Start of app widget to control media playback --- android/app/build.gradle | 6 +- android/app/src/main/AndroidManifest.xml | 115 ++++++++++-------- .../com/audiobookshelf/app/NewAppWidget.kt | 96 +++++++++++++++ .../app/device/DeviceManager.kt | 11 +- .../app/player/MediaSessionCallback.kt | 17 +++ .../app/player/PlayerListener.kt | 2 + .../app/player/PlayerNotificationService.kt | 3 +- .../example_appwidget_preview.png | Bin 0 -> 3522 bytes .../drawable-v21/app_widget_background.xml | 11 ++ .../app_widget_inner_view_background.xml | 11 ++ .../src/main/res/layout/new_app_widget.xml | 26 ++++ .../app/src/main/res/values-v21/styles.xml | 11 ++ android/app/src/main/res/values/attrs.xml | 7 ++ android/app/src/main/res/values/colors.xml | 6 + android/app/src/main/res/values/dimens.xml | 10 ++ android/app/src/main/res/values/strings.xml | 2 + android/app/src/main/res/values/styles.xml | 9 ++ android/app/src/main/res/values/themes.xml | 17 +++ .../src/main/res/xml/new_app_widget_info.xml | 12 ++ 19 files changed, 315 insertions(+), 57 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt create mode 100644 android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png create mode 100644 android/app/src/main/res/drawable-v21/app_widget_background.xml create mode 100644 android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml create mode 100644 android/app/src/main/res/layout/new_app_widget.xml create mode 100644 android/app/src/main/res/values-v21/styles.xml create mode 100644 android/app/src/main/res/values/attrs.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/dimens.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/new_app_widget_info.xml diff --git a/android/app/build.gradle b/android/app/build.gradle index bf08564d..cb382c3d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -21,7 +21,11 @@ kotlin { } android { - kotlinOptions { + + buildFeatures { + viewBinding true + } + kotlinOptions { freeCompilerArgs = ['-Xjvm-default=all'] } compileSdkVersion rootProject.ext.compileSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e737fd2b..929d7ec5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,83 +1,94 @@ + xmlns:dist="http://schemas.android.com/apk/distribution" + xmlns:tools="http://schemas.android.com/tools" + package="com.audiobookshelf.app" + android:installLocation="preferExternal" > - - + + android:usesCleartextTraffic="true" > + + + + + + - - + - - - + android:name="com.google.android.gms.car.notification.SmallIcon" + android:resource="@drawable/icon" /> + + + android:label="@string/title_activity_main" + android:launchMode="singleTask" + android:theme="@style/AppTheme.NoActionBarLaunch" > + + - - - - + + + + - - - - + + - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + diff --git a/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt b/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt new file mode 100644 index 00000000..c160ca06 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/NewAppWidget.kt @@ -0,0 +1,96 @@ +package com.audiobookshelf.app + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE +import android.util.Log +import android.widget.RemoteViews +import androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent +import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.device.WidgetEventEmitter +import com.audiobookshelf.app.player.PlayerNotificationService +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.AppWidgetTarget +import com.bumptech.glide.request.transition.Transition + +/** + * Implementation of App Widget functionality. + */ +class NewAppWidget : AppWidgetProvider() { + val tag = "NewAppWidget" + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + // There may be multiple widgets active, so update all of them + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId, null,false) + } + } + + override fun onEnabled(context: Context) { + Log.w(tag, "onEnabled check context ${context.packageName}") + + // Enter relevant functionality for when the first widget is created + DeviceManager.widgetUpdater = (object : WidgetEventEmitter { + override fun onPlayerChanged(pns:PlayerNotificationService) { + val isPlaying = pns.currentPlayer.isPlaying + Log.i(tag, "onPlayerChanged | Is Playing? $isPlaying") + + val appWidgetManager = AppWidgetManager.getInstance(context) + val componentName = ComponentName(context, NewAppWidget::class.java) + val ids = appWidgetManager.getAppWidgetIds(componentName) + + val playbackSession = pns.getCurrentPlaybackSessionCopy() + val cover = playbackSession?.getCoverUri() + + for (widgetId in ids) { + updateAppWidget(context, appWidgetManager, widgetId, cover, isPlaying) + } + } + }) + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } +} + +internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, coverUri:Uri?, isPlaying:Boolean) { + + val views = RemoteViews(context.packageName, R.layout.new_app_widget) + + val playPausePI = buildMediaButtonPendingIntent(context, ACTION_PLAY_PAUSE) + views.setOnClickPendingIntent(R.id.playPauseIcon, playPausePI) + + val wholeWidgetClickI = Intent(context, MainActivity::class.java) + wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val wholeWidgetClickPI = PendingIntent.getActivity( + context, + System.currentTimeMillis().toInt(), + wholeWidgetClickI, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.appWidget, wholeWidgetClickPI) + + val imageUri = coverUri ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon) + val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.imageView, views, appWidgetId) { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + super.onResourceReady(resource, transition) + } + } + + val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon) + Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt) + + val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark + views.setImageViewResource(R.id.playPauseIcon, playPauseResource) + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index d233ffc4..ef6b65cd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -1,9 +1,12 @@ package com.audiobookshelf.app.device import android.util.Log -import com.audiobookshelf.app.data.DbManager -import com.audiobookshelf.app.data.DeviceData -import com.audiobookshelf.app.data.ServerConnectionConfig +import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.player.PlayerNotificationService + +interface WidgetEventEmitter { + fun onPlayerChanged(pns:PlayerNotificationService) +} object DeviceManager { const val tag = "DeviceManager" @@ -19,6 +22,8 @@ object DeviceManager { val isConnectedToServer get() = serverConnectionConfig != null val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null + var widgetUpdater:WidgetEventEmitter? = null + init { Log.d(tag, "Device Manager Singleton invoked") } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index d23f7451..3809286d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -132,8 +132,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi } fun handleCallMediaButton(intent: Intent): Boolean { + Log.w(tag, "handleCallMediaButton $intent | ${intent.action}") + if(Intent.ACTION_MEDIA_BUTTON == intent.action) { val keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}") + + if (keyEvent?.action == KeyEvent.ACTION_DOWN) { + Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}") + when (keyEvent.keyCode) { + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { + Log.d(tag, "handleCallMediaButton: Media Play/Pause") + if (playerNotificationService.mPlayer.isPlaying) { + playerNotificationService.pause() + } else { + playerNotificationService.play() + } + } + } + } if (keyEvent?.action == KeyEvent.ACTION_UP) { Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}") diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index 302abf33..239ebb97 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -118,6 +118,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) : } playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying) + + DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 68cdd8ec..2cbd9c35 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -210,6 +210,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { .apply { setSessionActivity(sessionActivityPendingIntent) isActive = true + setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) } val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) @@ -768,7 +769,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app" private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox" private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant" - private val VALID_MEDIA_BROWSERS = mutableListOf(ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) + private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", "com.android.systemui", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) private val AUTO_MEDIA_ROOT = "/" private val ALL_ROOT = "__ALL__" diff --git a/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..894b069a4907d258f60b1b2406b90f5a0fe1c35b GIT binary patch literal 3522 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 + + + + + + diff --git a/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml new file mode 100644 index 00000000..cd72ec57 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/new_app_widget.xml b/android/app/src/main/res/layout/new_app_widget.xml new file mode 100644 index 00000000..99ac1b46 --- /dev/null +++ b/android/app/src/main/res/layout/new_app_widget.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..80d515ff --- /dev/null +++ b/android/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..7781ac86 --- /dev/null +++ b/android/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..b2bffa8d --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #FFE1F5FE + #FF81D4FA + #FF039BE5 + #FF01579B + \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..4db8c590 --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + + + 0dp + + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index a99df0e9..b0944308 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -4,4 +4,6 @@ audiobookshelf com.audiobookshelf.app com.audiobookshelf.app + Add widget + Simple widget for audiobookshelf playback diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 102f1273..ccd013a3 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -19,4 +19,13 @@ + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..254f4a55 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/android/app/src/main/res/xml/new_app_widget_info.xml b/android/app/src/main/res/xml/new_app_widget_info.xml new file mode 100644 index 00000000..ffa76ee7 --- /dev/null +++ b/android/app/src/main/res/xml/new_app_widget_info.xml @@ -0,0 +1,12 @@ + +