react-native-audio-api 0.11.0-nightly-db51488-20251208 → 0.11.0-nightly-6ba0571-20251209

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 (134) 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 +8 -23
  5. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +127 -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 +668 -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 +43 -0
  15. package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +119 -0
  16. package/ios/audioapi/ios/AudioAPIModule.h +2 -2
  17. package/ios/audioapi/ios/AudioAPIModule.mm +108 -18
  18. package/ios/audioapi/ios/system/AudioEngine.mm +2 -2
  19. package/ios/audioapi/ios/system/AudioSessionManager.mm +1 -1
  20. package/ios/audioapi/ios/system/NotificationManager.mm +1 -1
  21. package/ios/audioapi/ios/system/notification/BaseNotification.h +58 -0
  22. package/ios/audioapi/ios/system/notification/NotificationRegistry.h +70 -0
  23. package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +172 -0
  24. package/ios/audioapi/ios/system/notification/PlaybackNotification.h +27 -0
  25. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +427 -0
  26. package/lib/commonjs/api.js +59 -10
  27. package/lib/commonjs/api.js.map +1 -1
  28. package/lib/commonjs/api.web.js +27 -14
  29. package/lib/commonjs/api.web.js.map +1 -1
  30. package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
  31. package/lib/commonjs/system/AudioManager.js +6 -9
  32. package/lib/commonjs/system/AudioManager.js.map +1 -1
  33. package/lib/commonjs/system/index.js +13 -0
  34. package/lib/commonjs/system/index.js.map +1 -1
  35. package/lib/commonjs/system/notification/PlaybackNotificationManager.js +135 -0
  36. package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -0
  37. package/lib/commonjs/system/notification/RecordingNotificationManager.js +182 -0
  38. package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -0
  39. package/lib/commonjs/system/notification/SimpleNotificationManager.js +122 -0
  40. package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +1 -0
  41. package/lib/commonjs/system/notification/index.js +45 -0
  42. package/lib/commonjs/system/notification/index.js.map +1 -0
  43. package/lib/commonjs/system/notification/types.js +6 -0
  44. package/lib/commonjs/system/notification/types.js.map +1 -0
  45. package/lib/commonjs/web-system/index.js +17 -0
  46. package/lib/commonjs/web-system/index.js.map +1 -0
  47. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +34 -0
  48. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  49. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +34 -0
  50. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -0
  51. package/lib/commonjs/web-system/notification/index.js +21 -0
  52. package/lib/commonjs/web-system/notification/index.js.map +1 -0
  53. package/lib/module/api.js +5 -1
  54. package/lib/module/api.js.map +1 -1
  55. package/lib/module/api.web.js +3 -1
  56. package/lib/module/api.web.js.map +1 -1
  57. package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
  58. package/lib/module/system/AudioManager.js +6 -9
  59. package/lib/module/system/AudioManager.js.map +1 -1
  60. package/lib/module/system/index.js +1 -0
  61. package/lib/module/system/index.js.map +1 -1
  62. package/lib/module/system/notification/PlaybackNotificationManager.js +131 -0
  63. package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -0
  64. package/lib/module/system/notification/RecordingNotificationManager.js +178 -0
  65. package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -0
  66. package/lib/module/system/notification/SimpleNotificationManager.js +118 -0
  67. package/lib/module/system/notification/SimpleNotificationManager.js.map +1 -0
  68. package/lib/module/system/notification/index.js +7 -0
  69. package/lib/module/system/notification/index.js.map +1 -0
  70. package/lib/module/system/notification/types.js +4 -0
  71. package/lib/module/system/notification/types.js.map +1 -0
  72. package/lib/module/web-system/index.js +4 -0
  73. package/lib/module/web-system/index.js.map +1 -0
  74. package/lib/module/web-system/notification/PlaybackNotificationManager.js +30 -0
  75. package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  76. package/lib/module/web-system/notification/RecordingNotificationManager.js +30 -0
  77. package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -0
  78. package/lib/module/web-system/notification/index.js +5 -0
  79. package/lib/module/web-system/notification/index.js.map +1 -0
  80. package/lib/typescript/api.d.ts +3 -1
  81. package/lib/typescript/api.d.ts.map +1 -1
  82. package/lib/typescript/api.web.d.ts +3 -1
  83. package/lib/typescript/api.web.d.ts.map +1 -1
  84. package/lib/typescript/events/types.d.ts +4 -18
  85. package/lib/typescript/events/types.d.ts.map +1 -1
  86. package/lib/typescript/specs/NativeAudioAPIModule.d.ts +16 -5
  87. package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
  88. package/lib/typescript/system/AudioManager.d.ts +4 -5
  89. package/lib/typescript/system/AudioManager.d.ts.map +1 -1
  90. package/lib/typescript/system/index.d.ts +1 -0
  91. package/lib/typescript/system/index.d.ts.map +1 -1
  92. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +22 -0
  93. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  94. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +23 -0
  95. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -0
  96. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +20 -0
  97. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +1 -0
  98. package/lib/typescript/system/notification/index.d.ts +5 -0
  99. package/lib/typescript/system/notification/index.d.ts.map +1 -0
  100. package/lib/typescript/system/notification/types.d.ts +65 -0
  101. package/lib/typescript/system/notification/types.d.ts.map +1 -0
  102. package/lib/typescript/system/types.d.ts +0 -16
  103. package/lib/typescript/system/types.d.ts.map +1 -1
  104. package/lib/typescript/web-system/index.d.ts +2 -0
  105. package/lib/typescript/web-system/index.d.ts.map +1 -0
  106. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +19 -0
  107. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  108. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +19 -0
  109. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -0
  110. package/lib/typescript/web-system/notification/index.d.ts +3 -0
  111. package/lib/typescript/web-system/notification/index.d.ts.map +1 -0
  112. package/package.json +1 -1
  113. package/src/api.ts +17 -2
  114. package/src/api.web.ts +7 -2
  115. package/src/events/types.ts +4 -20
  116. package/src/specs/NativeAudioAPIModule.ts +23 -7
  117. package/src/system/AudioManager.ts +10 -23
  118. package/src/system/index.ts +1 -0
  119. package/src/system/notification/PlaybackNotificationManager.ts +193 -0
  120. package/src/system/notification/RecordingNotificationManager.ts +242 -0
  121. package/src/system/notification/SimpleNotificationManager.ts +170 -0
  122. package/src/system/notification/index.ts +4 -0
  123. package/src/system/notification/types.ts +110 -0
  124. package/src/system/types.ts +0 -18
  125. package/src/web-system/index.ts +1 -0
  126. package/src/web-system/notification/PlaybackNotificationManager.ts +60 -0
  127. package/src/web-system/notification/RecordingNotificationManager.ts +60 -0
  128. package/src/web-system/notification/index.ts +2 -0
  129. package/android/src/main/java/com/swmansion/audioapi/system/LockScreenManager.kt +0 -347
  130. package/android/src/main/java/com/swmansion/audioapi/system/MediaNotificationManager.kt +0 -273
  131. package/android/src/main/java/com/swmansion/audioapi/system/MediaReceiver.kt +0 -57
  132. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionCallback.kt +0 -61
  133. package/ios/audioapi/ios/system/LockScreenManager.h +0 -23
  134. package/ios/audioapi/ios/system/LockScreenManager.mm +0 -314
