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.
- package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +164 -16
- package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioPlayer.kt +10 -8
- package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioRecorder.kt +10 -8
- package/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +8 -23
- package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +127 -0
- package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +116 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +115 -107
- package/android/src/main/java/com/swmansion/audioapi/system/PermissionRequestListener.kt +2 -1
- package/android/src/main/java/com/swmansion/audioapi/system/notification/BaseNotification.kt +47 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +191 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +668 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +33 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +303 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +43 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +119 -0
- package/ios/audioapi/ios/AudioAPIModule.h +2 -2
- package/ios/audioapi/ios/AudioAPIModule.mm +108 -18
- package/ios/audioapi/ios/system/AudioEngine.mm +2 -2
- package/ios/audioapi/ios/system/AudioSessionManager.mm +1 -1
- package/ios/audioapi/ios/system/NotificationManager.mm +1 -1
- package/ios/audioapi/ios/system/notification/BaseNotification.h +58 -0
- package/ios/audioapi/ios/system/notification/NotificationRegistry.h +70 -0
- package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +172 -0
- package/ios/audioapi/ios/system/notification/PlaybackNotification.h +27 -0
- package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +427 -0
- package/lib/commonjs/api.js +59 -10
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/api.web.js +27 -14
- package/lib/commonjs/api.web.js.map +1 -1
- package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
- package/lib/commonjs/system/AudioManager.js +6 -9
- package/lib/commonjs/system/AudioManager.js.map +1 -1
- package/lib/commonjs/system/index.js +13 -0
- package/lib/commonjs/system/index.js.map +1 -1
- package/lib/commonjs/system/notification/PlaybackNotificationManager.js +135 -0
- package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -0
- package/lib/commonjs/system/notification/RecordingNotificationManager.js +182 -0
- package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -0
- package/lib/commonjs/system/notification/SimpleNotificationManager.js +122 -0
- package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +1 -0
- package/lib/commonjs/system/notification/index.js +45 -0
- package/lib/commonjs/system/notification/index.js.map +1 -0
- package/lib/commonjs/system/notification/types.js +6 -0
- package/lib/commonjs/system/notification/types.js.map +1 -0
- package/lib/commonjs/web-system/index.js +17 -0
- package/lib/commonjs/web-system/index.js.map +1 -0
- package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +34 -0
- package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -0
- package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +34 -0
- package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -0
- package/lib/commonjs/web-system/notification/index.js +21 -0
- package/lib/commonjs/web-system/notification/index.js.map +1 -0
- package/lib/module/api.js +5 -1
- package/lib/module/api.js.map +1 -1
- package/lib/module/api.web.js +3 -1
- package/lib/module/api.web.js.map +1 -1
- package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
- package/lib/module/system/AudioManager.js +6 -9
- package/lib/module/system/AudioManager.js.map +1 -1
- package/lib/module/system/index.js +1 -0
- package/lib/module/system/index.js.map +1 -1
- package/lib/module/system/notification/PlaybackNotificationManager.js +131 -0
- package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -0
- package/lib/module/system/notification/RecordingNotificationManager.js +178 -0
- package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -0
- package/lib/module/system/notification/SimpleNotificationManager.js +118 -0
- package/lib/module/system/notification/SimpleNotificationManager.js.map +1 -0
- package/lib/module/system/notification/index.js +7 -0
- package/lib/module/system/notification/index.js.map +1 -0
- package/lib/module/system/notification/types.js +4 -0
- package/lib/module/system/notification/types.js.map +1 -0
- package/lib/module/web-system/index.js +4 -0
- package/lib/module/web-system/index.js.map +1 -0
- package/lib/module/web-system/notification/PlaybackNotificationManager.js +30 -0
- package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -0
- package/lib/module/web-system/notification/RecordingNotificationManager.js +30 -0
- package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -0
- package/lib/module/web-system/notification/index.js +5 -0
- package/lib/module/web-system/notification/index.js.map +1 -0
- package/lib/typescript/api.d.ts +3 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/api.web.d.ts +3 -1
- package/lib/typescript/api.web.d.ts.map +1 -1
- package/lib/typescript/events/types.d.ts +4 -18
- package/lib/typescript/events/types.d.ts.map +1 -1
- package/lib/typescript/specs/NativeAudioAPIModule.d.ts +16 -5
- package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
- package/lib/typescript/system/AudioManager.d.ts +4 -5
- package/lib/typescript/system/AudioManager.d.ts.map +1 -1
- package/lib/typescript/system/index.d.ts +1 -0
- package/lib/typescript/system/index.d.ts.map +1 -1
- package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +22 -0
- package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -0
- package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +23 -0
- package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -0
- package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +20 -0
- package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +1 -0
- package/lib/typescript/system/notification/index.d.ts +5 -0
- package/lib/typescript/system/notification/index.d.ts.map +1 -0
- package/lib/typescript/system/notification/types.d.ts +65 -0
- package/lib/typescript/system/notification/types.d.ts.map +1 -0
- package/lib/typescript/system/types.d.ts +0 -16
- package/lib/typescript/system/types.d.ts.map +1 -1
- package/lib/typescript/web-system/index.d.ts +2 -0
- package/lib/typescript/web-system/index.d.ts.map +1 -0
- package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +19 -0
- package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -0
- package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +19 -0
- package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -0
- package/lib/typescript/web-system/notification/index.d.ts +3 -0
- package/lib/typescript/web-system/notification/index.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api.ts +17 -2
- package/src/api.web.ts +7 -2
- package/src/events/types.ts +4 -20
- package/src/specs/NativeAudioAPIModule.ts +23 -7
- package/src/system/AudioManager.ts +10 -23
- package/src/system/index.ts +1 -0
- package/src/system/notification/PlaybackNotificationManager.ts +193 -0
- package/src/system/notification/RecordingNotificationManager.ts +242 -0
- package/src/system/notification/SimpleNotificationManager.ts +170 -0
- package/src/system/notification/index.ts +4 -0
- package/src/system/notification/types.ts +110 -0
- package/src/system/types.ts +0 -18
- package/src/web-system/index.ts +1 -0
- package/src/web-system/notification/PlaybackNotificationManager.ts +60 -0
- package/src/web-system/notification/RecordingNotificationManager.ts +60 -0
- package/src/web-system/notification/index.ts +2 -0
- package/android/src/main/java/com/swmansion/audioapi/system/LockScreenManager.kt +0 -347
- package/android/src/main/java/com/swmansion/audioapi/system/MediaNotificationManager.kt +0 -273
- package/android/src/main/java/com/swmansion/audioapi/system/MediaReceiver.kt +0 -57
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionCallback.kt +0 -61
- package/ios/audioapi/ios/system/LockScreenManager.h +0 -23
- package/ios/audioapi/ios/system/LockScreenManager.mm +0 -314
package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system.notification
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.content.IntentFilter
|
|
10
|
+
import android.graphics.Color
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import androidx.core.app.NotificationCompat
|
|
14
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
15
|
+
import com.facebook.react.bridge.ReadableMap
|
|
16
|
+
import com.swmansion.audioapi.AudioAPIModule
|
|
17
|
+
import java.lang.ref.WeakReference
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* RecordingNotification
|
|
21
|
+
*
|
|
22
|
+
* Simple notification for audio recording:
|
|
23
|
+
* - Shows recording status with red background when recording
|
|
24
|
+
* - Simple start/stop button with microphone icon
|
|
25
|
+
* - Is persistent and cannot be swiped away when recording
|
|
26
|
+
* - Notifies its dismissal via RecordingNotificationReceiver
|
|
27
|
+
*/
|
|
28
|
+
class RecordingNotification(
|
|
29
|
+
private val reactContext: WeakReference<ReactApplicationContext>,
|
|
30
|
+
private val audioAPIModule: WeakReference<AudioAPIModule>,
|
|
31
|
+
private val notificationId: Int,
|
|
32
|
+
private val channelId: String,
|
|
33
|
+
) : BaseNotification {
|
|
34
|
+
companion object {
|
|
35
|
+
private const val TAG = "RecordingNotification"
|
|
36
|
+
const val ACTION_START = "com.swmansion.audioapi.RECORDING_START"
|
|
37
|
+
const val ACTION_STOP = "com.swmansion.audioapi.RECORDING_STOP"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private var notificationBuilder: NotificationCompat.Builder? = null
|
|
41
|
+
private var isRecording: Boolean = false
|
|
42
|
+
private var title: String = "Audio Recording"
|
|
43
|
+
private var description: String = "Ready to record"
|
|
44
|
+
private var receiver: RecordingNotificationReceiver? = null
|
|
45
|
+
private var startEnabled: Boolean = true
|
|
46
|
+
private var stopEnabled: Boolean = true
|
|
47
|
+
|
|
48
|
+
override fun init(params: ReadableMap?): Notification {
|
|
49
|
+
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
50
|
+
|
|
51
|
+
// Register broadcast receiver
|
|
52
|
+
registerReceiver()
|
|
53
|
+
|
|
54
|
+
// Create notification channel first
|
|
55
|
+
createNotificationChannel()
|
|
56
|
+
|
|
57
|
+
// Create notification builder
|
|
58
|
+
notificationBuilder =
|
|
59
|
+
NotificationCompat
|
|
60
|
+
.Builder(context, channelId)
|
|
61
|
+
.setSmallIcon(android.R.drawable.ic_btn_speak_now)
|
|
62
|
+
.setContentTitle(title)
|
|
63
|
+
.setContentText(description)
|
|
64
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
65
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
66
|
+
.setOngoing(false)
|
|
67
|
+
.setAutoCancel(false)
|
|
68
|
+
|
|
69
|
+
// Set content intent to open app
|
|
70
|
+
val packageName = context.packageName
|
|
71
|
+
val openAppIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
72
|
+
if (openAppIntent != null) {
|
|
73
|
+
val pendingIntent =
|
|
74
|
+
PendingIntent.getActivity(
|
|
75
|
+
context,
|
|
76
|
+
0,
|
|
77
|
+
openAppIntent,
|
|
78
|
+
PendingIntent.FLAG_IMMUTABLE,
|
|
79
|
+
)
|
|
80
|
+
notificationBuilder?.setContentIntent(pendingIntent)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Set delete intent to handle dismissal
|
|
84
|
+
val deleteIntent = Intent(RecordingNotificationReceiver.ACTION_NOTIFICATION_DISMISSED)
|
|
85
|
+
deleteIntent.setPackage(context.packageName)
|
|
86
|
+
val deletePendingIntent =
|
|
87
|
+
PendingIntent.getBroadcast(
|
|
88
|
+
context,
|
|
89
|
+
notificationId,
|
|
90
|
+
deleteIntent,
|
|
91
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
92
|
+
)
|
|
93
|
+
notificationBuilder?.setDeleteIntent(deletePendingIntent)
|
|
94
|
+
|
|
95
|
+
// Apply initial params if provided
|
|
96
|
+
if (params != null) {
|
|
97
|
+
update(params)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return buildNotification()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun reset() {
|
|
104
|
+
// Unregister receiver
|
|
105
|
+
unregisterReceiver()
|
|
106
|
+
|
|
107
|
+
// Reset state
|
|
108
|
+
title = "Audio Recording"
|
|
109
|
+
description = "Ready to record"
|
|
110
|
+
isRecording = false
|
|
111
|
+
notificationBuilder = null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override fun getNotificationId(): Int = notificationId
|
|
115
|
+
|
|
116
|
+
override fun getChannelId(): String = channelId
|
|
117
|
+
|
|
118
|
+
override fun update(options: ReadableMap?): Notification {
|
|
119
|
+
if (options == null) {
|
|
120
|
+
return buildNotification()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle control enable/disable
|
|
124
|
+
if (options.hasKey("control") && options.hasKey("enabled")) {
|
|
125
|
+
val control = options.getString("control")
|
|
126
|
+
val enabled = options.getBoolean("enabled")
|
|
127
|
+
when (control) {
|
|
128
|
+
"start" -> startEnabled = enabled
|
|
129
|
+
"stop" -> stopEnabled = enabled
|
|
130
|
+
}
|
|
131
|
+
updateActions()
|
|
132
|
+
return buildNotification()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Update metadata
|
|
136
|
+
if (options.hasKey("title")) {
|
|
137
|
+
title = options.getString("title") ?: "Audio Recording"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.hasKey("description")) {
|
|
141
|
+
description = options.getString("description") ?: "Ready to record"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Update recording state
|
|
145
|
+
if (options.hasKey("state")) {
|
|
146
|
+
when (options.getString("state")) {
|
|
147
|
+
"recording" -> isRecording = true
|
|
148
|
+
"stopped" -> isRecording = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Update notification content
|
|
153
|
+
val statusText =
|
|
154
|
+
description.ifEmpty {
|
|
155
|
+
if (isRecording) "Recording..." else "Ready to record"
|
|
156
|
+
}
|
|
157
|
+
notificationBuilder
|
|
158
|
+
?.setContentTitle(title)
|
|
159
|
+
?.setContentText(statusText)
|
|
160
|
+
?.setOngoing(isRecording)
|
|
161
|
+
|
|
162
|
+
// Set red color when recording
|
|
163
|
+
if (isRecording) {
|
|
164
|
+
notificationBuilder
|
|
165
|
+
?.setColor(Color.RED)
|
|
166
|
+
?.setColorized(true)
|
|
167
|
+
} else {
|
|
168
|
+
notificationBuilder
|
|
169
|
+
?.setColorized(false)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Update action button
|
|
173
|
+
updateActions()
|
|
174
|
+
|
|
175
|
+
return buildNotification()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun buildNotification(): Notification =
|
|
179
|
+
notificationBuilder?.build()
|
|
180
|
+
?: throw IllegalStateException("Notification not initialized. Call init() first.")
|
|
181
|
+
|
|
182
|
+
private fun updateActions() {
|
|
183
|
+
val context = reactContext.get() ?: return
|
|
184
|
+
|
|
185
|
+
// Clear existing actions
|
|
186
|
+
notificationBuilder?.clearActions()
|
|
187
|
+
|
|
188
|
+
// Add appropriate action based on recording state and enabled controls
|
|
189
|
+
// Note: Android shows text labels in collapsed view, icons only in expanded/Auto/Wear
|
|
190
|
+
if (isRecording && stopEnabled) {
|
|
191
|
+
// Show STOP button when recording
|
|
192
|
+
val stopIntent = Intent(ACTION_STOP)
|
|
193
|
+
stopIntent.setPackage(context.packageName)
|
|
194
|
+
val stopPendingIntent =
|
|
195
|
+
PendingIntent.getBroadcast(
|
|
196
|
+
context,
|
|
197
|
+
1001,
|
|
198
|
+
stopIntent,
|
|
199
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
200
|
+
)
|
|
201
|
+
val stopAction =
|
|
202
|
+
NotificationCompat.Action
|
|
203
|
+
.Builder(
|
|
204
|
+
android.R.drawable.ic_delete,
|
|
205
|
+
"Stop",
|
|
206
|
+
stopPendingIntent,
|
|
207
|
+
).build()
|
|
208
|
+
notificationBuilder?.addAction(stopAction)
|
|
209
|
+
} else if (!isRecording && startEnabled) {
|
|
210
|
+
// Show START button when not recording
|
|
211
|
+
val startIntent = Intent(ACTION_START)
|
|
212
|
+
startIntent.setPackage(context.packageName)
|
|
213
|
+
val startPendingIntent =
|
|
214
|
+
PendingIntent.getBroadcast(
|
|
215
|
+
context,
|
|
216
|
+
1000,
|
|
217
|
+
startIntent,
|
|
218
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
219
|
+
)
|
|
220
|
+
val startAction =
|
|
221
|
+
NotificationCompat.Action
|
|
222
|
+
.Builder(
|
|
223
|
+
android.R.drawable.ic_btn_speak_now,
|
|
224
|
+
"Record",
|
|
225
|
+
startPendingIntent,
|
|
226
|
+
).build()
|
|
227
|
+
notificationBuilder?.addAction(startAction)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Use BigTextStyle to ensure actions are visible
|
|
231
|
+
val statusText =
|
|
232
|
+
description.ifEmpty {
|
|
233
|
+
if (isRecording) "Recording in progress..." else "Ready to record"
|
|
234
|
+
}
|
|
235
|
+
notificationBuilder?.setStyle(
|
|
236
|
+
NotificationCompat
|
|
237
|
+
.BigTextStyle()
|
|
238
|
+
.bigText(statusText),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private fun createNotificationChannel() {
|
|
243
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
244
|
+
val context = reactContext.get() ?: return
|
|
245
|
+
|
|
246
|
+
val channel =
|
|
247
|
+
NotificationChannel(
|
|
248
|
+
channelId,
|
|
249
|
+
"Audio Recording",
|
|
250
|
+
NotificationManager.IMPORTANCE_HIGH,
|
|
251
|
+
).apply {
|
|
252
|
+
description = "Recording controls and status"
|
|
253
|
+
setShowBadge(true)
|
|
254
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
255
|
+
enableLights(true)
|
|
256
|
+
lightColor = Color.RED
|
|
257
|
+
enableVibration(false)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
val notificationManager =
|
|
261
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
262
|
+
notificationManager.createNotificationChannel(channel)
|
|
263
|
+
|
|
264
|
+
Log.d(TAG, "Notification channel created: $channelId")
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun registerReceiver() {
|
|
269
|
+
val context = reactContext.get() ?: return
|
|
270
|
+
|
|
271
|
+
if (receiver == null) {
|
|
272
|
+
receiver = RecordingNotificationReceiver()
|
|
273
|
+
RecordingNotificationReceiver.setAudioAPIModule(audioAPIModule.get())
|
|
274
|
+
|
|
275
|
+
val filter = IntentFilter()
|
|
276
|
+
filter.addAction(ACTION_START)
|
|
277
|
+
filter.addAction(ACTION_STOP)
|
|
278
|
+
filter.addAction(RecordingNotificationReceiver.ACTION_NOTIFICATION_DISMISSED)
|
|
279
|
+
|
|
280
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
281
|
+
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
282
|
+
} else {
|
|
283
|
+
context.registerReceiver(receiver, filter)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
Log.d(TAG, "RecordingNotificationReceiver registered")
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private fun unregisterReceiver() {
|
|
291
|
+
val context = reactContext.get() ?: return
|
|
292
|
+
|
|
293
|
+
receiver?.let {
|
|
294
|
+
try {
|
|
295
|
+
context.unregisterReceiver(it)
|
|
296
|
+
receiver = null
|
|
297
|
+
Log.d(TAG, "RecordingNotificationReceiver unregistered")
|
|
298
|
+
} catch (e: Exception) {
|
|
299
|
+
Log.e(TAG, "Error unregistering receiver: ${e.message}", e)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system.notification
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.swmansion.audioapi.AudioAPIModule
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Broadcast receiver for handling recording notification actions and dismissal.
|
|
11
|
+
*/
|
|
12
|
+
class RecordingNotificationReceiver : BroadcastReceiver() {
|
|
13
|
+
companion object {
|
|
14
|
+
const val ACTION_NOTIFICATION_DISMISSED = "com.swmansion.audioapi.RECORDING_NOTIFICATION_DISMISSED"
|
|
15
|
+
private const val TAG = "RecordingNotificationReceiver"
|
|
16
|
+
|
|
17
|
+
private var audioAPIModule: AudioAPIModule? = null
|
|
18
|
+
|
|
19
|
+
fun setAudioAPIModule(module: AudioAPIModule?) {
|
|
20
|
+
audioAPIModule = module
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onReceive(
|
|
25
|
+
context: Context?,
|
|
26
|
+
intent: Intent?,
|
|
27
|
+
) {
|
|
28
|
+
when (intent?.action) {
|
|
29
|
+
ACTION_NOTIFICATION_DISMISSED -> {
|
|
30
|
+
Log.d(TAG, "Recording notification dismissed by user")
|
|
31
|
+
audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationDismissed", mapOf())
|
|
32
|
+
}
|
|
33
|
+
RecordingNotification.ACTION_START -> {
|
|
34
|
+
Log.d(TAG, "Start recording action received")
|
|
35
|
+
audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationStart", mapOf())
|
|
36
|
+
}
|
|
37
|
+
RecordingNotification.ACTION_STOP -> {
|
|
38
|
+
Log.d(TAG, "Stop recording action received")
|
|
39
|
+
audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationStop", mapOf())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system.notification
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap
|
|
12
|
+
import com.swmansion.audioapi.R
|
|
13
|
+
import java.lang.ref.WeakReference
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This serves as a reference implementation and starting point for more complex notifications.
|
|
17
|
+
*/
|
|
18
|
+
class SimpleNotification(
|
|
19
|
+
private val reactContext: WeakReference<ReactApplicationContext>,
|
|
20
|
+
private val notificationId: Int = DEFAULT_NOTIFICATION_ID,
|
|
21
|
+
) : BaseNotification {
|
|
22
|
+
companion object {
|
|
23
|
+
const val DEFAULT_NOTIFICATION_ID = 200
|
|
24
|
+
const val CHANNEL_ID = "react-native-audio-api-simple"
|
|
25
|
+
const val CHANNEL_NAME = "Simple Notifications"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private var title: String = "Audio Playing"
|
|
29
|
+
private var text: String = ""
|
|
30
|
+
|
|
31
|
+
init {
|
|
32
|
+
createNotificationChannel()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun init(options: ReadableMap?): Notification {
|
|
36
|
+
// Parse options from JavaScript
|
|
37
|
+
if (options != null) {
|
|
38
|
+
if (options.hasKey("title")) {
|
|
39
|
+
title = options.getString("title") ?: "Audio Playing"
|
|
40
|
+
}
|
|
41
|
+
if (options.hasKey("text")) {
|
|
42
|
+
text = options.getString("text") ?: ""
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return buildNotification()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun update(options: ReadableMap?): Notification {
|
|
50
|
+
// Update works the same as init for simple notifications
|
|
51
|
+
return init(options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun reset() {
|
|
55
|
+
// Reset to default values
|
|
56
|
+
title = "Audio Playing"
|
|
57
|
+
text = ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun getNotificationId(): Int = notificationId
|
|
61
|
+
|
|
62
|
+
override fun getChannelId(): String = CHANNEL_ID
|
|
63
|
+
|
|
64
|
+
private fun buildNotification(): Notification {
|
|
65
|
+
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
66
|
+
|
|
67
|
+
// Use a system icon that's guaranteed to exist
|
|
68
|
+
val icon = android.R.drawable.ic_dialog_info
|
|
69
|
+
|
|
70
|
+
val builder =
|
|
71
|
+
NotificationCompat
|
|
72
|
+
.Builder(context, CHANNEL_ID)
|
|
73
|
+
.setContentTitle(title)
|
|
74
|
+
.setContentText(text)
|
|
75
|
+
.setSmallIcon(icon)
|
|
76
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
77
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
78
|
+
.setOngoing(false)
|
|
79
|
+
.setAutoCancel(false)
|
|
80
|
+
|
|
81
|
+
// Add content intent to open the app when notification is tapped
|
|
82
|
+
val packageName = context.packageName
|
|
83
|
+
val openAppIntent = context.packageManager?.getLaunchIntentForPackage(packageName)
|
|
84
|
+
if (openAppIntent != null) {
|
|
85
|
+
val pendingIntent =
|
|
86
|
+
PendingIntent.getActivity(
|
|
87
|
+
context,
|
|
88
|
+
0,
|
|
89
|
+
openAppIntent,
|
|
90
|
+
PendingIntent.FLAG_IMMUTABLE,
|
|
91
|
+
)
|
|
92
|
+
builder.setContentIntent(pendingIntent)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return builder.build()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun createNotificationChannel() {
|
|
99
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
100
|
+
val context = reactContext.get() ?: return
|
|
101
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
102
|
+
|
|
103
|
+
val channel =
|
|
104
|
+
NotificationChannel(
|
|
105
|
+
CHANNEL_ID,
|
|
106
|
+
CHANNEL_NAME,
|
|
107
|
+
NotificationManager.IMPORTANCE_HIGH,
|
|
108
|
+
).apply {
|
|
109
|
+
description = "Simple notification channel for audio playback"
|
|
110
|
+
setShowBadge(true)
|
|
111
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
112
|
+
enableLights(true)
|
|
113
|
+
enableVibration(false)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
notificationManager.createNotificationChannel(channel)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
@class AudioEngine;
|
|
12
12
|
@class NotificationManager;
|
|
13
13
|
@class AudioSessionManager;
|
|
14
|
-
@class
|
|
14
|
+
@class NotificationRegistry;
|
|
15
15
|
|
|
16
16
|
@interface AudioAPIModule : RCTEventEmitter
|
|
17
17
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
@property (nonatomic, strong) AudioEngine *audioEngine;
|
|
24
24
|
@property (nonatomic, strong) NotificationManager *notificationManager;
|
|
25
25
|
@property (nonatomic, strong) AudioSessionManager *audioSessionManager;
|
|
26
|
-
@property (nonatomic, strong)
|
|
26
|
+
@property (nonatomic, strong) NotificationRegistry *notificationRegistry;
|
|
27
27
|
|
|
28
28
|
- (void)invokeHandlerWithEventName:(NSString *)eventName eventBody:(NSDictionary *)eventBody;
|
|
29
29
|
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
#import <audioapi/AudioAPIModuleInstaller.h>
|
|
12
12
|
#import <audioapi/ios/system/AudioEngine.h>
|
|
13
13
|
#import <audioapi/ios/system/AudioSessionManager.h>
|
|
14
|
-
#import <audioapi/ios/system/LockScreenManager.h>
|
|
15
14
|
#import <audioapi/ios/system/NotificationManager.h>
|
|
15
|
+
#import <audioapi/ios/system/notification/NotificationRegistry.h>
|
|
16
16
|
|
|
17
17
|
#import <audioapi/events/AudioEventHandlerRegistry.h>
|
|
18
18
|
|
|
@@ -50,7 +50,7 @@ RCT_EXPORT_MODULE(AudioAPIModule);
|
|
|
50
50
|
[self.audioEngine cleanup];
|
|
51
51
|
[self.notificationManager cleanup];
|
|
52
52
|
[self.audioSessionManager cleanup];
|
|
53
|
-
[self.
|
|
53
|
+
[self.notificationRegistry cleanup];
|
|
54
54
|
|
|
55
55
|
_eventHandler = nullptr;
|
|
56
56
|
|
|
@@ -61,8 +61,8 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install)
|
|
|
61
61
|
{
|
|
62
62
|
self.audioSessionManager = [[AudioSessionManager alloc] init];
|
|
63
63
|
self.audioEngine = [[AudioEngine alloc] initWithAudioSessionManager:self.audioSessionManager];
|
|
64
|
-
self.lockScreenManager = [[LockScreenManager alloc] initWithAudioAPIModule:self];
|
|
65
64
|
self.notificationManager = [[NotificationManager alloc] initWithAudioAPIModule:self];
|
|
65
|
+
self.notificationRegistry = [[NotificationRegistry alloc] initWithAudioAPIModule:self];
|
|
66
66
|
|
|
67
67
|
auto jsiRuntime = reinterpret_cast<facebook::jsi::Runtime *>(self.bridge.runtime);
|
|
68
68
|
|
|
@@ -142,21 +142,6 @@ RCT_EXPORT_METHOD(
|
|
|
142
142
|
allowHaptics:allowHaptics];
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
RCT_EXPORT_METHOD(setLockScreenInfo : (NSDictionary *)info)
|
|
146
|
-
{
|
|
147
|
-
[self.lockScreenManager setLockScreenInfo:info];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
RCT_EXPORT_METHOD(resetLockScreenInfo)
|
|
151
|
-
{
|
|
152
|
-
[self.lockScreenManager resetLockScreenInfo];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
RCT_EXPORT_METHOD(enableRemoteCommand : (NSString *)name enabled : (BOOL)enabled)
|
|
156
|
-
{
|
|
157
|
-
[self.lockScreenManager enableRemoteCommand:name enabled:enabled];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
145
|
RCT_EXPORT_METHOD(observeAudioInterruptions : (BOOL)enabled)
|
|
161
146
|
{
|
|
162
147
|
[self.notificationManager observeAudioInterruptions:enabled];
|
|
@@ -190,6 +175,25 @@ RCT_EXPORT_METHOD(
|
|
|
190
175
|
});
|
|
191
176
|
}
|
|
192
177
|
|
|
178
|
+
RCT_EXPORT_METHOD(
|
|
179
|
+
requestNotificationPermissions : (nonnull RCTPromiseResolveBlock)
|
|
180
|
+
resolve reject : (nonnull RCTPromiseRejectBlock)reject)
|
|
181
|
+
{
|
|
182
|
+
// iOS doesn't require explicit notification permissions for media controls
|
|
183
|
+
// MPNowPlayingInfoCenter and MPRemoteCommandCenter work without permissions
|
|
184
|
+
// Return 'granted' to match the spec interface
|
|
185
|
+
resolve(@"granted");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
RCT_EXPORT_METHOD(
|
|
189
|
+
checkNotificationPermissions : (nonnull RCTPromiseResolveBlock)
|
|
190
|
+
resolve reject : (nonnull RCTPromiseRejectBlock)reject)
|
|
191
|
+
{
|
|
192
|
+
// iOS doesn't require explicit notification permissions for media controls
|
|
193
|
+
// Return 'granted' to match the spec interface
|
|
194
|
+
resolve(@"granted");
|
|
195
|
+
}
|
|
196
|
+
|
|
193
197
|
RCT_EXPORT_METHOD(
|
|
194
198
|
getDevicesInfo : (nonnull RCTPromiseResolveBlock)
|
|
195
199
|
resolve reject : (nonnull RCTPromiseRejectBlock)reject)
|
|
@@ -204,6 +208,92 @@ RCT_EXPORT_METHOD(disableSessionManagement)
|
|
|
204
208
|
[self.audioSessionManager disableSessionManagement];
|
|
205
209
|
}
|
|
206
210
|
|
|
211
|
+
// New notification system methods
|
|
212
|
+
RCT_EXPORT_METHOD(
|
|
213
|
+
registerNotification : (NSString *)type key : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
214
|
+
resolve reject : (RCTPromiseRejectBlock)reject)
|
|
215
|
+
{
|
|
216
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
217
|
+
BOOL success = [self.notificationRegistry registerNotificationType:type withKey:key];
|
|
218
|
+
|
|
219
|
+
if (success) {
|
|
220
|
+
resolve(@{@"success" : @true});
|
|
221
|
+
} else {
|
|
222
|
+
resolve(@{@"success" : @false, @"error" : @"Failed to register notification"});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
RCT_EXPORT_METHOD(
|
|
228
|
+
showNotification : (NSString *)key options : (NSDictionary *)
|
|
229
|
+
options resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
|
|
230
|
+
{
|
|
231
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
232
|
+
BOOL success = [self.notificationRegistry showNotificationWithKey:key options:options];
|
|
233
|
+
|
|
234
|
+
if (success) {
|
|
235
|
+
resolve(@{@"success" : @true});
|
|
236
|
+
} else {
|
|
237
|
+
resolve(@{@"success" : @false, @"error" : @"Failed to show notification"});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
RCT_EXPORT_METHOD(
|
|
243
|
+
updateNotification : (NSString *)key options : (NSDictionary *)
|
|
244
|
+
options resolve : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject)
|
|
245
|
+
{
|
|
246
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
247
|
+
BOOL success = [self.notificationRegistry updateNotificationWithKey:key options:options];
|
|
248
|
+
|
|
249
|
+
if (success) {
|
|
250
|
+
resolve(@{@"success" : @true});
|
|
251
|
+
} else {
|
|
252
|
+
resolve(@{@"success" : @false, @"error" : @"Failed to update notification"});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
RCT_EXPORT_METHOD(
|
|
258
|
+
hideNotification : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
259
|
+
resolve reject : (RCTPromiseRejectBlock)reject)
|
|
260
|
+
{
|
|
261
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
262
|
+
BOOL success = [self.notificationRegistry hideNotificationWithKey:key];
|
|
263
|
+
|
|
264
|
+
if (success) {
|
|
265
|
+
resolve(@{@"success" : @true});
|
|
266
|
+
} else {
|
|
267
|
+
resolve(@{@"success" : @false, @"error" : @"Failed to hide notification"});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
RCT_EXPORT_METHOD(
|
|
273
|
+
unregisterNotification : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
274
|
+
resolve reject : (RCTPromiseRejectBlock)reject)
|
|
275
|
+
{
|
|
276
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
277
|
+
BOOL success = [self.notificationRegistry unregisterNotificationWithKey:key];
|
|
278
|
+
|
|
279
|
+
if (success) {
|
|
280
|
+
resolve(@{@"success" : @true});
|
|
281
|
+
} else {
|
|
282
|
+
resolve(@{@"success" : @false, @"error" : @"Failed to unregister notification"});
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
RCT_EXPORT_METHOD(
|
|
288
|
+
isNotificationActive : (NSString *)key resolve : (RCTPromiseResolveBlock)
|
|
289
|
+
resolve reject : (RCTPromiseRejectBlock)reject)
|
|
290
|
+
{
|
|
291
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
292
|
+
BOOL isActive = [self.notificationRegistry isNotificationActiveWithKey:key];
|
|
293
|
+
resolve(@(isActive));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
207
297
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
208
298
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
209
299
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
@@ -255,10 +255,10 @@ static AudioEngine *_sharedInstance = nil;
|
|
|
255
255
|
NSLog(@"================ 🎧 AVAudioEngine STATE ================");
|
|
256
256
|
|
|
257
257
|
// AVAudioEngine state
|
|
258
|
-
NSLog(@"➡️ engine.isRunning: %@", self.audioEngine.isRunning ? @"
|
|
258
|
+
NSLog(@"➡️ engine.isRunning: %@", self.audioEngine.isRunning ? @"true" : @"false");
|
|
259
259
|
NSLog(
|
|
260
260
|
@"➡️ engine.isInManualRenderingMode: %@",
|
|
261
|
-
self.audioEngine.isInManualRenderingMode ? @"
|
|
261
|
+
self.audioEngine.isInManualRenderingMode ? @"true" : @"false");
|
|
262
262
|
|
|
263
263
|
// Session state
|
|
264
264
|
NSLog(@"🎚️ Session category: %@", session.category);
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
self.sessionCategory,
|
|
183
183
|
self.sessionMode,
|
|
184
184
|
(unsigned long)self.sessionOptions,
|
|
185
|
-
self.allowHapticsAndSystemSoundsDuringRecording ? @"
|
|
185
|
+
self.allowHapticsAndSystemSoundsDuringRecording ? @"true" : @"false");
|
|
186
186
|
|
|
187
187
|
NSError *error = nil;
|
|
188
188
|
|
|
@@ -261,7 +261,7 @@ static NSString *NotificationManagerContext = @"NotificationManagerContext";
|
|
|
261
261
|
target:self
|
|
262
262
|
selector:@selector(checkSecondaryAudioHint)
|
|
263
263
|
userInfo:nil
|
|
264
|
-
repeats:
|
|
264
|
+
repeats:true];
|
|
265
265
|
|
|
266
266
|
[[NSRunLoop mainRunLoop] addTimer:self.hintPollingTimer forMode:NSRunLoopCommonModes];
|
|
267
267
|
}
|