react-native-audio-api 0.11.0-alpha.4 → 0.11.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +164 -16
  2. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioPlayer.kt +10 -8
  3. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioRecorder.kt +10 -8
  4. package/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +3 -4
  5. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +128 -0
  6. package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +116 -0
  7. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +115 -107
  8. package/android/src/main/java/com/swmansion/audioapi/system/PermissionRequestListener.kt +2 -1
  9. package/android/src/main/java/com/swmansion/audioapi/system/notification/BaseNotification.kt +47 -0
  10. package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +191 -0
  11. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +669 -0
  12. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +33 -0
  13. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +303 -0
  14. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +45 -0
  15. package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +119 -0
  16. package/common/cpp/audioapi/utils/AudioFileProperties.h +17 -17
  17. package/ios/audioapi/ios/AudioAPIModule.h +2 -2
  18. package/ios/audioapi/ios/AudioAPIModule.mm +108 -18
  19. package/ios/audioapi/ios/core/NativeAudioRecorder.m +8 -1
  20. package/ios/audioapi/ios/system/AudioEngine.mm +2 -2
  21. package/ios/audioapi/ios/system/AudioSessionManager.mm +12 -9
  22. package/ios/audioapi/ios/system/NotificationManager.mm +1 -1
  23. package/ios/audioapi/ios/system/notification/BaseNotification.h +58 -0
  24. package/ios/audioapi/ios/system/notification/NotificationRegistry.h +70 -0
  25. package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +172 -0
  26. package/ios/audioapi/ios/system/notification/PlaybackNotification.h +27 -0
  27. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +427 -0
  28. package/lib/commonjs/api.js +72 -1
  29. package/lib/commonjs/api.js.map +1 -1
  30. package/lib/commonjs/api.web.js +27 -14
  31. package/lib/commonjs/api.web.js.map +1 -1
  32. package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
  33. package/lib/commonjs/system/AudioManager.js +6 -9
  34. package/lib/commonjs/system/AudioManager.js.map +1 -1
  35. package/lib/commonjs/system/index.js +13 -0
  36. package/lib/commonjs/system/index.js.map +1 -1
  37. package/lib/commonjs/system/notification/PlaybackNotificationManager.js +135 -0
  38. package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -0
  39. package/lib/commonjs/system/notification/RecordingNotificationManager.js +182 -0
  40. package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -0
  41. package/lib/commonjs/system/notification/SimpleNotificationManager.js +122 -0
  42. package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +1 -0
  43. package/lib/commonjs/system/notification/index.js +45 -0
  44. package/lib/commonjs/system/notification/index.js.map +1 -0
  45. package/lib/commonjs/system/notification/types.js +6 -0
  46. package/lib/commonjs/system/notification/types.js.map +1 -0
  47. package/lib/commonjs/types.js +17 -17
  48. package/lib/commonjs/types.js.map +1 -1
  49. package/lib/commonjs/web-system/index.js +17 -0
  50. package/lib/commonjs/web-system/index.js.map +1 -0
  51. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +34 -0
  52. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  53. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +34 -0
  54. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -0
  55. package/lib/commonjs/web-system/notification/index.js +21 -0
  56. package/lib/commonjs/web-system/notification/index.js.map +1 -0
  57. package/lib/module/api.js +4 -0
  58. package/lib/module/api.js.map +1 -1
  59. package/lib/module/api.web.js +3 -1
  60. package/lib/module/api.web.js.map +1 -1
  61. package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
  62. package/lib/module/system/AudioManager.js +6 -9
  63. package/lib/module/system/AudioManager.js.map +1 -1
  64. package/lib/module/system/index.js +1 -0
  65. package/lib/module/system/index.js.map +1 -1
  66. package/lib/module/system/notification/PlaybackNotificationManager.js +131 -0
  67. package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -0
  68. package/lib/module/system/notification/RecordingNotificationManager.js +178 -0
  69. package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -0
  70. package/lib/module/system/notification/SimpleNotificationManager.js +118 -0
  71. package/lib/module/system/notification/SimpleNotificationManager.js.map +1 -0
  72. package/lib/module/system/notification/index.js +7 -0
  73. package/lib/module/system/notification/index.js.map +1 -0
  74. package/lib/module/system/notification/types.js +4 -0
  75. package/lib/module/system/notification/types.js.map +1 -0
  76. package/lib/module/types.js +17 -17
  77. package/lib/module/types.js.map +1 -1
  78. package/lib/module/web-system/index.js +4 -0
  79. package/lib/module/web-system/index.js.map +1 -0
  80. package/lib/module/web-system/notification/PlaybackNotificationManager.js +30 -0
  81. package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  82. package/lib/module/web-system/notification/RecordingNotificationManager.js +30 -0
  83. package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -0
  84. package/lib/module/web-system/notification/index.js +5 -0
  85. package/lib/module/web-system/notification/index.js.map +1 -0
  86. package/lib/typescript/api.d.ts +2 -0
  87. package/lib/typescript/api.d.ts.map +1 -1
  88. package/lib/typescript/api.web.d.ts +3 -1
  89. package/lib/typescript/api.web.d.ts.map +1 -1
  90. package/lib/typescript/events/types.d.ts +3 -3
  91. package/lib/typescript/events/types.d.ts.map +1 -1
  92. package/lib/typescript/specs/NativeAudioAPIModule.d.ts +16 -5
  93. package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
  94. package/lib/typescript/system/AudioManager.d.ts +4 -5
  95. package/lib/typescript/system/AudioManager.d.ts.map +1 -1
  96. package/lib/typescript/system/index.d.ts +1 -0
  97. package/lib/typescript/system/index.d.ts.map +1 -1
  98. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +22 -0
  99. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  100. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +23 -0
  101. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -0
  102. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +20 -0
  103. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +1 -0
  104. package/lib/typescript/system/notification/index.d.ts +5 -0
  105. package/lib/typescript/system/notification/index.d.ts.map +1 -0
  106. package/lib/typescript/system/notification/types.d.ts +65 -0
  107. package/lib/typescript/system/notification/types.d.ts.map +1 -0
  108. package/lib/typescript/system/types.d.ts +0 -16
  109. package/lib/typescript/system/types.d.ts.map +1 -1
  110. package/lib/typescript/types.d.ts +16 -16
  111. package/lib/typescript/types.d.ts.map +1 -1
  112. package/lib/typescript/web-system/index.d.ts +2 -0
  113. package/lib/typescript/web-system/index.d.ts.map +1 -0
  114. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +19 -0
  115. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  116. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +19 -0
  117. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -0
  118. package/lib/typescript/web-system/notification/index.d.ts +3 -0
  119. package/lib/typescript/web-system/notification/index.d.ts.map +1 -0
  120. package/package.json +1 -1
  121. package/src/api.ts +17 -0
  122. package/src/api.web.ts +7 -2
  123. package/src/events/types.ts +3 -4
  124. package/src/specs/NativeAudioAPIModule.ts +23 -7
  125. package/src/system/AudioManager.ts +10 -23
  126. package/src/system/index.ts +1 -0
  127. package/src/system/notification/PlaybackNotificationManager.ts +193 -0
  128. package/src/system/notification/RecordingNotificationManager.ts +242 -0
  129. package/src/system/notification/SimpleNotificationManager.ts +170 -0
  130. package/src/system/notification/index.ts +4 -0
  131. package/src/system/notification/types.ts +111 -0
  132. package/src/system/types.ts +0 -18
  133. package/src/types.ts +17 -17
  134. package/src/web-system/index.ts +1 -0
  135. package/src/web-system/notification/PlaybackNotificationManager.ts +60 -0
  136. package/src/web-system/notification/RecordingNotificationManager.ts +60 -0
  137. package/src/web-system/notification/index.ts +2 -0
  138. package/android/src/main/java/com/swmansion/audioapi/system/LockScreenManager.kt +0 -347
  139. package/android/src/main/java/com/swmansion/audioapi/system/MediaNotificationManager.kt +0 -273
  140. package/android/src/main/java/com/swmansion/audioapi/system/MediaReceiver.kt +0 -57
  141. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionCallback.kt +0 -61
