react-native-audio-api 0.11.0-nightly-db51488-20251207 → 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
@@ -1,6 +1,7 @@
1
1
  package com.swmansion.audioapi
2
2
 
3
3
  import com.facebook.jni.HybridData
4
+ import com.facebook.react.bridge.Arguments
4
5
  import com.facebook.react.bridge.LifecycleEventListener
5
6
  import com.facebook.react.bridge.Promise
6
7
  import com.facebook.react.bridge.ReactApplicationContext
@@ -9,6 +10,7 @@ import com.facebook.react.bridge.ReadableMap
9
10
  import com.facebook.react.common.annotations.FrameworkAPI
10
11
  import com.facebook.react.module.annotations.ReactModule
11
12
  import com.facebook.react.turbomodule.core.CallInvokerHolderImpl
13
+ import com.swmansion.audioapi.system.ForegroundServiceManager
12
14
  import com.swmansion.audioapi.system.MediaSessionManager
13
15
  import com.swmansion.audioapi.system.PermissionRequestListener
14
16
  import java.lang.ref.WeakReference
@@ -84,7 +86,8 @@ class AudioAPIModule(
84
86
 
85
87
  override fun invalidate() {
86
88
  reactContext.get()?.removeLifecycleEventListener(this)
87
- // think about cleaning up resources, singletons etc.
89
+ // Cleanup foreground service manager
90
+ ForegroundServiceManager.cleanup()
88
91
  }
89
92
 
90
93
  override fun getDevicePreferredSampleRate(): Double = MediaSessionManager.getDevicePreferredSampleRate()
@@ -109,21 +112,6 @@ class AudioAPIModule(
109
112
  // nothing to do here
110
113
  }
111
114
 
112
- override fun setLockScreenInfo(info: ReadableMap?) {
113
- MediaSessionManager.setLockScreenInfo(info)
114
- }
115
-
116
- override fun resetLockScreenInfo() {
117
- MediaSessionManager.resetLockScreenInfo()
118
- }
119
-
120
- override fun enableRemoteCommand(
121
- name: String?,
122
- enabled: Boolean,
123
- ) {
124
- MediaSessionManager.enableRemoteCommand(name!!, enabled)
125
- }
126
-
127
115
  override fun observeAudioInterruptions(enabled: Boolean) {
128
116
  MediaSessionManager.observeAudioInterruptions(enabled)
129
117
  }
@@ -145,7 +133,167 @@ class AudioAPIModule(
145
133
  promise.resolve(MediaSessionManager.checkRecordingPermissions())
146
134
  }
147
135
 
136
+ override fun requestNotificationPermissions(promise: Promise) {
137
+ val permissionRequestListener = PermissionRequestListener(promise)
138
+ MediaSessionManager.requestNotificationPermissions(permissionRequestListener)
139
+ }
140
+
141
+ override fun checkNotificationPermissions(promise: Promise) {
142
+ promise.resolve(MediaSessionManager.checkNotificationPermissions())
143
+ }
144
+
148
145
  override fun getDevicesInfo(promise: Promise) {
149
146
  promise.resolve(MediaSessionManager.getDevicesInfo())
150
147
  }
148
+
149
+ // New notification system methods
150
+ override fun registerNotification(
151
+ type: String?,
152
+ key: String?,
153
+ promise: Promise?,
154
+ ) {
155
+ try {
156
+ if (type == null || key == null) {
157
+ val result = Arguments.createMap()
158
+ result.putBoolean("success", false)
159
+ result.putString("error", "Type and key are required")
160
+ promise?.resolve(result)
161
+ return
162
+ }
163
+
164
+ MediaSessionManager.registerNotification(type, key)
165
+
166
+ val result = Arguments.createMap()
167
+ result.putBoolean("success", true)
168
+ promise?.resolve(result)
169
+ } catch (e: Exception) {
170
+ val result = Arguments.createMap()
171
+ result.putBoolean("success", false)
172
+ result.putString("error", e.message ?: "Unknown error")
173
+ promise?.resolve(result)
174
+ }
175
+ }
176
+
177
+ override fun showNotification(
178
+ key: String?,
179
+ options: ReadableMap?,
180
+ promise: Promise?,
181
+ ) {
182
+ try {
183
+ if (key == null) {
184
+ val result = Arguments.createMap()
185
+ result.putBoolean("success", false)
186
+ result.putString("error", "Key is required")
187
+ promise?.resolve(result)
188
+ return
189
+ }
190
+
191
+ MediaSessionManager.showNotification(key, options)
192
+
193
+ val result = Arguments.createMap()
194
+ result.putBoolean("success", true)
195
+ promise?.resolve(result)
196
+ } catch (e: Exception) {
197
+ val result = Arguments.createMap()
198
+ result.putBoolean("success", false)
199
+ result.putString("error", e.message ?: "Unknown error")
200
+ promise?.resolve(result)
201
+ }
202
+ }
203
+
204
+ override fun updateNotification(
205
+ key: String?,
206
+ options: ReadableMap?,
207
+ promise: Promise?,
208
+ ) {
209
+ try {
210
+ if (key == null) {
211
+ val result = Arguments.createMap()
212
+ result.putBoolean("success", false)
213
+ result.putString("error", "Key is required")
214
+ promise?.resolve(result)
215
+ return
216
+ }
217
+
218
+ MediaSessionManager.updateNotification(key, options)
219
+
220
+ val result = Arguments.createMap()
221
+ result.putBoolean("success", true)
222
+ promise?.resolve(result)
223
+ } catch (e: Exception) {
224
+ val result = Arguments.createMap()
225
+ result.putBoolean("success", false)
226
+ result.putString("error", e.message ?: "Unknown error")
227
+ promise?.resolve(result)
228
+ }
229
+ }
230
+
231
+ override fun hideNotification(
232
+ key: String?,
233
+ promise: Promise?,
234
+ ) {
235
+ try {
236
+ if (key == null) {
237
+ val result = Arguments.createMap()
238
+ result.putBoolean("success", false)
239
+ result.putString("error", "Key is required")
240
+ promise?.resolve(result)
241
+ return
242
+ }
243
+
244
+ MediaSessionManager.hideNotification(key)
245
+
246
+ val result = Arguments.createMap()
247
+ result.putBoolean("success", true)
248
+ promise?.resolve(result)
249
+ } catch (e: Exception) {
250
+ val result = Arguments.createMap()
251
+ result.putBoolean("success", false)
252
+ result.putString("error", e.message ?: "Unknown error")
253
+ promise?.resolve(result)
254
+ }
255
+ }
256
+
257
+ override fun unregisterNotification(
258
+ key: String?,
259
+ promise: Promise?,
260
+ ) {
261
+ try {
262
+ if (key == null) {
263
+ val result = Arguments.createMap()
264
+ result.putBoolean("success", false)
265
+ result.putString("error", "Key is required")
266
+ promise?.resolve(result)
267
+ return
268
+ }
269
+
270
+ MediaSessionManager.unregisterNotification(key)
271
+
272
+ val result = Arguments.createMap()
273
+ result.putBoolean("success", true)
274
+ promise?.resolve(result)
275
+ } catch (e: Exception) {
276
+ val result = Arguments.createMap()
277
+ result.putBoolean("success", false)
278
+ result.putString("error", e.message ?: "Unknown error")
279
+ promise?.resolve(result)
280
+ }
281
+ }
282
+
283
+ override fun isNotificationActive(
284
+ key: String?,
285
+ promise: Promise?,
286
+ ) {
287
+ try {
288
+ if (key == null) {
289
+ promise?.resolve(false)
290
+ return
291
+ }
292
+
293
+ val isActive = MediaSessionManager.isNotificationActive(key)
294
+ promise?.resolve(isActive)
295
+ } catch (e: Exception) {
296
+ promise?.resolve(false)
297
+ }
298
+ }
151
299
  }
