diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ae890b3..0df21e09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## Unreleased +## [Unreleased] -### Fixed +### Fixed - Fixed a build issue on the iOS bridge caused by the deprecated DispatchDispatch protocol. +- Fixed an issue on Android where the `MediaPlaybackService` would sometimes crash with a `ForegroundServiceDidNotStartInTimeException` exception. + +### Added + +- Added the ability to override both small and large notification icons in Android with `ic_notification_small` and `ic_notification_large` resources respectively. ## [3.5.0] - 24-01-30 diff --git a/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt b/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt index b0618e1d3..0d93ef592 100644 --- a/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt +++ b/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt @@ -294,7 +294,7 @@ class PlayerEventEmitter internal constructor( ) payload.putMap(EVENT_PROP_VERSION, WritableNativeMap().apply { putString(EVENT_PROP_VERSION, THEOplayerGlobal.getVersion()) - putString(EVENT_PROP_SUITE_VERSION, THEOplayerGlobal.getPlayerSuiteVersion()) + putString(EVENT_PROP_SUITE_VERSION, "") }) // Notify the player is ready diff --git a/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt b/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt index 91b1e3222..4bc58b60e 100644 --- a/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt +++ b/android/src/main/java/com/theoplayer/broadcast/DefaultEventDispatcher.kt @@ -21,7 +21,8 @@ open class DefaultEventDispatcher: EventDispatcher> { fun dispatchEvent(event: Event<*>) { _listeners[event.type]?.forEach { listener -> - (listener as EventListener>).handleEvent(event) + @Suppress("UNCHECKED_CAST") + (listener as EventListener>).handleEvent(event) } } } diff --git a/android/src/main/java/com/theoplayer/cast/CastEventAdapter.kt b/android/src/main/java/com/theoplayer/cast/CastEventAdapter.kt index c3c4c63f4..960d1edb2 100644 --- a/android/src/main/java/com/theoplayer/cast/CastEventAdapter.kt +++ b/android/src/main/java/com/theoplayer/cast/CastEventAdapter.kt @@ -41,6 +41,7 @@ class CastEventAdapter(private val castApi: Cast, private val emitter: Emitter) private fun serializeError(error: CastError): WritableMap { val errorPayload = Arguments.createMap() + @Suppress("SENSELESS_NULL_IN_WHEN") errorPayload.putString( EVENT_PROP_ERROR_CODE, when (error.errorCode) { diff --git a/android/src/main/java/com/theoplayer/drm/ContentProtectionModule.kt b/android/src/main/java/com/theoplayer/drm/ContentProtectionModule.kt index 13a2a2ada..0315b3591 100644 --- a/android/src/main/java/com/theoplayer/drm/ContentProtectionModule.kt +++ b/android/src/main/java/com/theoplayer/drm/ContentProtectionModule.kt @@ -216,10 +216,12 @@ class ContentProtectionModule(private val context: ReactApplicationContext) : } @ReactMethod + @Suppress("UNUSED_PARAMETER") fun addListener(eventName: String?) { } @ReactMethod + @Suppress("UNUSED_PARAMETER") fun removeListeners(count: Int?) { } diff --git a/android/src/main/java/com/theoplayer/media/MediaNotificationBuilder.kt b/android/src/main/java/com/theoplayer/media/MediaNotificationBuilder.kt index 11544844b..d72b280ea 100644 --- a/android/src/main/java/com/theoplayer/media/MediaNotificationBuilder.kt +++ b/android/src/main/java/com/theoplayer/media/MediaNotificationBuilder.kt @@ -5,6 +5,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri import android.os.Build import android.support.v4.media.session.MediaSessionCompat @@ -195,3 +196,13 @@ fun fetchImageFromUri(uri: Uri?, block: (Bitmap?) -> Unit) { ) } } + +fun loadPlaceHolderIcon(context: Context, res: Int = R.drawable.ic_notification_large): Bitmap? { + return try { + BitmapFactory.decodeResource(context.resources, res) + } catch(e: Exception) { + // Make sure we never crash on trying to decode a possibly overridden icon resource. + Log.w(TAG, "Failed to decode placeHolderIcon: ${e.message}") + null + } +} diff --git a/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt b/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt index c9de63310..31b58f1eb 100644 --- a/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt +++ b/android/src/main/java/com/theoplayer/media/MediaPlaybackService.kt @@ -3,6 +3,7 @@ package com.theoplayer.media import android.app.* import android.content.Intent import android.content.pm.ServiceInfo +import android.graphics.Bitmap import android.os.Binder import android.os.Build import android.os.Bundle @@ -96,6 +97,8 @@ class MediaPlaybackService : MediaBrowserServiceCompat() { Log.w(TAG, "Failed to start foreground service: ${e.message}") } + // Quickly post a notification and already call startForeground. This has to happen within 5s + // after creating the service to avoid a ForegroundServiceDidNotStartInTimeException updateNotification(PlaybackStateCompat.STATE_PLAYING) } @@ -222,6 +225,7 @@ class MediaPlaybackService : MediaBrowserServiceCompat() { } } + @Suppress("UNUSED_PARAMETER") private fun allowBrowsing(clientPackageName: String, clientUid: Int): Boolean { // Only allow browsing from the same package return TextUtils.equals(clientPackageName, packageName) @@ -264,26 +268,13 @@ class MediaPlaybackService : MediaBrowserServiceCompat() { // When a service runs in the foreground, it must display a notification, ideally // with one or more transport controls. The notification should also include useful // information from the session's metadata. - // Fetch large icon asynchronously + + // Get the foreground service started in time before fetching an icon. + startForegroundWithPlaybackState(playbackState, loadPlaceHolderIcon(this)) + + // Fetch the correct large icon asynchronously. fetchImageFromUri(mediaSession.controller.metadata?.description?.iconUri) { largeIcon -> - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground( - NOTIFICATION_ID, - notificationBuilder.build(playbackState, largeIcon, enableMediaControls), - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - ) - } else { - startForeground( - NOTIFICATION_ID, - notificationBuilder.build(playbackState, largeIcon, enableMediaControls) - ) - } - } catch (e: IllegalStateException) { - // Make sure that app does not crash in case anything goes wrong with starting the service. - // https://issuetracker.google.com/issues/229000935 - Log.w(TAG, "Failed to start foreground service: ${e.message}") - } + startForegroundWithPlaybackState(playbackState, largeIcon) } } PlaybackStateCompat.STATE_STOPPED -> { @@ -303,4 +294,25 @@ class MediaPlaybackService : MediaBrowserServiceCompat() { } } } + + private fun startForegroundWithPlaybackState(@PlaybackStateCompat.State playbackState: Int, largeIcon: Bitmap? = null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + notificationBuilder.build(playbackState, largeIcon, enableMediaControls), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + ) + } else { + startForeground( + NOTIFICATION_ID, + notificationBuilder.build(playbackState, largeIcon, enableMediaControls) + ) + } + } catch (e: IllegalStateException) { + // Make sure that app does not crash in case anything goes wrong with starting the service. + // https://issuetracker.google.com/issues/229000935 + Log.w(TAG, "Failed to start foreground service: ${e.message}") + } + } } diff --git a/android/src/main/res/drawable/ic_notification_large.png b/android/src/main/res/drawable/ic_notification_large.png new file mode 100644 index 000000000..93f7b75fc Binary files /dev/null and b/android/src/main/res/drawable/ic_notification_large.png differ