react-native-audio-api 0.11.0-nightly-b30bac9-20260114 → 0.11.0

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