@@ -1,24 +1,26 @@
1
1
  package com.swmansion.audioapi.core
2
2
 
3
3
  import com.facebook.common.internal.DoNotStrip
4
- import com.swmansion.audioapi.system.MediaSessionManager
4
+ import com.swmansion.audioapi.system.ForegroundServiceManager
5
+ import java.util.UUID
5
6
 
6
7
  @DoNotStrip
7
8
  class NativeAudioPlayer {
8
- private var sourceNodeId: String? = null
9
+ private var playerId: String? = null
9
10
 
10
11
  @DoNotStrip
11
12
  fun start() {
12
- this.sourceNodeId = MediaSessionManager.attachAudioPlayer(this)
13
- MediaSessionManager.startForegroundServiceIfNecessary()
13
+ if (playerId == null) {
14
+ playerId = UUID.randomUUID().toString()
15
+ ForegroundServiceManager.subscribe("player_$playerId")
16
+ }
14
17
  }
15
18
 
16
19
  @DoNotStrip
17
20
  fun stop() {
18
- this.sourceNodeId?.let {
19
- MediaSessionManager.detachAudioPlayer(it)
20
- this.sourceNodeId = null
21
+ playerId?.let {
22
+ ForegroundServiceManager.unsubscribe("player_$it")
23
+ playerId = null
21
24
  }
22
- MediaSessionManager.stopForegroundServiceIfNecessary()
23
25
  }
24
26
  }
@@ -1,24 +1,26 @@
1
1
  package com.swmansion.audioapi.core
2
2
 
3
3
  import com.facebook.common.internal.DoNotStrip
4
- import com.swmansion.audioapi.system.MediaSessionManager
4
+ import com.swmansion.audioapi.system.ForegroundServiceManager
5
+ import java.util.UUID
5
6
 
6
7
  @DoNotStrip
7
8
  class NativeAudioRecorder {
8
- private var inputNodeId: String? = null
9
+ private var recorderId: String? = null
9
10
 
10
11
  @DoNotStrip
11
12
  fun start() {
12
- this.inputNodeId = MediaSessionManager.attachAudioRecorder(this)
13
- MediaSessionManager.startForegroundServiceIfNecessary()
13
+ if (recorderId == null) {
14
+ recorderId = UUID.randomUUID().toString()
15
+ ForegroundServiceManager.subscribe("recorder_$recorderId")
16
+ }
14
17
  }
15
18
 
16
19
  @DoNotStrip
17
20
  fun stop() {
18
- this.inputNodeId?.let {
19
- MediaSessionManager.detachAudioRecorder(it)
20
- this.inputNodeId = null
21
+ recorderId?.let {
22
+ ForegroundServiceManager.unsubscribe("recorder_$it")
23
+ recorderId = null
21
24
  }
22
- MediaSessionManager.stopForegroundServiceIfNecessary()
23
25
  }
24
26
  }
@@ -11,52 +11,37 @@ import java.util.HashMap
11
11
  class AudioFocusListener(
12
12
  private val audioManager: WeakReference<AudioManager>,
13
13
  private val audioAPIModule: WeakReference<AudioAPIModule>,
14
- private val lockScreenManager: WeakReference<LockScreenManager>,
15
14
  ) : AudioManager.OnAudioFocusChangeListener {
16
- private var playOnAudioFocus: Boolean = false
17
15
  private var focusRequest: AudioFocusRequest? = null
18
16
 
19
17
  override fun onAudioFocusChange(focusChange: Int) {
20
18
  Log.d("AudioFocusListener", "onAudioFocusChange: $focusChange")
21
19
  when (focusChange) {
22
20
  AudioManager.AUDIOFOCUS_LOSS -> {
23
- playOnAudioFocus = false
24
21
  val body =
25
22
  HashMap<String, Any>().apply {
26
23
  put("type", "began")
27
- put("shouldResume", false)
24
+ put("isTransient", false)
28
25
  }
29
26
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
30
27
  }
31
28
 
32
29
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
33
- playOnAudioFocus = lockScreenManager.get()?.isPlaying == true
34
30
  val body =
35
31
  HashMap<String, Any>().apply {
36
32
  put("type", "began")
37
- put("shouldResume", playOnAudioFocus)
33
+ put("isTransient", true)
38
34
  }
39
35
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
40
36
  }
41
37
 
42
38
  AudioManager.AUDIOFOCUS_GAIN -> {
43
- if (playOnAudioFocus) {
44
- val body =
45
- HashMap<String, Any>().apply {
46
- put("type", "ended")
47
- put("shouldResume", true)
48
- }
49
- audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
50
- } else {
51
- val body =
52
- HashMap<String, Any>().apply {
53
- put("type", "ended")
54
- put("shouldResume", false)
55
- }
56
- audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
57
- }
58
-
59
- playOnAudioFocus = false
39
+ val body =
40
+ HashMap<String, Any>().apply {
41
+ put("type", "ended")
42
+ put("isTransient", false)
43
+ }
44
+ audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
60
45
  }
61
46
  }
62
47
  }
@@ -0,0 +1,127 @@
1
+ package com.swmansion.audioapi.system
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.Service
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.ServiceInfo
10
+ import android.os.Build
11
+ import android.os.IBinder
12
+ import android.util.Log
13
+ import androidx.core.app.NotificationCompat
14
+ import com.swmansion.audioapi.system.MediaSessionManager.CHANNEL_ID
15
+ import com.swmansion.audioapi.system.notification.NotificationRegistry
16
+
17
+ /**
18
+ * Centralized foreground service that can be used by any component that needs foreground capabilities.
19
+ */
20
+ class CentralizedForegroundService : Service() {
21
+ companion object {
22
+ private const val TAG = "CentralizedForegroundService"
23
+ private const val NOTIFICATION_ID = 100
24
+ const val ACTION_START = "START_FOREGROUND"
25
+ const val ACTION_STOP = "STOP_FOREGROUND"
26
+ }
27
+
28
+ override fun onBind(intent: Intent?): IBinder? = null
29
+
30
+ override fun onStartCommand(
31
+ intent: Intent?,
32
+ flags: Int,
33
+ startId: Int,
34
+ ): Int {
35
+ when (intent?.action) {
36
+ ACTION_START -> {
37
+ startForegroundWithNotification()
38
+ }
39
+ ACTION_STOP -> {
40
+ stopForeground(STOP_FOREGROUND_REMOVE)
41
+ stopSelf()
42
+ }
43
+ }
44
+ return START_NOT_STICKY
45
+ }
46
+
47
+ private fun startForegroundWithNotification() {
48
+ try {
49
+ createNotificationChannelIfNeeded()
50
+
51
+ // Try to use an existing notification first
52
+ val existingNotification = findExistingNotification()
53
+ val (notificationId, notification) =
54
+ if (existingNotification != null) {
55
+ existingNotification
56
+ } else {
57
+ // Fallback to default service notification
58
+ NOTIFICATION_ID to createServiceNotification()
59
+ }
60
+
61
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
62
+ startForeground(
63
+ notificationId,
64
+ notification,
65
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
66
+ )
67
+ } else {
68
+ startForeground(notificationId, notification)
69
+ }
70
+
71
+ Log.d(TAG, "Centralized foreground service started with notification ID: $notificationId")
72
+ } catch (e: Exception) {
73
+ Log.e(TAG, "Error starting foreground service: ${e.message}", e)
74
+ }
75
+ }
76
+
77
+ private fun findExistingNotification(): Pair<Int, Notification>? {
78
+ // Check for recording notification first (priority)
79
+ NotificationRegistry.getBuiltNotification(101)?.let {
80
+ return 101 to it
81
+ }
82
+
83
+ // Check for playback notification
84
+ NotificationRegistry.getBuiltNotification(100)?.let {
85
+ return 100 to it
86
+ }
87
+
88
+ return null
89
+ }
90
+
91
+ private fun createServiceNotification(): Notification =
92
+ NotificationCompat
93
+ .Builder(this, CHANNEL_ID)
94
+ .setContentTitle("Audio Service")
95
+ .setContentText("Audio processing in progress")
96
+ .setSmallIcon(android.R.drawable.ic_btn_speak_now)
97
+ .setPriority(NotificationCompat.PRIORITY_LOW)
98
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
99
+ .setOngoing(true)
100
+ .setAutoCancel(false)
101
+ .build()
102
+
103
+ private fun createNotificationChannelIfNeeded() {
104
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
105
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
106
+
107
+ if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) {
108
+ val channel =
109
+ NotificationChannel(
110
+ CHANNEL_ID,
111
+ "Audio Service",
112
+ NotificationManager.IMPORTANCE_LOW,
113
+ ).apply {
114
+ description = "Background audio processing"
115
+ setShowBadge(false)
116
+ lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
117
+ }
118
+ notificationManager.createNotificationChannel(channel)
119
+ }
120
+ }
121
+ }
122
+
123
+ override fun onDestroy() {
124
+ Log.d(TAG, "Centralized foreground service destroyed")
125
+ super.onDestroy()
126
+ }
127
+ }
@@ -0,0 +1,116 @@
1
+ package com.swmansion.audioapi.system
2
+
3
+ import android.content.Intent
4
+ import android.os.Build
5
+ import android.util.Log
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import java.lang.ref.WeakReference
8
+
9
+ /**
10
+ * Centralized manager for foreground service lifecycle.
11
+ * Handles starting/stopping foreground service based on active subscribers.
12
+ */
13
+ object ForegroundServiceManager {
14
+ private const val TAG = "ForegroundServiceManager"
15
+
16
+ private lateinit var reactContext: WeakReference<ReactApplicationContext>
17
+ private val subscribers = mutableSetOf<String>()
18
+ private var isServiceRunning = false
19
+
20
+ fun initialize(reactContext: WeakReference<ReactApplicationContext>) {
21
+ this.reactContext = reactContext
22
+ }
23
+
24
+ /**
25
+ * Subscribe to foreground service. Service will start if not already running.
26
+ * @param subscriberId Unique identifier for the subscriber
27
+ */
28
+ @Synchronized
29
+ fun subscribe(subscriberId: String) {
30
+ if (subscribers.add(subscriberId)) {
31
+ Log.d(TAG, "Subscriber added: $subscriberId (total: ${subscribers.size})")
32
+ startServiceIfNeeded()
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Unsubscribe from foreground service. Service will stop if no more subscribers.
38
+ * @param subscriberId Unique identifier for the subscriber
39
+ */
40
+ @Synchronized
41
+ fun unsubscribe(subscriberId: String) {
42
+ if (subscribers.remove(subscriberId)) {
43
+ Log.d(TAG, "Subscriber removed: $subscriberId (total: ${subscribers.size})")
44
+ stopServiceIfNotNeeded()
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get count of active subscribers
50
+ */
51
+ fun getSubscriberCount(): Int = subscribers.size
52
+
53
+ /**
54
+ * Check if service is currently running
55
+ */
56
+ fun isServiceRunning(): Boolean = isServiceRunning
57
+
58
+ private fun startServiceIfNeeded() {
59
+ if (!isServiceRunning && subscribers.isNotEmpty()) {
60
+ startForegroundService()
61
+ }
62
+ }
63
+
64
+ private fun stopServiceIfNotNeeded() {
65
+ if (isServiceRunning && subscribers.isEmpty()) {
66
+ stopForegroundService()
67
+ }
68
+ }
69
+
70
+ private fun startForegroundService() {
71
+ val context = reactContext.get() ?: return
72
+
73
+ try {
74
+ val intent = Intent(context, CentralizedForegroundService::class.java)
75
+ intent.action = CentralizedForegroundService.ACTION_START
76
+
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
78
+ context.startForegroundService(intent)
79
+ } else {
80
+ context.startService(intent)
81
+ }
82
+
83
+ isServiceRunning = true
84
+ Log.d(TAG, "Centralized foreground service started")
85
+ } catch (e: Exception) {
86
+ Log.e(TAG, "Error starting foreground service: ${e.message}", e)
87
+ }
88
+ }
89
+
90
+ private fun stopForegroundService() {
91
+ val context = reactContext.get() ?: return
92
+
93
+ try {
94
+ val intent = Intent(context, CentralizedForegroundService::class.java)
95
+ intent.action = CentralizedForegroundService.ACTION_STOP
96
+
97
+ context.startService(intent)
98
+ isServiceRunning = false
99
+ Log.d(TAG, "Centralized foreground service stopped")
100
+ } catch (e: Exception) {
101
+ Log.e(TAG, "Error stopping foreground service: ${e.message}", e)
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Cleanup all subscribers and stop service
107
+ */
108
+ fun cleanup() {
109
+ synchronized(this) {
110
+ subscribers.clear()
111
+ if (isServiceRunning) {
112
+ stopForegroundService()
113
+ }
114
+ }
115
+ }
116
+ }