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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +164 -16
  2. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioPlayer.kt +10 -8
  3. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioRecorder.kt +10 -8
  4. package/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +5 -2
  5. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +128 -0
  6. package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +116 -0
  7. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +115 -107
  8. package/android/src/main/java/com/swmansion/audioapi/system/PermissionRequestListener.kt +2 -1
  9. package/android/src/main/java/com/swmansion/audioapi/system/notification/BaseNotification.kt +47 -0
  10. package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +191 -0
  11. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +669 -0
  12. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +33 -0
  13. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +303 -0
  14. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +45 -0
  15. package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +119 -0
  16. package/common/cpp/audioapi/utils/AudioFileProperties.h +17 -17
  17. package/ios/audioapi/ios/AudioAPIModule.h +2 -2
  18. package/ios/audioapi/ios/AudioAPIModule.mm +108 -18
  19. package/ios/audioapi/ios/core/NativeAudioRecorder.m +8 -1
  20. package/ios/audioapi/ios/system/AudioEngine.mm +2 -2
  21. package/ios/audioapi/ios/system/AudioSessionManager.mm +12 -9
  22. package/ios/audioapi/ios/system/NotificationManager.mm +1 -1
  23. package/ios/audioapi/ios/system/notification/BaseNotification.h +58 -0
  24. package/ios/audioapi/ios/system/notification/NotificationRegistry.h +70 -0
  25. package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +172 -0
  26. package/ios/audioapi/ios/system/notification/PlaybackNotification.h +27 -0
  27. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +427 -0
  28. package/lib/commonjs/api.js +72 -1
  29. package/lib/commonjs/api.js.map +1 -1
  30. package/lib/commonjs/api.web.js +27 -14
  31. package/lib/commonjs/api.web.js.map +1 -1
  32. package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
  33. package/lib/commonjs/system/AudioManager.js +6 -9
  34. package/lib/commonjs/system/AudioManager.js.map +1 -1
  35. package/lib/commonjs/system/index.js +13 -0
  36. package/lib/commonjs/system/index.js.map +1 -1
  37. package/lib/commonjs/system/notification/PlaybackNotificationManager.js +135 -0
  38. package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -0
  39. package/lib/commonjs/system/notification/RecordingNotificationManager.js +182 -0
  40. package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -0
  41. package/lib/commonjs/system/notification/SimpleNotificationManager.js +122 -0
  42. package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +1 -0
  43. package/lib/commonjs/system/notification/index.js +45 -0
  44. package/lib/commonjs/system/notification/index.js.map +1 -0
  45. package/lib/commonjs/system/notification/types.js +6 -0
  46. package/lib/commonjs/system/notification/types.js.map +1 -0
  47. package/lib/commonjs/types.js +17 -17
  48. package/lib/commonjs/types.js.map +1 -1
  49. package/lib/commonjs/web-system/index.js +17 -0
  50. package/lib/commonjs/web-system/index.js.map +1 -0
  51. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +34 -0
  52. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  53. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +34 -0
  54. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -0
  55. package/lib/commonjs/web-system/notification/index.js +21 -0
  56. package/lib/commonjs/web-system/notification/index.js.map +1 -0
  57. package/lib/module/api.js +4 -0
  58. package/lib/module/api.js.map +1 -1
  59. package/lib/module/api.web.js +3 -1
  60. package/lib/module/api.web.js.map +1 -1
  61. package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
  62. package/lib/module/system/AudioManager.js +6 -9
  63. package/lib/module/system/AudioManager.js.map +1 -1
  64. package/lib/module/system/index.js +1 -0
  65. package/lib/module/system/index.js.map +1 -1
  66. package/lib/module/system/notification/PlaybackNotificationManager.js +131 -0
  67. package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -0
  68. package/lib/module/system/notification/RecordingNotificationManager.js +178 -0
  69. package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -0
  70. package/lib/module/system/notification/SimpleNotificationManager.js +118 -0
  71. package/lib/module/system/notification/SimpleNotificationManager.js.map +1 -0
  72. package/lib/module/system/notification/index.js +7 -0
  73. package/lib/module/system/notification/index.js.map +1 -0
  74. package/lib/module/system/notification/types.js +4 -0
  75. package/lib/module/system/notification/types.js.map +1 -0
  76. package/lib/module/types.js +17 -17
  77. package/lib/module/types.js.map +1 -1
  78. package/lib/module/web-system/index.js +4 -0
  79. package/lib/module/web-system/index.js.map +1 -0
  80. package/lib/module/web-system/notification/PlaybackNotificationManager.js +30 -0
  81. package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -0
  82. package/lib/module/web-system/notification/RecordingNotificationManager.js +30 -0
  83. package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -0
  84. package/lib/module/web-system/notification/index.js +5 -0
  85. package/lib/module/web-system/notification/index.js.map +1 -0
  86. package/lib/typescript/api.d.ts +2 -0
  87. package/lib/typescript/api.d.ts.map +1 -1
  88. package/lib/typescript/api.web.d.ts +3 -1
  89. package/lib/typescript/api.web.d.ts.map +1 -1
  90. package/lib/typescript/events/types.d.ts +2 -2
  91. package/lib/typescript/events/types.d.ts.map +1 -1
  92. package/lib/typescript/specs/NativeAudioAPIModule.d.ts +16 -5
  93. package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
  94. package/lib/typescript/system/AudioManager.d.ts +4 -5
  95. package/lib/typescript/system/AudioManager.d.ts.map +1 -1
  96. package/lib/typescript/system/index.d.ts +1 -0
  97. package/lib/typescript/system/index.d.ts.map +1 -1
  98. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +22 -0
  99. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  100. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +23 -0
  101. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -0
  102. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +20 -0
  103. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +1 -0
  104. package/lib/typescript/system/notification/index.d.ts +5 -0
  105. package/lib/typescript/system/notification/index.d.ts.map +1 -0
  106. package/lib/typescript/system/notification/types.d.ts +65 -0
  107. package/lib/typescript/system/notification/types.d.ts.map +1 -0
  108. package/lib/typescript/system/types.d.ts +0 -16
  109. package/lib/typescript/system/types.d.ts.map +1 -1
  110. package/lib/typescript/types.d.ts +16 -16
  111. package/lib/typescript/types.d.ts.map +1 -1
  112. package/lib/typescript/web-system/index.d.ts +2 -0
  113. package/lib/typescript/web-system/index.d.ts.map +1 -0
  114. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +19 -0
  115. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -0
  116. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +19 -0
  117. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -0
  118. package/lib/typescript/web-system/notification/index.d.ts +3 -0
  119. package/lib/typescript/web-system/notification/index.d.ts.map +1 -0
  120. package/package.json +1 -1
  121. package/src/api.ts +17 -0
  122. package/src/api.web.ts +7 -2
  123. package/src/events/types.ts +2 -3
  124. package/src/specs/NativeAudioAPIModule.ts +23 -7
  125. package/src/system/AudioManager.ts +10 -23
  126. package/src/system/index.ts +1 -0
  127. package/src/system/notification/PlaybackNotificationManager.ts +193 -0
  128. package/src/system/notification/RecordingNotificationManager.ts +242 -0
  129. package/src/system/notification/SimpleNotificationManager.ts +170 -0
  130. package/src/system/notification/index.ts +4 -0
  131. package/src/system/notification/types.ts +111 -0
  132. package/src/system/types.ts +0 -18
  133. package/src/types.ts +17 -17
  134. package/src/web-system/index.ts +1 -0
  135. package/src/web-system/notification/PlaybackNotificationManager.ts +60 -0
  136. package/src/web-system/notification/RecordingNotificationManager.ts +60 -0
  137. package/src/web-system/notification/index.ts +2 -0
  138. package/android/src/main/java/com/swmansion/audioapi/system/LockScreenManager.kt +0 -347
  139. package/android/src/main/java/com/swmansion/audioapi/system/MediaNotificationManager.kt +0 -273
  140. package/android/src/main/java/com/swmansion/audioapi/system/MediaReceiver.kt +0 -57
  141. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionCallback.kt +0 -61