@@ -0,0 +1,669 @@
1
+ package com.swmansion.audioapi.system.notification
2
+
3
+ import android.app.Notification
4
+ import android.app.PendingIntent
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.graphics.Bitmap
8
+ import android.graphics.BitmapFactory
9
+ import android.graphics.drawable.BitmapDrawable
10
+ import android.os.Build
11
+ import android.provider.ContactsContract
12
+ import android.support.v4.media.MediaMetadataCompat
13
+ import android.support.v4.media.session.MediaSessionCompat
14
+ import android.support.v4.media.session.PlaybackStateCompat
15
+ import android.util.Log
16
+ import android.view.KeyEvent
17
+ import androidx.core.app.NotificationCompat
18
+ import androidx.core.graphics.drawable.IconCompat
19
+ import androidx.media.app.NotificationCompat.MediaStyle
20
+ import com.facebook.react.bridge.ReactApplicationContext
21
+ import com.facebook.react.bridge.ReadableMap
22
+ import com.facebook.react.bridge.ReadableType
23
+ import com.swmansion.audioapi.AudioAPIModule
24
+ import java.io.IOException
25
+ import java.lang.ref.WeakReference
26
+ import java.net.URL
27
+
28
+ /**
29
+ * PlaybackNotification
30
+ *
31
+ * This notification:
32
+ * - Shows media metadata (title, artist, album, artwork)
33
+ * - Supports playback controls (play, pause, next, previous, skip)
34
+ * - Integrates with Android MediaSession for lock screen controls
35
+ * - Is persistent and cannot be swiped away when playing
36
+ * - Notifies its dismissal via PlaybackNotificationReceiver
37
+ */
38
+ class PlaybackNotification(
39
+ private val reactContext: WeakReference<ReactApplicationContext>,
40
+ private val audioAPIModule: WeakReference<AudioAPIModule>,
41
+ private val notificationId: Int,
42
+ private val channelId: String,
43
+ ) : BaseNotification {
44
+ companion object {
45
+ private const val TAG = "PlaybackNotification"
46
+ const val MEDIA_BUTTON = "playback_notification_media_button"
47
+ const val PACKAGE_NAME = "com.swmansion.audioapi.playback"
48
+ }
49
+
50
+ private var mediaSession: MediaSessionCompat? = null
51
+ private var notificationBuilder: NotificationCompat.Builder? = null
52
+ private var playbackStateBuilder: PlaybackStateCompat.Builder = PlaybackStateCompat.Builder()
53
+ private var playbackState: PlaybackStateCompat = playbackStateBuilder.build()
54
+ private var playbackPlayingState: Int = PlaybackStateCompat.STATE_PAUSED
55
+
56
+ private var enabledControls: Long = 0
57
+ private var isPlaying: Boolean = false
58
+
59
+ // Metadata
60
+ private var title: String? = null
61
+ private var artist: String? = null
62
+ private var album: String? = null
63
+ private var artwork: Bitmap? = null
64
+ private var smallIcon: IconCompat? = null
65
+ private var duration: Long = 0L
66
+ private var elapsedTime: Long = 0L
67
+ private var speed: Float = 1.0F
68
+
69
+ // Actions
70
+ private var playAction: NotificationCompat.Action? = null
71
+ private var pauseAction: NotificationCompat.Action? = null
72
+ private var nextAction: NotificationCompat.Action? = null
73
+ private var previousAction: NotificationCompat.Action? = null
74
+ private var skipForwardAction: NotificationCompat.Action? = null
75
+ private var skipBackwardAction: NotificationCompat.Action? = null
76
+
77
+ private var artworkThread: Thread? = null
78
+ private var smallIconThread: Thread? = null
79
+
80
+ override fun init(params: ReadableMap?): Notification {
81
+ val context = reactContext.get() ?: throw IllegalStateException("React context is null")
82
+
83
+ // Create notification channel first
84
+ createNotificationChannel()
85
+
86
+ // Create MediaSession
87
+ mediaSession = MediaSessionCompat(context, "PlaybackNotification")
88
+ mediaSession?.isActive = true
89
+
90
+ // Set up media session callbacks
91
+ mediaSession?.setCallback(
92
+ object : MediaSessionCompat.Callback() {
93
+ override fun onPlay() {
94
+ Log.d(TAG, "MediaSession: onPlay")
95
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPlay", mapOf())
96
+ }
97
+
98
+ override fun onPause() {
99
+ Log.d(TAG, "MediaSession: onPause")
100
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPause", mapOf())
101
+ }
102
+
103
+ override fun onSkipToNext() {
104
+ Log.d(TAG, "MediaSession: onSkipToNext")
105
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationNext", mapOf())
106
+ }
107
+
108
+ override fun onSkipToPrevious() {
109
+ Log.d(TAG, "MediaSession: onSkipToPrevious")
110
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationPrevious", mapOf())
111
+ }
112
+
113
+ override fun onFastForward() {
114
+ Log.d(TAG, "MediaSession: onFastForward")
115
+ val body = HashMap<String, Any>().apply { put("value", 15) }
116
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipForward", body)
117
+ }
118
+
119
+ override fun onRewind() {
120
+ Log.d(TAG, "MediaSession: onRewind")
121
+ val body = HashMap<String, Any>().apply { put("value", 15) }
122
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipBackward", body)
123
+ }
124
+
125
+ override fun onSeekTo(pos: Long) {
126
+ Log.d(TAG, "MediaSession: onSeekTo - position: $pos")
127
+ val body = HashMap<String, Any>().apply { put("value", pos / 1000.0) } // Convert to seconds
128
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSeekTo", body)
129
+ }
130
+ },
131
+ )
132
+
133
+ // Create notification builder
134
+ notificationBuilder =
135
+ NotificationCompat
136
+ .Builder(context, channelId)
137
+ .setSmallIcon(android.R.drawable.ic_media_play)
138
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
139
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
140
+ .setOngoing(true) // Make it persistent (can't swipe away)
141
+
142
+ // Set content intent to open app
143
+ val packageName = context.packageName
144
+ val openAppIntent = context.packageManager.getLaunchIntentForPackage(packageName)
145
+ if (openAppIntent != null) {
146
+ val pendingIntent =
147
+ PendingIntent.getActivity(
148
+ context,
149
+ 0,
150
+ openAppIntent,
151
+ PendingIntent.FLAG_IMMUTABLE,
152
+ )
153
+ notificationBuilder?.setContentIntent(pendingIntent)
154
+ }
155
+
156
+ // Set delete intent to handle dismissal
157
+ val deleteIntent = Intent(PlaybackNotificationReceiver.ACTION_NOTIFICATION_DISMISSED)
158
+ deleteIntent.setPackage(context.packageName)
159
+ val deletePendingIntent =
160
+ PendingIntent.getBroadcast(
161
+ context,
162
+ notificationId,
163
+ deleteIntent,
164
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
165
+ )
166
+ notificationBuilder?.setDeleteIntent(deletePendingIntent)
167
+
168
+ // Enable default controls
169
+ enableControl("play", true)
170
+ enableControl("pause", true)
171
+ enableControl("next", true)
172
+ enableControl("previous", true)
173
+ enableControl("seekTo", true)
174
+
175
+ updateMediaStyle()
176
+ updatePlaybackState(PlaybackStateCompat.STATE_PAUSED)
177
+
178
+ // Apply initial params if provided
179
+ if (params != null) {
180
+ update(params)
181
+ }
182
+
183
+ return buildNotification()
184
+ }
185
+
186
+ override fun reset() {
187
+ // Interrupt artwork loading if in progress
188
+ artworkThread?.interrupt()
189
+ artworkThread = null
190
+ smallIconThread?.interrupt()
191
+ smallIconThread = null
192
+
193
+ // Reset metadata
194
+ title = null
195
+ artist = null
196
+ album = null
197
+ artwork = null
198
+ smallIcon = null
199
+ duration = 0L
200
+ elapsedTime = 0L
201
+ speed = 1.0F
202
+ isPlaying = false
203
+
204
+ // Reset media session
205
+ val emptyMetadata = MediaMetadataCompat.Builder().build()
206
+ mediaSession?.setMetadata(emptyMetadata)
207
+
208
+ playbackState =
209
+ playbackStateBuilder
210
+ .setState(PlaybackStateCompat.STATE_NONE, 0, 0f)
211
+ .setActions(enabledControls)
212
+ .build()
213
+ mediaSession?.setPlaybackState(playbackState)
214
+ mediaSession?.isActive = false
215
+ mediaSession?.release()
216
+ mediaSession = null
217
+ }
218
+
219
+ override fun getNotificationId(): Int = notificationId
220
+
221
+ override fun getChannelId(): String = channelId
222
+
223
+ override fun update(options: ReadableMap?): Notification {
224
+ if (options == null) {
225
+ return buildNotification()
226
+ }
227
+
228
+ // Handle control enable/disable
229
+ if (options.hasKey("control") && options.hasKey("enabled")) {
230
+ val control = options.getString("control")
231
+ val enabled = options.getBoolean("enabled")
232
+ if (control != null) {
233
+ enableControl(control, enabled)
234
+ }
235
+ return buildNotification()
236
+ }
237
+
238
+ // Update metadata
239
+ if (options.hasKey("title")) {
240
+ title = options.getString("title")
241
+ }
242
+
243
+ if (options.hasKey("artist")) {
244
+ artist = options.getString("artist")
245
+ }
246
+
247
+ if (options.hasKey("album")) {
248
+ album = options.getString("album")
249
+ }
250
+
251
+ if (options.hasKey("duration")) {
252
+ duration = (options.getDouble("duration") * 1000).toLong()
253
+ }
254
+
255
+ if (options.hasKey("elapsedTime")) {
256
+ elapsedTime = (options.getDouble("elapsedTime") * 1000).toLong()
257
+ } else {
258
+ // Use the current position from the media session controller (live calculated position)
259
+ val controllerPosition = mediaSession?.controller?.playbackState?.position
260
+ if (controllerPosition != null && controllerPosition > 0) {
261
+ elapsedTime = controllerPosition
262
+ }
263
+ }
264
+
265
+ if (options.hasKey("speed")) {
266
+ speed = options.getDouble("speed").toFloat()
267
+ } else {
268
+ // Use the current speed from the media session controller
269
+ val controllerSpeed = mediaSession?.controller?.playbackState?.playbackSpeed
270
+ if (controllerSpeed != null && controllerSpeed > 0) {
271
+ speed = controllerSpeed
272
+ }
273
+ }
274
+
275
+ // Ensure speed is at least 1.0 when playing
276
+ if (isPlaying && speed == 0f) {
277
+ speed = 1.0f
278
+ }
279
+
280
+ // Update playback state
281
+ if (options.hasKey("state")) {
282
+ when (options.getString("state")) {
283
+ "playing" -> {
284
+ playbackPlayingState = PlaybackStateCompat.STATE_PLAYING
285
+ }
286
+
287
+ "paused" -> {
288
+ playbackPlayingState = PlaybackStateCompat.STATE_PAUSED
289
+ }
290
+ }
291
+ }
292
+
293
+ // Build MediaMetadata
294
+ val metadataBuilder =
295
+ MediaMetadataCompat
296
+ .Builder()
297
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
298
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
299
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
300
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
301
+
302
+ // Update notification builder
303
+ notificationBuilder
304
+ ?.setContentTitle(title)
305
+ ?.setContentText(artist)
306
+
307
+ // Handle artwork (large icon)
308
+ if (options.hasKey("artwork")) {
309
+ artworkThread?.interrupt()
310
+
311
+ val artworkUrl: String?
312
+ val isLocal: Boolean
313
+
314
+ if (options.getType("artwork") == ReadableType.Map) {
315
+ artworkUrl = options.getMap("artwork")?.getString("uri")
316
+ isLocal = true
317
+ } else {
318
+ artworkUrl = options.getString("artwork")
319
+ isLocal = false
320
+ }
321
+
322
+ if (artworkUrl != null) {
323
+ artworkThread =
324
+ Thread {
325
+ try {
326
+ val bitmap = loadArtwork(artworkUrl, isLocal)
327
+ if (bitmap != null) {
328
+ // Post UI updates to main thread for thread safety
329
+ val context = reactContext.get()
330
+ context?.runOnUiQueueThread {
331
+ try {
332
+ artwork = bitmap
333
+ notificationBuilder?.setLargeIcon(bitmap)
334
+
335
+ // Add artwork to current metadata without touching other fields
336
+ val currentMetadata = mediaSession?.controller?.metadata
337
+ if (currentMetadata != null) {
338
+ val updatedBuilder = MediaMetadataCompat.Builder(currentMetadata)
339
+ updatedBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
340
+ mediaSession?.setMetadata(updatedBuilder.build())
341
+ }
342
+
343
+ // Refresh the notification on main thread
344
+ val notificationManager =
345
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
346
+ notificationManager.notify(notificationId, buildNotification())
347
+ } catch (e: Exception) {
348
+ Log.e(TAG, "Error updating notification with artwork: ${e.message}", e)
349
+ }
350
+ }
351
+ }
352
+ artworkThread = null
353
+ } catch (e: Exception) {
354
+ Log.e(TAG, "Error loading artwork: ${e.message}", e)
355
+ artworkThread = null
356
+ }
357
+ }
358
+ artworkThread?.start()
359
+ }
360
+ }
361
+
362
+ // Handle androidSmallIcon (small icon)
363
+ if (options.hasKey("androidSmallIcon")) {
364
+ smallIconThread?.interrupt()
365
+
366
+ val smallIconUrl: String?
367
+ val isLocal: Boolean
368
+
369
+ if (options.getType("androidSmallIcon") == ReadableType.Map) {
370
+ smallIconUrl = options.getMap("androidSmallIcon")?.getString("uri")
371
+ isLocal = true
372
+ } else {
373
+ smallIconUrl = options.getString("androidSmallIcon")
374
+ isLocal = false
375
+ }
376
+
377
+ if (smallIconUrl != null) {
378
+ smallIconThread =
379
+ Thread {
380
+ try {
381
+ val bitmap = loadArtwork(smallIconUrl, isLocal)
382
+ if (bitmap != null) {
383
+ // Post UI updates to main thread for thread safety
384
+ val context = reactContext.get()
385
+ context?.runOnUiQueueThread {
386
+ try {
387
+ val icon = IconCompat.createWithBitmap(bitmap)
388
+ smallIcon = icon
389
+ notificationBuilder?.setSmallIcon(icon)
390
+
391
+ // Refresh the notification on main thread
392
+ val notificationManager =
393
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
394
+ notificationManager.notify(notificationId, buildNotification())
395
+ } catch (e: Exception) {
396
+ Log.e(TAG, "Error updating notification with small icon: ${e.message}", e)
397
+ }
398
+ }
399
+ }
400
+ smallIconThread = null
401
+ } catch (e: Exception) {
402
+ Log.e(TAG, "Error loading small icon: ${e.message}", e)
403
+ smallIconThread = null
404
+ }
405
+ }
406
+ smallIconThread?.start()
407
+ }
408
+ }
409
+
410
+ updatePlaybackState(playbackPlayingState)
411
+ mediaSession?.setMetadata(metadataBuilder.build())
412
+ mediaSession?.isActive = true
413
+
414
+ return buildNotification()
415
+ }
416
+
417
+ private fun buildNotification(): Notification =
418
+ notificationBuilder?.build()
419
+ ?: throw IllegalStateException("Notification not initialized. Call init() first.")
420
+
421
+ /**
422
+ * Enable or disable a specific control action.
423
+ */
424
+ private fun enableControl(
425
+ name: String,
426
+ enabled: Boolean,
427
+ ) {
428
+ val controlValue =
429
+ when (name) {
430
+ "play" -> PlaybackStateCompat.ACTION_PLAY
431
+ "pause" -> PlaybackStateCompat.ACTION_PAUSE
432
+ "next" -> PlaybackStateCompat.ACTION_SKIP_TO_NEXT
433
+ "previous" -> PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
434
+ "skipForward" -> PlaybackStateCompat.ACTION_FAST_FORWARD
435
+ "skipBackward" -> PlaybackStateCompat.ACTION_REWIND
436
+ "seekTo" -> PlaybackStateCompat.ACTION_SEEK_TO
437
+ else -> 0L
438
+ }
439
+
440
+ if (controlValue == 0L) return
441
+
442
+ enabledControls =
443
+ if (enabled) {
444
+ enabledControls or controlValue
445
+ } else {
446
+ enabledControls and controlValue.inv()
447
+ }
448
+
449
+ // Update actions
450
+ updateActions()
451
+ updateMediaStyle()
452
+
453
+ // Update playback state with new controls
454
+ playbackState =
455
+ playbackStateBuilder
456
+ .setActions(enabledControls)
457
+ .build()
458
+ mediaSession?.setPlaybackState(playbackState)
459
+ }
460
+
461
+ private fun updateActions() {
462
+ val context = reactContext.get() ?: return
463
+ val packageName = context.packageName
464
+
465
+ playAction =
466
+ createAction(
467
+ "play",
468
+ "Play",
469
+ android.R.drawable.ic_media_play,
470
+ PlaybackStateCompat.ACTION_PLAY,
471
+ )
472
+
473
+ pauseAction =
474
+ createAction(
475
+ "pause",
476
+ "Pause",
477
+ android.R.drawable.ic_media_pause,
478
+ PlaybackStateCompat.ACTION_PAUSE,
479
+ )
480
+
481
+ nextAction =
482
+ createAction(
483
+ "next",
484
+ "Next",
485
+ android.R.drawable.ic_media_next,
486
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
487
+ )
488
+
489
+ previousAction =
490
+ createAction(
491
+ "previous",
492
+ "Previous",
493
+ android.R.drawable.ic_media_previous,
494
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
495
+ )
496
+
497
+ skipForwardAction =
498
+ createAction(
499
+ "skip_forward",
500
+ "Skip Forward",
501
+ android.R.drawable.ic_media_ff,
502
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
503
+ )
504
+
505
+ skipBackwardAction =
506
+ createAction(
507
+ "skip_backward",
508
+ "Skip Backward",
509
+ android.R.drawable.ic_media_rew,
510
+ PlaybackStateCompat.ACTION_REWIND,
511
+ )
512
+ }
513
+
514
+ private fun createAction(
515
+ name: String,
516
+ title: String,
517
+ icon: Int,
518
+ action: Long,
519
+ ): NotificationCompat.Action? {
520
+ val context = reactContext.get() ?: return null
521
+
522
+ if ((enabledControls and action) == 0L) {
523
+ return null
524
+ }
525
+
526
+ val keyCode = PlaybackStateCompat.toKeyCode(action)
527
+ val intent = Intent(MEDIA_BUTTON)
528
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
529
+ intent.putExtra(ContactsContract.Directory.PACKAGE_NAME, context.packageName)
530
+
531
+ val pendingIntent =
532
+ PendingIntent.getBroadcast(
533
+ context,
534
+ keyCode,
535
+ intent,
536
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
537
+ )
538
+
539
+ return NotificationCompat.Action(icon, title, pendingIntent)
540
+ }
541
+
542
+ private fun updatePlaybackState(state: Int) {
543
+ isPlaying = state == PlaybackStateCompat.STATE_PLAYING
544
+
545
+ playbackState =
546
+ playbackStateBuilder
547
+ .setState(state, elapsedTime, speed)
548
+ .setActions(enabledControls)
549
+ .build()
550
+ if (mediaSession != null) {
551
+ Log.d(TAG, "mediaSession is not null")
552
+ } else {
553
+ Log.d(TAG, "mediaSession is null")
554
+ }
555
+ mediaSession?.setPlaybackState(playbackState)
556
+
557
+ // Update ongoing state - only persistent when playing
558
+ notificationBuilder?.setOngoing(isPlaying)
559
+ }
560
+
561
+ private fun updateMediaStyle() {
562
+ val style = MediaStyle()
563
+ style.setMediaSession(mediaSession?.sessionToken)
564
+
565
+ // Clear existing actions
566
+ notificationBuilder?.clearActions()
567
+
568
+ // Add actions in order based on enabled controls
569
+ val compactActions = mutableListOf<Int>()
570
+ var actionIndex = 0
571
+
572
+ if (previousAction != null) {
573
+ notificationBuilder?.addAction(previousAction)
574
+ actionIndex++
575
+ }
576
+
577
+ if (skipBackwardAction != null) {
578
+ notificationBuilder?.addAction(skipBackwardAction)
579
+ actionIndex++
580
+ }
581
+
582
+ if (playAction != null && !isPlaying) {
583
+ notificationBuilder?.addAction(playAction)
584
+ compactActions.add(actionIndex)
585
+ actionIndex++
586
+ }
587
+
588
+ if (pauseAction != null && isPlaying) {
589
+ notificationBuilder?.addAction(pauseAction)
590
+ compactActions.add(actionIndex)
591
+ actionIndex++
592
+ }
593
+
594
+ if (skipForwardAction != null) {
595
+ notificationBuilder?.addAction(skipForwardAction)
596
+ actionIndex++
597
+ }
598
+
599
+ if (nextAction != null) {
600
+ notificationBuilder?.addAction(nextAction)
601
+ actionIndex++
602
+ }
603
+
604
+ // Show up to 3 actions in compact view
605
+ style.setShowActionsInCompactView(*compactActions.take(3).toIntArray())
606
+ notificationBuilder?.setStyle(style)
607
+ }
608
+
609
+ private fun loadArtwork(
610
+ url: String,
611
+ isLocal: Boolean,
612
+ ): Bitmap? {
613
+ val context = reactContext.get() ?: return null
614
+
615
+ return try {
616
+ if (isLocal && !url.startsWith("http")) {
617
+ // Load local resource
618
+ val helper =
619
+ com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
620
+ .getInstance()
621
+ val drawable = helper.getResourceDrawable(context, url)
622
+
623
+ if (drawable is BitmapDrawable) {
624
+ drawable.bitmap
625
+ } else {
626
+ BitmapFactory.decodeFile(url)
627
+ }
628
+ } else {
629
+ // Load from URL
630
+ val connection = URL(url).openConnection()
631
+ connection.connect()
632
+ val inputStream = connection.getInputStream()
633
+ val bitmap = BitmapFactory.decodeStream(inputStream)
634
+ inputStream.close()
635
+ bitmap
636
+ }
637
+ } catch (e: IOException) {
638
+ Log.e(TAG, "Failed to load artwork: ${e.message}", e)
639
+ null
640
+ } catch (e: Exception) {
641
+ Log.e(TAG, "Error loading artwork: ${e.message}", e)
642
+ null
643
+ }
644
+ }
645
+
646
+ private fun createNotificationChannel() {
647
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
648
+ val context = reactContext.get() ?: return
649
+
650
+ val channel =
651
+ android.app
652
+ .NotificationChannel(
653
+ channelId,
654
+ "Audio Playback",
655
+ android.app.NotificationManager.IMPORTANCE_LOW,
656
+ ).apply {
657
+ description = "Media playback controls and information"
658
+ setShowBadge(false)
659
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
660
+ }
661
+
662
+ val notificationManager =
663
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
664
+ notificationManager.createNotificationChannel(channel)
665
+
666
+ Log.d(TAG, "Notification channel created: $channelId")
667
+ }
668
+ }
669
+ }
@@ -0,0 +1,33 @@
1
+ package com.swmansion.audioapi.system.notification
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+ import com.swmansion.audioapi.AudioAPIModule
8
+
9
+ /**
10
+ * Broadcast receiver for handling playback notification dismissal.
11
+ */
12
+ class PlaybackNotificationReceiver : BroadcastReceiver() {
13
+ companion object {
14
+ const val ACTION_NOTIFICATION_DISMISSED = "com.swmansion.audioapi.PLAYBACK_NOTIFICATION_DISMISSED"
15
+ private const val TAG = "PlaybackNotificationReceiver"
16
+
17
+ private var audioAPIModule: AudioAPIModule? = null
18
+
19
+ fun setAudioAPIModule(module: AudioAPIModule?) {
20
+ audioAPIModule = module
21
+ }
22
+ }
23
+
24
+ override fun onReceive(
25
+ context: Context?,
26
+ intent: Intent?,
27
+ ) {
28
+ if (intent?.action == ACTION_NOTIFICATION_DISMISSED) {
29
+ Log.d(TAG, "Notification dismissed by user")
30
+ audioAPIModule?.invokeHandlerWithEventNameAndEventBody("playbackNotificationDismissed", mapOf())
31
+ }
32
+ }
33
+ }