@@ -0,0 +1,668 @@
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
+ "paused" -> {
287
+ playbackPlayingState = PlaybackStateCompat.STATE_PAUSED
288
+ }
289
+ }
290
+ }
291
+
292
+ // Build MediaMetadata
293
+ val metadataBuilder =
294
+ MediaMetadataCompat
295
+ .Builder()
296
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
297
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
298
+ .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
299
+ .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
300
+
301
+ // Update notification builder
302
+ notificationBuilder
303
+ ?.setContentTitle(title)
304
+ ?.setContentText(artist)
305
+
306
+ // Handle artwork (large icon)
307
+ if (options.hasKey("artwork")) {
308
+ artworkThread?.interrupt()
309
+
310
+ val artworkUrl: String?
311
+ val isLocal: Boolean
312
+
313
+ if (options.getType("artwork") == ReadableType.Map) {
314
+ artworkUrl = options.getMap("artwork")?.getString("uri")
315
+ isLocal = true
316
+ } else {
317
+ artworkUrl = options.getString("artwork")
318
+ isLocal = false
319
+ }
320
+
321
+ if (artworkUrl != null) {
322
+ artworkThread =
323
+ Thread {
324
+ try {
325
+ val bitmap = loadArtwork(artworkUrl, isLocal)
326
+ if (bitmap != null) {
327
+ // Post UI updates to main thread for thread safety
328
+ val context = reactContext.get()
329
+ context?.runOnUiQueueThread {
330
+ try {
331
+ artwork = bitmap
332
+ notificationBuilder?.setLargeIcon(bitmap)
333
+
334
+ // Add artwork to current metadata without touching other fields
335
+ val currentMetadata = mediaSession?.controller?.metadata
336
+ if (currentMetadata != null) {
337
+ val updatedBuilder = MediaMetadataCompat.Builder(currentMetadata)
338
+ updatedBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
339
+ mediaSession?.setMetadata(updatedBuilder.build())
340
+ }
341
+
342
+ // Refresh the notification on main thread
343
+ val notificationManager =
344
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
345
+ notificationManager.notify(notificationId, buildNotification())
346
+ } catch (e: Exception) {
347
+ Log.e(TAG, "Error updating notification with artwork: ${e.message}", e)
348
+ }
349
+ }
350
+ }
351
+ artworkThread = null
352
+ } catch (e: Exception) {
353
+ Log.e(TAG, "Error loading artwork: ${e.message}", e)
354
+ artworkThread = null
355
+ }
356
+ }
357
+ artworkThread?.start()
358
+ }
359
+ }
360
+
361
+ // Handle androidSmallIcon (small icon)
362
+ if (options.hasKey("androidSmallIcon")) {
363
+ smallIconThread?.interrupt()
364
+
365
+ val smallIconUrl: String?
366
+ val isLocal: Boolean
367
+
368
+ if (options.getType("androidSmallIcon") == ReadableType.Map) {
369
+ smallIconUrl = options.getMap("androidSmallIcon")?.getString("uri")
370
+ isLocal = true
371
+ } else {
372
+ smallIconUrl = options.getString("androidSmallIcon")
373
+ isLocal = false
374
+ }
375
+
376
+ if (smallIconUrl != null) {
377
+ smallIconThread =
378
+ Thread {
379
+ try {
380
+ val bitmap = loadArtwork(smallIconUrl, isLocal)
381
+ if (bitmap != null) {
382
+ // Post UI updates to main thread for thread safety
383
+ val context = reactContext.get()
384
+ context?.runOnUiQueueThread {
385
+ try {
386
+ val icon = IconCompat.createWithBitmap(bitmap)
387
+ smallIcon = icon
388
+ notificationBuilder?.setSmallIcon(icon)
389
+
390
+ // Refresh the notification on main thread
391
+ val notificationManager =
392
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
393
+ notificationManager.notify(notificationId, buildNotification())
394
+ } catch (e: Exception) {
395
+ Log.e(TAG, "Error updating notification with small icon: ${e.message}", e)
396
+ }
397
+ }
398
+ }
399
+ smallIconThread = null
400
+ } catch (e: Exception) {
401
+ Log.e(TAG, "Error loading small icon: ${e.message}", e)
402
+ smallIconThread = null
403
+ }
404
+ }
405
+ smallIconThread?.start()
406
+ }
407
+ }
408
+
409
+ updatePlaybackState(playbackPlayingState)
410
+ mediaSession?.setMetadata(metadataBuilder.build())
411
+ mediaSession?.isActive = true
412
+
413
+ return buildNotification()
414
+ }
415
+
416
+ private fun buildNotification(): Notification =
417
+ notificationBuilder?.build()
418
+ ?: throw IllegalStateException("Notification not initialized. Call init() first.")
419
+
420
+ /**
421
+ * Enable or disable a specific control action.
422
+ */
423
+ private fun enableControl(
424
+ name: String,
425
+ enabled: Boolean,
426
+ ) {
427
+ val controlValue =
428
+ when (name) {
429
+ "play" -> PlaybackStateCompat.ACTION_PLAY
430
+ "pause" -> PlaybackStateCompat.ACTION_PAUSE
431
+ "next" -> PlaybackStateCompat.ACTION_SKIP_TO_NEXT
432
+ "previous" -> PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
433
+ "skipForward" -> PlaybackStateCompat.ACTION_FAST_FORWARD
434
+ "skipBackward" -> PlaybackStateCompat.ACTION_REWIND
435
+ "seekTo" -> PlaybackStateCompat.ACTION_SEEK_TO
436
+ else -> 0L
437
+ }
438
+
439
+ if (controlValue == 0L) return
440
+
441
+ enabledControls =
442
+ if (enabled) {
443
+ enabledControls or controlValue
444
+ } else {
445
+ enabledControls and controlValue.inv()
446
+ }
447
+
448
+ // Update actions
449
+ updateActions()
450
+ updateMediaStyle()
451
+
452
+ // Update playback state with new controls
453
+ playbackState =
454
+ playbackStateBuilder
455
+ .setActions(enabledControls)
456
+ .build()
457
+ mediaSession?.setPlaybackState(playbackState)
458
+ }
459
+
460
+ private fun updateActions() {
461
+ val context = reactContext.get() ?: return
462
+ val packageName = context.packageName
463
+
464
+ playAction =
465
+ createAction(
466
+ "play",
467
+ "Play",
468
+ android.R.drawable.ic_media_play,
469
+ PlaybackStateCompat.ACTION_PLAY,
470
+ )
471
+
472
+ pauseAction =
473
+ createAction(
474
+ "pause",
475
+ "Pause",
476
+ android.R.drawable.ic_media_pause,
477
+ PlaybackStateCompat.ACTION_PAUSE,
478
+ )
479
+
480
+ nextAction =
481
+ createAction(
482
+ "next",
483
+ "Next",
484
+ android.R.drawable.ic_media_next,
485
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
486
+ )
487
+
488
+ previousAction =
489
+ createAction(
490
+ "previous",
491
+ "Previous",
492
+ android.R.drawable.ic_media_previous,
493
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
494
+ )
495
+
496
+ skipForwardAction =
497
+ createAction(
498
+ "skip_forward",
499
+ "Skip Forward",
500
+ android.R.drawable.ic_media_ff,
501
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
502
+ )
503
+
504
+ skipBackwardAction =
505
+ createAction(
506
+ "skip_backward",
507
+ "Skip Backward",
508
+ android.R.drawable.ic_media_rew,
509
+ PlaybackStateCompat.ACTION_REWIND,
510
+ )
511
+ }
512
+
513
+ private fun createAction(
514
+ name: String,
515
+ title: String,
516
+ icon: Int,
517
+ action: Long,
518
+ ): NotificationCompat.Action? {
519
+ val context = reactContext.get() ?: return null
520
+
521
+ if ((enabledControls and action) == 0L) {
522
+ return null
523
+ }
524
+
525
+ val keyCode = PlaybackStateCompat.toKeyCode(action)
526
+ val intent = Intent(MEDIA_BUTTON)
527
+ intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
528
+ intent.putExtra(ContactsContract.Directory.PACKAGE_NAME, context.packageName)
529
+
530
+ val pendingIntent =
531
+ PendingIntent.getBroadcast(
532
+ context,
533
+ keyCode,
534
+ intent,
535
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
536
+ )
537
+
538
+ return NotificationCompat.Action(icon, title, pendingIntent)
539
+ }
540
+
541
+ private fun updatePlaybackState(state: Int) {
542
+ isPlaying = state == PlaybackStateCompat.STATE_PLAYING
543
+
544
+ playbackState =
545
+ playbackStateBuilder
546
+ .setState(state, elapsedTime, speed)
547
+ .setActions(enabledControls)
548
+ .build()
549
+ if (mediaSession != null) {
550
+ Log.d(TAG, "mediaSession is not null")
551
+ } else {
552
+ Log.d(TAG, "mediaSession is null")
553
+ }
554
+ mediaSession?.setPlaybackState(playbackState)
555
+
556
+ // Update ongoing state - only persistent when playing
557
+ notificationBuilder?.setOngoing(isPlaying)
558
+ }
559
+
560
+ private fun updateMediaStyle() {
561
+ val style = MediaStyle()
562
+ style.setMediaSession(mediaSession?.sessionToken)
563
+
564
+ // Clear existing actions
565
+ notificationBuilder?.clearActions()
566
+
567
+ // Add actions in order based on enabled controls
568
+ val compactActions = mutableListOf<Int>()
569
+ var actionIndex = 0
570
+
571
+ if (previousAction != null) {
572
+ notificationBuilder?.addAction(previousAction)
573
+ actionIndex++
574
+ }
575
+
576
+ if (skipBackwardAction != null) {
577
+ notificationBuilder?.addAction(skipBackwardAction)
578
+ actionIndex++
579
+ }
580
+
581
+ if (playAction != null && !isPlaying) {
582
+ notificationBuilder?.addAction(playAction)
583
+ compactActions.add(actionIndex)
584
+ actionIndex++
585
+ }
586
+
587
+ if (pauseAction != null && isPlaying) {
588
+ notificationBuilder?.addAction(pauseAction)
589
+ compactActions.add(actionIndex)
590
+ actionIndex++
591
+ }
592
+
593
+ if (skipForwardAction != null) {
594
+ notificationBuilder?.addAction(skipForwardAction)
595
+ actionIndex++
596
+ }
597
+
598
+ if (nextAction != null) {
599
+ notificationBuilder?.addAction(nextAction)
600
+ actionIndex++
601
+ }
602
+
603
+ // Show up to 3 actions in compact view
604
+ style.setShowActionsInCompactView(*compactActions.take(3).toIntArray())
605
+ notificationBuilder?.setStyle(style)
606
+ }
607
+
608
+ private fun loadArtwork(
609
+ url: String,
610
+ isLocal: Boolean,
611
+ ): Bitmap? {
612
+ val context = reactContext.get() ?: return null
613
+
614
+ return try {
615
+ if (isLocal && !url.startsWith("http")) {
616
+ // Load local resource
617
+ val helper =
618
+ com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
619
+ .getInstance()
620
+ val drawable = helper.getResourceDrawable(context, url)
621
+
622
+ if (drawable is BitmapDrawable) {
623
+ drawable.bitmap
624
+ } else {
625
+ BitmapFactory.decodeFile(url)
626
+ }
627
+ } else {
628
+ // Load from URL
629
+ val connection = URL(url).openConnection()
630
+ connection.connect()
631
+ val inputStream = connection.getInputStream()
632
+ val bitmap = BitmapFactory.decodeStream(inputStream)
633
+ inputStream.close()
634
+ bitmap
635
+ }
636
+ } catch (e: IOException) {
637
+ Log.e(TAG, "Failed to load artwork: ${e.message}", e)
638
+ null
639
+ } catch (e: Exception) {
640
+ Log.e(TAG, "Error loading artwork: ${e.message}", e)
641
+ null
642
+ }
643
+ }
644
+
645
+ private fun createNotificationChannel() {
646
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
647
+ val context = reactContext.get() ?: return
648
+
649
+ val channel =
650
+ android.app
651
+ .NotificationChannel(
652
+ channelId,
653
+ "Audio Playback",
654
+ android.app.NotificationManager.IMPORTANCE_LOW,
655
+ ).apply {
656
+ description = "Media playback controls and information"
657
+ setShowBadge(false)
658
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
659
+ }
660
+
661
+ val notificationManager =
662
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
663
+ notificationManager.createNotificationChannel(channel)
664
+
665
+ Log.d(TAG, "Notification channel created: $channelId")
666
+ }
667
+ }
668
+ }
@@ -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
+ }