@@ -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.NativeFileInfo
14
16
  import com.swmansion.audioapi.system.PermissionRequestListener
@@ -86,7 +88,8 @@ class AudioAPIModule(
86
88
 
87
89
  override fun invalidate() {
88
90
  reactContext.get()?.removeLifecycleEventListener(this)
89
- // think about cleaning up resources, singletons etc.
91
+ // Cleanup foreground service manager
92
+ ForegroundServiceManager.cleanup()
90
93
  }
91
94
 
92
95
  override fun getDevicePreferredSampleRate(): Double = MediaSessionManager.getDevicePreferredSampleRate()
@@ -111,21 +114,6 @@ class AudioAPIModule(
111
114
  // nothing to do here
112
115
  }
113
116
 
114
- override fun setLockScreenInfo(info: ReadableMap?) {
115
- MediaSessionManager.setLockScreenInfo(info)
116
- }
117
-
118
- override fun resetLockScreenInfo() {
119
- MediaSessionManager.resetLockScreenInfo()
120
- }
121
-
122
- override fun enableRemoteCommand(
123
- name: String?,
124
- enabled: Boolean,
125
- ) {
126
- MediaSessionManager.enableRemoteCommand(name!!, enabled)
127
- }
128
-
129
117
  override fun observeAudioInterruptions(enabled: Boolean) {
130
118
  MediaSessionManager.observeAudioInterruptions(enabled)
131
119
  }
@@ -147,7 +135,167 @@ class AudioAPIModule(
147
135
  promise.resolve(MediaSessionManager.checkRecordingPermissions())
148
136
  }
149
137
 
138
+ override fun requestNotificationPermissions(promise: Promise) {
139
+ val permissionRequestListener = PermissionRequestListener(promise)
140
+ MediaSessionManager.requestNotificationPermissions(permissionRequestListener)
141
+ }
142
+
143
+ override fun checkNotificationPermissions(promise: Promise) {
144
+ promise.resolve(MediaSessionManager.checkNotificationPermissions())
145
+ }
146
+
150
147
  override fun getDevicesInfo(promise: Promise) {
151
148
  promise.resolve(MediaSessionManager.getDevicesInfo())
152
149
  }
150
+
151
+ // New notification system methods
152
+ override fun registerNotification(
153
+ type: String?,
154
+ key: String?,
155
+ promise: Promise?,
156
+ ) {
157
+ try {
158
+ if (type == null || key == null) {
159
+ val result = Arguments.createMap()
160
+ result.putBoolean("success", false)
161
+ result.putString("error", "Type and key are required")
162
+ promise?.resolve(result)
163
+ return
164
+ }
165
+
166
+ MediaSessionManager.registerNotification(type, key)
167
+
168
+ val result = Arguments.createMap()
169
+ result.putBoolean("success", true)
170
+ promise?.resolve(result)
171
+ } catch (e: Exception) {
172
+ val result = Arguments.createMap()
173
+ result.putBoolean("success", false)
174
+ result.putString("error", e.message ?: "Unknown error")
175
+ promise?.resolve(result)
176
+ }
177
+ }
178
+
179
+ override fun showNotification(
180
+ key: String?,
181
+ options: ReadableMap?,
182
+ promise: Promise?,
183
+ ) {
184
+ try {
185
+ if (key == null) {
186
+ val result = Arguments.createMap()
187
+ result.putBoolean("success", false)
188
+ result.putString("error", "Key is required")
189
+ promise?.resolve(result)
190
+ return
191
+ }
192
+
193
+ MediaSessionManager.showNotification(key, options)
194
+
195
+ val result = Arguments.createMap()
196
+ result.putBoolean("success", true)
197
+ promise?.resolve(result)
198
+ } catch (e: Exception) {
199
+ val result = Arguments.createMap()
200
+ result.putBoolean("success", false)
201
+ result.putString("error", e.message ?: "Unknown error")
202
+ promise?.resolve(result)
203
+ }
204
+ }
205
+
206
+ override fun updateNotification(
207
+ key: String?,
208
+ options: ReadableMap?,
209
+ promise: Promise?,
210
+ ) {
211
+ try {
212
+ if (key == null) {
213
+ val result = Arguments.createMap()
214
+ result.putBoolean("success", false)
215
+ result.putString("error", "Key is required")
216
+ promise?.resolve(result)
217
+ return
218
+ }
219
+
220
+ MediaSessionManager.updateNotification(key, options)
221
+
222
+ val result = Arguments.createMap()
223
+ result.putBoolean("success", true)
224
+ promise?.resolve(result)
225
+ } catch (e: Exception) {
226
+ val result = Arguments.createMap()
227
+ result.putBoolean("success", false)
228
+ result.putString("error", e.message ?: "Unknown error")
229
+ promise?.resolve(result)
230
+ }
231
+ }
232
+
233
+ override fun hideNotification(
234
+ key: String?,
235
+ promise: Promise?,
236
+ ) {
237
+ try {
238
+ if (key == null) {
239
+ val result = Arguments.createMap()
240
+ result.putBoolean("success", false)
241
+ result.putString("error", "Key is required")
242
+ promise?.resolve(result)
243
+ return
244
+ }
245
+
246
+ MediaSessionManager.hideNotification(key)
247
+
248
+ val result = Arguments.createMap()
249
+ result.putBoolean("success", true)
250
+ promise?.resolve(result)
251
+ } catch (e: Exception) {
252
+ val result = Arguments.createMap()
253
+ result.putBoolean("success", false)
254
+ result.putString("error", e.message ?: "Unknown error")
255
+ promise?.resolve(result)
256
+ }
257
+ }
258
+
259
+ override fun unregisterNotification(
260
+ key: String?,
261
+ promise: Promise?,
262
+ ) {
263
+ try {
264
+ if (key == null) {
265
+ val result = Arguments.createMap()
266
+ result.putBoolean("success", false)
267
+ result.putString("error", "Key is required")
268
+ promise?.resolve(result)
269
+ return
270
+ }
271
+
272
+ MediaSessionManager.unregisterNotification(key)
273
+
274
+ val result = Arguments.createMap()
275
+ result.putBoolean("success", true)
276
+ promise?.resolve(result)
277
+ } catch (e: Exception) {
278
+ val result = Arguments.createMap()
279
+ result.putBoolean("success", false)
280
+ result.putString("error", e.message ?: "Unknown error")
281
+ promise?.resolve(result)
282
+ }
283
+ }
284
+
285
+ override fun isNotificationActive(
286
+ key: String?,
287
+ promise: Promise?,
288
+ ) {
289
+ try {
290
+ if (key == null) {
291
+ promise?.resolve(false)
292
+ return
293
+ }
294
+
295
+ val isActive = MediaSessionManager.isNotificationActive(key)
296
+ promise?.resolve(isActive)
297
+ } catch (e: Exception) {
298
+ promise?.resolve(false)
299
+ }
300
+ }
153
301
  }
@@ -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,9 +11,9 @@ 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
15
  private var focusRequest: AudioFocusRequest? = null
16
+ private var isTransientLoss: Boolean = false
17
17
 
18
18
  override fun onAudioFocusChange(focusChange: Int) {
19
19
  Log.d("AudioFocusListener", "onAudioFocusChange: $focusChange")
@@ -23,6 +23,7 @@ class AudioFocusListener(
23
23
  HashMap<String, Any>().apply {
24
24
  put("type", "began")
25
25
  put("shouldResume", false)
26
+ isTransientLoss = false
26
27
  }
27
28
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
28
29
  }
@@ -32,6 +33,7 @@ class AudioFocusListener(
32
33
  HashMap<String, Any>().apply {
33
34
  put("type", "began")
34
35
  put("shouldResume", false)
36
+ isTransientLoss = true
35
37
  }
36
38
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
37
39
  }
@@ -40,7 +42,8 @@ class AudioFocusListener(
40
42
  val body =
41
43
  HashMap<String, Any>().apply {
42
44
  put("type", "ended")
43
- put("shouldResume", true)
45
+ put("shouldResume", isTransientLoss)
46
+ isTransientLoss = false
44
47
  }
45
48
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body)
46
49
  }
@@ -0,0 +1,128 @@
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
+
40
+ ACTION_STOP -> {
41
+ stopForeground(STOP_FOREGROUND_REMOVE)
42
+ stopSelf()
43
+ }
44
+ }
45
+ return START_NOT_STICKY
46
+ }
47
+
48
+ private fun startForegroundWithNotification() {
49
+ try {
50
+ createNotificationChannelIfNeeded()
51
+
52
+ // Try to use an existing notification first
53
+ val existingNotification = findExistingNotification()
54
+ val (notificationId, notification) =
55
+ if (existingNotification != null) {
56
+ existingNotification
57
+ } else {
58
+ // Fallback to default service notification
59
+ NOTIFICATION_ID to createServiceNotification()
60
+ }
61
+
62
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
63
+ startForeground(
64
+ notificationId,
65
+ notification,
66
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
67
+ )
68
+ } else {
69
+ startForeground(notificationId, notification)
70
+ }
71
+
72
+ Log.d(TAG, "Centralized foreground service started with notification ID: $notificationId")
73
+ } catch (e: Exception) {
74
+ Log.e(TAG, "Error starting foreground service: ${e.message}", e)
75
+ }
76
+ }
77
+
78
+ private fun findExistingNotification(): Pair<Int, Notification>? {
79
+ // Check for recording notification first (priority)
80
+ NotificationRegistry.getBuiltNotification(101)?.let {
81
+ return 101 to it
82
+ }
83
+
84
+ // Check for playback notification
85
+ NotificationRegistry.getBuiltNotification(100)?.let {
86
+ return 100 to it
87
+ }
88
+
89
+ return null
90
+ }
91
+
92
+ private fun createServiceNotification(): Notification =
93
+ NotificationCompat
94
+ .Builder(this, CHANNEL_ID)
95
+ .setContentTitle("Audio Service")
96
+ .setContentText("Audio processing in progress")
97
+ .setSmallIcon(android.R.drawable.ic_btn_speak_now)
98
+ .setPriority(NotificationCompat.PRIORITY_LOW)
99
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
100
+ .setOngoing(true)
101
+ .setAutoCancel(false)
102
+ .build()
103
+
104
+ private fun createNotificationChannelIfNeeded() {
105
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
106
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
107
+
108
+ if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) {
109
+ val channel =
110
+ NotificationChannel(
111
+ CHANNEL_ID,
112
+ "Audio Service",
113
+ NotificationManager.IMPORTANCE_LOW,
114
+ ).apply {
115
+ description = "Background audio processing"
116
+ setShowBadge(false)
117
+ lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
118
+ }
119
+ notificationManager.createNotificationChannel(channel)
120
+ }
121
+ }
122
+ }
123
+
124
+ override fun onDestroy() {
125
+ Log.d(TAG, "Centralized foreground service destroyed")
126
+ super.onDestroy()
127
+ }
128
+ }
@@ -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
+ }