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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +3 -10
  2. package/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +0 -4
  3. package/android/src/main/java/com/swmansion/audioapi/AudioAPIModule.kt +4 -83
  4. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +14 -29
  5. package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +10 -9
  6. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +10 -51
  7. package/android/src/main/java/com/swmansion/audioapi/system/notification/BaseNotification.kt +6 -14
  8. package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +79 -60
  9. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotification.kt +249 -411
  10. package/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt +8 -3
  11. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +240 -222
  12. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +11 -22
  13. package/android/src/main/java/com/swmansion/audioapi/system/notification/state/RecordingNotificationState.kt +24 -0
  14. package/android/src/main/res/layout/btn_round_ripple.xml +9 -0
  15. package/android/src/main/res/layout/notification_collapsed.xml +45 -0
  16. package/android/src/main/res/layout/notification_expanded.xml +44 -0
  17. package/android/src/oldarch/NativeAudioAPIModuleSpec.java +1 -13
  18. package/common/cpp/audioapi/core/utils/AudioFileWriter.cpp +1 -1
  19. package/ios/audioapi/ios/AudioAPIModule.mm +5 -48
  20. package/ios/audioapi/ios/system/notification/BaseNotification.h +0 -7
  21. package/ios/audioapi/ios/system/notification/NotificationRegistry.h +5 -25
  22. package/ios/audioapi/ios/system/notification/NotificationRegistry.mm +19 -64
  23. package/ios/audioapi/ios/system/notification/PlaybackNotification.mm +4 -15
  24. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +2 -1
  25. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js.map +1 -1
  26. package/lib/commonjs/api.js +1 -29
  27. package/lib/commonjs/api.js.map +1 -1
  28. package/lib/commonjs/core/AudioDecoder.js +42 -16
  29. package/lib/commonjs/core/AudioDecoder.js.map +1 -1
  30. package/lib/commonjs/core/AudioRecorder.js +2 -1
  31. package/lib/commonjs/core/AudioRecorder.js.map +1 -1
  32. package/lib/commonjs/core/AudioStretcher.js +2 -1
  33. package/lib/commonjs/core/AudioStretcher.js.map +1 -1
  34. package/lib/commonjs/core/BaseAudioContext.js +2 -5
  35. package/lib/commonjs/core/BaseAudioContext.js.map +1 -1
  36. package/lib/commonjs/errors/AudioApiError.js +14 -0
  37. package/lib/commonjs/errors/AudioApiError.js.map +1 -0
  38. package/lib/commonjs/errors/index.js +7 -0
  39. package/lib/commonjs/errors/index.js.map +1 -1
  40. package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -1
  41. package/lib/commonjs/specs/NativeAudioAPIModule.web.js +0 -9
  42. package/lib/commonjs/specs/NativeAudioAPIModule.web.js.map +1 -1
  43. package/lib/commonjs/system/notification/PlaybackNotificationManager.js +40 -85
  44. package/lib/commonjs/system/notification/PlaybackNotificationManager.js.map +1 -1
  45. package/lib/commonjs/system/notification/RecordingNotificationManager.ios.js +51 -0
  46. package/lib/commonjs/system/notification/RecordingNotificationManager.ios.js.map +1 -0
  47. package/lib/commonjs/system/notification/RecordingNotificationManager.js +30 -144
  48. package/lib/commonjs/system/notification/RecordingNotificationManager.js.map +1 -1
  49. package/lib/commonjs/system/notification/index.js +1 -9
  50. package/lib/commonjs/system/notification/index.js.map +1 -1
  51. package/lib/commonjs/utils/index.js +3 -2
  52. package/lib/commonjs/utils/index.js.map +1 -1
  53. package/lib/commonjs/utils/paths.js +18 -0
  54. package/lib/commonjs/utils/paths.js.map +1 -0
  55. package/lib/commonjs/web-core/AudioContext.js +20 -11
  56. package/lib/commonjs/web-core/AudioContext.js.map +1 -1
  57. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js +0 -1
  58. package/lib/commonjs/web-system/notification/PlaybackNotificationManager.js.map +1 -1
  59. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js +1 -6
  60. package/lib/commonjs/web-system/notification/RecordingNotificationManager.js.map +1 -1
  61. package/lib/module/AudioAPIModule/AudioAPIModule.js +2 -1
  62. package/lib/module/AudioAPIModule/AudioAPIModule.js.map +1 -1
  63. package/lib/module/api.js +3 -2
  64. package/lib/module/api.js.map +1 -1
  65. package/lib/module/core/AudioDecoder.js +42 -16
  66. package/lib/module/core/AudioDecoder.js.map +1 -1
  67. package/lib/module/core/AudioRecorder.js +3 -1
  68. package/lib/module/core/AudioRecorder.js.map +1 -1
  69. package/lib/module/core/AudioStretcher.js +2 -1
  70. package/lib/module/core/AudioStretcher.js.map +1 -1
  71. package/lib/module/core/BaseAudioContext.js +2 -5
  72. package/lib/module/core/BaseAudioContext.js.map +1 -1
  73. package/lib/module/errors/AudioApiError.js +10 -0
  74. package/lib/module/errors/AudioApiError.js.map +1 -0
  75. package/lib/module/errors/index.js +1 -0
  76. package/lib/module/errors/index.js.map +1 -1
  77. package/lib/module/specs/NativeAudioAPIModule.js.map +1 -1
  78. package/lib/module/specs/NativeAudioAPIModule.web.js +0 -9
  79. package/lib/module/specs/NativeAudioAPIModule.web.js.map +1 -1
  80. package/lib/module/system/notification/PlaybackNotificationManager.js +40 -85
  81. package/lib/module/system/notification/PlaybackNotificationManager.js.map +1 -1
  82. package/lib/module/system/notification/RecordingNotificationManager.ios.js +47 -0
  83. package/lib/module/system/notification/RecordingNotificationManager.ios.js.map +1 -0
  84. package/lib/module/system/notification/RecordingNotificationManager.js +30 -144
  85. package/lib/module/system/notification/RecordingNotificationManager.js.map +1 -1
  86. package/lib/module/system/notification/index.js +1 -2
  87. package/lib/module/system/notification/index.js.map +1 -1
  88. package/lib/module/utils/index.js +3 -2
  89. package/lib/module/utils/index.js.map +1 -1
  90. package/lib/module/utils/paths.js +12 -0
  91. package/lib/module/utils/paths.js.map +1 -0
  92. package/lib/module/web-core/AudioContext.js +20 -11
  93. package/lib/module/web-core/AudioContext.js.map +1 -1
  94. package/lib/module/web-system/notification/PlaybackNotificationManager.js +0 -1
  95. package/lib/module/web-system/notification/PlaybackNotificationManager.js.map +1 -1
  96. package/lib/module/web-system/notification/RecordingNotificationManager.js +1 -6
  97. package/lib/module/web-system/notification/RecordingNotificationManager.js.map +1 -1
  98. package/lib/typescript/AudioAPIModule/AudioAPIModule.d.ts.map +1 -1
  99. package/lib/typescript/api.d.ts +3 -2
  100. package/lib/typescript/api.d.ts.map +1 -1
  101. package/lib/typescript/core/AudioDecoder.d.ts +2 -1
  102. package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
  103. package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
  104. package/lib/typescript/core/AudioStretcher.d.ts.map +1 -1
  105. package/lib/typescript/core/BaseAudioContext.d.ts +2 -2
  106. package/lib/typescript/core/BaseAudioContext.d.ts.map +1 -1
  107. package/lib/typescript/errors/AudioApiError.d.ts +5 -0
  108. package/lib/typescript/errors/AudioApiError.d.ts.map +1 -0
  109. package/lib/typescript/errors/index.d.ts +1 -0
  110. package/lib/typescript/errors/index.d.ts.map +1 -1
  111. package/lib/typescript/interfaces.d.ts +1 -1
  112. package/lib/typescript/interfaces.d.ts.map +1 -1
  113. package/lib/typescript/specs/NativeAudioAPIModule.d.ts +2 -5
  114. package/lib/typescript/specs/NativeAudioAPIModule.d.ts.map +1 -1
  115. package/lib/typescript/specs/NativeAudioAPIModule.web.d.ts +1 -4
  116. package/lib/typescript/specs/NativeAudioAPIModule.web.d.ts.map +1 -1
  117. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts +32 -9
  118. package/lib/typescript/system/notification/PlaybackNotificationManager.d.ts.map +1 -1
  119. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts +26 -13
  120. package/lib/typescript/system/notification/RecordingNotificationManager.d.ts.map +1 -1
  121. package/lib/typescript/system/notification/RecordingNotificationManager.ios.d.ts +36 -0
  122. package/lib/typescript/system/notification/RecordingNotificationManager.ios.d.ts.map +1 -0
  123. package/lib/typescript/system/notification/index.d.ts +0 -1
  124. package/lib/typescript/system/notification/index.d.ts.map +1 -1
  125. package/lib/typescript/system/notification/types.d.ts +12 -22
  126. package/lib/typescript/system/notification/types.d.ts.map +1 -1
  127. package/lib/typescript/types.d.ts +1 -0
  128. package/lib/typescript/types.d.ts.map +1 -1
  129. package/lib/typescript/utils/index.d.ts.map +1 -1
  130. package/lib/typescript/utils/paths.d.ts +4 -0
  131. package/lib/typescript/utils/paths.d.ts.map +1 -0
  132. package/lib/typescript/web-core/AudioContext.d.ts +8 -9
  133. package/lib/typescript/web-core/AudioContext.d.ts.map +1 -1
  134. package/lib/typescript/web-core/BaseAudioContext.d.ts +6 -7
  135. package/lib/typescript/web-core/BaseAudioContext.d.ts.map +1 -1
  136. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts +1 -2
  137. package/lib/typescript/web-system/notification/PlaybackNotificationManager.d.ts.map +1 -1
  138. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts +2 -7
  139. package/lib/typescript/web-system/notification/RecordingNotificationManager.d.ts.map +1 -1
  140. package/package.json +1 -1
  141. package/src/AudioAPIModule/AudioAPIModule.ts +2 -1
  142. package/src/api.ts +2 -8
  143. package/src/core/AudioDecoder.ts +91 -21
  144. package/src/core/AudioRecorder.ts +2 -1
  145. package/src/core/AudioStretcher.ts +2 -1
  146. package/src/core/BaseAudioContext.ts +4 -6
  147. package/src/errors/AudioApiError.ts +8 -0
  148. package/src/errors/index.ts +1 -0
  149. package/src/interfaces.ts +1 -1
  150. package/src/specs/NativeAudioAPIModule.ts +5 -15
  151. package/src/specs/NativeAudioAPIModule.web.ts +1 -12
  152. package/src/system/notification/PlaybackNotificationManager.ts +42 -117
  153. package/src/system/notification/RecordingNotificationManager.ios.ts +65 -0
  154. package/src/system/notification/RecordingNotificationManager.ts +33 -183
  155. package/src/system/notification/index.ts +0 -1
  156. package/src/system/notification/types.ts +15 -37
  157. package/src/types.ts +2 -0
  158. package/src/utils/index.ts +3 -2
  159. package/src/utils/paths.ts +11 -0
  160. package/src/web-core/AudioContext.tsx +34 -19
  161. package/src/web-core/BaseAudioContext.tsx +9 -7
  162. package/src/web-system/notification/PlaybackNotificationManager.ts +1 -7
  163. package/src/web-system/notification/RecordingNotificationManager.ts +1 -16
  164. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioPlayer.kt +0 -26
  165. package/android/src/main/java/com/swmansion/audioapi/core/NativeAudioRecorder.kt +0 -26
  166. package/android/src/main/java/com/swmansion/audioapi/system/notification/SimpleNotification.kt +0 -119
  167. package/lib/commonjs/system/notification/SimpleNotificationManager.js +0 -125
  168. package/lib/commonjs/system/notification/SimpleNotificationManager.js.map +0 -1
  169. package/lib/module/system/notification/SimpleNotificationManager.js +0 -121
  170. package/lib/module/system/notification/SimpleNotificationManager.js.map +0 -1
  171. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts +0 -21
  172. package/lib/typescript/system/notification/SimpleNotificationManager.d.ts.map +0 -1
  173. package/src/system/notification/SimpleNotificationManager.ts +0 -175
@@ -3,7 +3,6 @@ package com.swmansion.audioapi.system.notification
3
3
  import android.content.BroadcastReceiver
4
4
  import android.content.Context
5
5
  import android.content.Intent
6
- import android.util.Log
7
6
  import com.swmansion.audioapi.AudioAPIModule
8
7
 
9
8
  /**
@@ -12,7 +11,8 @@ import com.swmansion.audioapi.AudioAPIModule
12
11
  class PlaybackNotificationReceiver : BroadcastReceiver() {
13
12
  companion object {
14
13
  const val ACTION_NOTIFICATION_DISMISSED = "com.swmansion.audioapi.PLAYBACK_NOTIFICATION_DISMISSED"
15
- private const val TAG = "PlaybackNotificationReceiver"
14
+ const val ACTION_SKIP_FORWARD = "com.swmansion.audioapi.ACTION_SKIP_FORWARD"
15
+ const val ACTION_SKIP_BACKWARD = "com.swmansion.audioapi.ACTION_SKIP_BACKWARD"
16
16
 
17
17
  private var audioAPIModule: AudioAPIModule? = null
18
18
 
@@ -26,8 +26,13 @@ class PlaybackNotificationReceiver : BroadcastReceiver() {
26
26
  intent: Intent?,
27
27
  ) {
28
28
  if (intent?.action == ACTION_NOTIFICATION_DISMISSED) {
29
- Log.d(TAG, "Notification dismissed by user")
30
29
  audioAPIModule?.invokeHandlerWithEventNameAndEventBody("playbackNotificationDismissed", mapOf())
30
+ } else if (intent?.action == ACTION_SKIP_FORWARD) {
31
+ val body = HashMap<String, Any>().apply { put("value", 15) }
32
+ audioAPIModule?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipForward", body)
33
+ } else if (intent?.action == ACTION_SKIP_BACKWARD) {
34
+ val body = HashMap<String, Any>().apply { put("value", 15) }
35
+ audioAPIModule?.invokeHandlerWithEventNameAndEventBody("playbackNotificationSkipBackward", body)
31
36
  }
32
37
  }
33
38
  }
@@ -4,300 +4,318 @@ import android.app.Notification
4
4
  import android.app.NotificationChannel
5
5
  import android.app.NotificationManager
6
6
  import android.app.PendingIntent
7
+ import android.content.ComponentCallbacks
7
8
  import android.content.Context
8
9
  import android.content.Intent
9
10
  import android.content.IntentFilter
11
+ import android.content.res.Configuration
10
12
  import android.graphics.Color
13
+ import android.graphics.drawable.Icon
11
14
  import android.os.Build
12
15
  import android.util.Log
16
+ import android.widget.RemoteViews
17
+ import androidx.annotation.RequiresApi
13
18
  import androidx.core.app.NotificationCompat
19
+ import androidx.core.content.ContextCompat
14
20
  import com.facebook.react.bridge.ReactApplicationContext
15
21
  import com.facebook.react.bridge.ReadableMap
16
22
  import com.swmansion.audioapi.AudioAPIModule
23
+ import com.swmansion.audioapi.R
24
+ import com.swmansion.audioapi.system.notification.state.RecordingNotificationState
17
25
  import java.lang.ref.WeakReference
18
26
 
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
27
  class RecordingNotification(
29
28
  private val reactContext: WeakReference<ReactApplicationContext>,
30
29
  private val audioAPIModule: WeakReference<AudioAPIModule>,
31
30
  private val notificationId: Int,
32
31
  private val channelId: String,
33
- ) : BaseNotification {
32
+ ) : BaseNotification,
33
+ ComponentCallbacks {
34
34
  companion object {
35
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"
36
+ const val ID = 200
38
37
  }
39
38
 
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
39
+ private var state: RecordingNotificationState =
40
+ RecordingNotificationState(
41
+ darkTheme =
42
+ reactContext
43
+ .get()!!
44
+ .resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES,
45
+ initialized = false,
46
+ )
47
47
 
48
- override fun init(params: ReadableMap?): Notification {
48
+ private fun initializeNotification() {
49
49
  val context = reactContext.get() ?: throw IllegalStateException("React context is null")
50
+ if (!state.initialized) {
51
+ context.registerComponentCallbacks(this)
52
+ createNotificationChannel(context)
53
+ state.receiver =
54
+ RecordingNotificationReceiver(audioAPIModule.get()!!)
55
+ val filter =
56
+ IntentFilter().apply {
57
+ addAction(RecordingNotificationReceiver.NOTIFICATION_RECORDING_STOPPED)
58
+ addAction(RecordingNotificationReceiver.NOTIFICATION_RECORDING_RESUMED)
59
+ }
60
+ ContextCompat.registerReceiver(
61
+ context,
62
+ state.receiver,
63
+ filter,
64
+ ContextCompat.RECEIVER_NOT_EXPORTED,
65
+ )
50
66
 
51
- // Register broadcast receiver
52
- registerReceiver()
67
+ state.pauseIntent =
68
+ Intent(RecordingNotificationReceiver.NOTIFICATION_RECORDING_STOPPED).apply {
69
+ `package` = context.packageName
70
+ }
53
71
 
54
- // Create notification channel first
55
- createNotificationChannel()
72
+ state.resumeIntent =
73
+ Intent(RecordingNotificationReceiver.NOTIFICATION_RECORDING_RESUMED).apply {
74
+ `package` = context.packageName
75
+ }
76
+ state.darkTheme = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
77
+ state.initialized = true
78
+ }
79
+ }
56
80
 
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)
81
+ override fun show(options: ReadableMap?): Notification {
82
+ initializeNotification()
83
+ val context = reactContext.get() ?: throw IllegalStateException("React context is null")
84
+ if (options != state.cachedRNOptions) {
85
+ state.cachedRNOptions = options
86
+ parseMapFromRN(options)
87
+ }
88
+ val builder = getBuilder()
68
89
 
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(
90
+ if (state.smallIconResourceName != null) {
91
+ builder.setSmallIcon(context.resources.getIdentifier(state.smallIconResourceName, "drawable", context.packageName))
92
+ }
93
+
94
+ if (state.largeIconResourceName != null) {
95
+ val icon =
96
+ Icon.createWithResource(
75
97
  context,
76
- 0,
77
- openAppIntent,
78
- PendingIntent.FLAG_IMMUTABLE,
98
+ context.resources.getIdentifier(state.largeIconResourceName, "drawable", context.packageName),
79
99
  )
80
- notificationBuilder?.setContentIntent(pendingIntent)
100
+ builder.setLargeIcon(icon)
81
101
  }
82
102
 
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)
103
+ if (state.backgroundColor != null) {
104
+ builder.setColor(state.backgroundColor!!)
98
105
  }
99
106
 
100
- return buildNotification()
101
- }
107
+ val collapsedView = RemoteViews(context.packageName, R.layout.notification_collapsed)
108
+ val expandedView = RemoteViews(context.packageName, R.layout.notification_expanded)
102
109
 
103
- override fun reset() {
104
- // Unregister receiver
105
- unregisterReceiver()
110
+ val (pauseResumePendingIntent, iconId) = setupPauseResumeIntent(context)
106
111
 
107
- // Reset state
108
- title = "Audio Recording"
109
- description = "Ready to record"
110
- isRecording = false
111
- notificationBuilder = null
112
- }
112
+ setupRemoteView(listOf(collapsedView, expandedView), pauseResumePendingIntent, iconId)
113
113
 
114
- override fun getNotificationId(): Int = notificationId
114
+ builder
115
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
116
+ .setCustomContentView(collapsedView)
117
+ .setCustomBigContentView(expandedView)
118
+ .setContentTitle(state.title)
119
+ .setContentText(state.contentText)
115
120
 
116
- override fun getChannelId(): String = channelId
117
-
118
- override fun update(options: ReadableMap?): Notification {
119
- if (options == null) {
120
- return buildNotification()
121
+ if (state.backgroundColor != null) {
122
+ builder.setColor(state.backgroundColor!!)
121
123
  }
122
124
 
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
- }
125
+ return builder.build()
126
+ }
134
127
 
135
- // Update metadata
136
- if (options.hasKey("title")) {
137
- title = options.getString("title") ?: "Audio Recording"
138
- }
128
+ private fun setupPauseResumeIntent(context: Context): Pair<PendingIntent, Int> {
129
+ val pauseResumeIntent =
130
+ if (state.paused) {
131
+ state.resumeIntent
132
+ } else {
133
+ state.pauseIntent
134
+ }
139
135
 
140
- if (options.hasKey("description")) {
141
- description = options.getString("description") ?: "Ready to record"
142
- }
136
+ val pauseResumePendingIntent =
137
+ PendingIntent.getBroadcast(
138
+ context,
139
+ 0,
140
+ pauseResumeIntent!!,
141
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
142
+ )
143
143
 
144
- // Update recording state
145
- if (options.hasKey("state")) {
146
- when (options.getString("state")) {
147
- "recording" -> isRecording = true
148
- "stopped" -> isRecording = false
144
+ val pauseId =
145
+ if (state.pauseIconResourceName != null) {
146
+ context.resources.getIdentifier(state.pauseIconResourceName, "drawable", context.packageName)
147
+ } else {
148
+ android.R.drawable.ic_media_pause
149
149
  }
150
- }
151
-
152
- // Update notification content
153
- val statusText =
154
- description.ifEmpty {
155
- if (isRecording) "Recording..." else "Ready to record"
150
+ val resumeId =
151
+ if (state.resumeIconResourceName != null) {
152
+ context.resources.getIdentifier(state.resumeIconResourceName, "drawable", context.packageName)
153
+ } else {
154
+ android.R.drawable.ic_media_play
156
155
  }
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
156
 
172
- // Update action button
173
- updateActions()
174
-
175
- return buildNotification()
157
+ val iconId = if (state.paused) resumeId else pauseId
158
+ return pauseResumePendingIntent to iconId
176
159
  }
177
160
 
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)
161
+ private fun setupRemoteView(
162
+ views: List<RemoteViews>,
163
+ pauseResumePendingIntent: PendingIntent,
164
+ iconId: Int,
165
+ ) {
166
+ val iconColor =
167
+ if (state.darkTheme) {
168
+ Color.WHITE // Dark Mode -> White Icon
169
+ } else {
170
+ Color.BLACK // Light Mode -> Black Icon
171
+ }
172
+ for (view in views) {
173
+ view.setTextViewText(R.id.notification_title, state.title)
174
+ view.setTextViewText(R.id.notification_content, state.contentText)
175
+ view.setImageViewResource(R.id.notification_action_btn, iconId)
176
+ view.setInt(R.id.notification_action_btn, "setColorFilter", iconColor)
177
+ view.setOnClickPendingIntent(R.id.notification_action_btn, pauseResumePendingIntent)
228
178
  }
179
+ }
229
180
 
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
- )
181
+ // not used currently, left for future reference
182
+ // private fun loadBitmapFromUri(
183
+ // context: Context,
184
+ // uriString: String?,
185
+ // ): Bitmap? =
186
+ // try {
187
+ // val uri = android.net.Uri.parse(uriString)
188
+ // val inputStream: InputStream
189
+ // if (uri.scheme == "http" || uri.scheme == "https") {
190
+ // // web URL
191
+ // val connection = java.net.URL(uriString).openConnection()
192
+ // connection.doInput = true
193
+ // connection.connect()
194
+ // inputStream = connection.inputStream
195
+ // } else {
196
+ // // local files
197
+ // inputStream = context.contentResolver.openInputStream(uri)!!
198
+ // }
199
+ // android.graphics.BitmapFactory.decodeStream(inputStream)
200
+ // } catch (e: Exception) {
201
+ // Log.e(TAG, "Failed to load bitmap from URI: $uriString", e)
202
+ // null
203
+ // }
204
+
205
+ private fun getBuilder(): NotificationCompat.Builder {
206
+ val context = reactContext.get() ?: throw IllegalStateException("React context is null")
207
+ if (state.builder == null) {
208
+ val openAppIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
209
+ val pendingIntent = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_IMMUTABLE)
210
+
211
+ state.builder =
212
+ NotificationCompat
213
+ .Builder(context, channelId)
214
+ .setOngoing(true)
215
+ .setContentIntent(pendingIntent)
216
+ }
217
+ if (state.smallIconResourceName == null) {
218
+ state.builder!!.setSmallIcon(android.R.drawable.ic_btn_speak_now)
219
+ }
220
+ return state.builder!!
240
221
  }
241
222
 
242
- private fun createNotificationChannel() {
223
+ private fun createNotificationChannel(context: ReactApplicationContext) {
243
224
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
244
- val context = reactContext.get() ?: return
245
-
246
225
  val channel =
247
226
  NotificationChannel(
248
227
  channelId,
249
- "Audio Recording",
250
- NotificationManager.IMPORTANCE_HIGH,
228
+ "Recording Audio",
229
+ NotificationManager.IMPORTANCE_LOW,
251
230
  ).apply {
252
- description = "Recording controls and status"
253
- setShowBadge(true)
231
+ description = "Notifications for ongoing audio recordings"
254
232
  lockscreenVisibility = Notification.VISIBILITY_PUBLIC
255
- enableLights(true)
256
- lightColor = Color.RED
257
- enableVibration(false)
258
233
  }
259
-
260
234
  val notificationManager =
261
235
  context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
262
236
  notificationManager.createNotificationChannel(channel)
263
-
264
- Log.d(TAG, "Notification channel created: $channelId")
265
237
  }
238
+ Log.d(TAG, "Notification channel created: $channelId")
266
239
  }
267
240
 
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)
241
+ private fun parseMapFromRN(options: ReadableMap?) {
242
+ state.title = if (options?.hasKey("title") == true) options.getString("title") else state.title ?: "Recording Audio"
243
+ state.contentText =
244
+ if (options?.hasKey("contentText") == true) {
245
+ options.getString("contentText")
246
+ } else {
247
+ state.contentText ?: "Audio recording is in progress/paused"
248
+ }
249
+ state.smallIconResourceName =
250
+ if (options?.hasKey("smallIconResourceName") ==
251
+ true
252
+ ) {
253
+ options.getString("smallIconResourceName")
254
+ } else {
255
+ state.smallIconResourceName ?: null
256
+ }
257
+ state.largeIconResourceName =
258
+ if (options?.hasKey("largeIconResourceName") ==
259
+ true
260
+ ) {
261
+ options.getString("largeIconResourceName")
262
+ } else {
263
+ state.largeIconResourceName ?: null
264
+ }
265
+ state.pauseIconResourceName =
266
+ if (options?.hasKey("pauseIconResourceName") ==
267
+ true
268
+ ) {
269
+ options.getString("pauseIconResourceName")
282
270
  } else {
283
- context.registerReceiver(receiver, filter)
271
+ state.pauseIconResourceName ?: null
284
272
  }
273
+ state.resumeIconResourceName =
274
+ if (options?.hasKey("resumeIconResourceName") ==
275
+ true
276
+ ) {
277
+ options.getString("resumeIconResourceName")
278
+ } else {
279
+ state.resumeIconResourceName ?: null
280
+ }
281
+ state.backgroundColor = if (options?.hasKey("color") == true) options.getInt("color") else state.backgroundColor ?: null
282
+ state.paused = if (options?.hasKey("paused") == true) options.getBoolean("paused") else false
283
+ }
285
284
 
286
- Log.d(TAG, "RecordingNotificationReceiver registered")
285
+ override fun hide() {
286
+ val context = reactContext.get() ?: throw IllegalStateException("React context is null")
287
+ if (state.receiver != null) {
288
+ context.unregisterReceiver(state.receiver)
289
+ context.unregisterComponentCallbacks(this)
290
+ state.receiver = null
287
291
  }
292
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
293
+ notificationManager.cancel(notificationId)
294
+ state.initialized = false
295
+ state.builder = null
288
296
  }
289
297
 
290
- private fun unregisterReceiver() {
291
- val context = reactContext.get() ?: return
298
+ override fun getNotificationId(): Int = notificationId
292
299
 
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
+ override fun getChannelId(): String = channelId
301
+
302
+ @RequiresApi(Build.VERSION_CODES.O)
303
+ override fun onConfigurationChanged(newConfig: Configuration) {
304
+ val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
305
+ if (currentNightMode != state.darkTheme) {
306
+ // Theme changed, rebuild notification
307
+ state.darkTheme = currentNightMode
308
+ val notification = show(state.cachedRNOptions)
309
+ val context = reactContext.get()
310
+ if (context != null) {
311
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
312
+ notificationManager.notify(notificationId, notification)
300
313
  }
301
314
  }
302
315
  }
316
+
317
+ @Deprecated("Deprecated in Java")
318
+ override fun onLowMemory() {
319
+ // left to listen for ui mode changes
320
+ }
303
321
  }
@@ -6,19 +6,13 @@ import android.content.Intent
6
6
  import android.util.Log
7
7
  import com.swmansion.audioapi.AudioAPIModule
8
8
 
9
- /**
10
- * Broadcast receiver for handling recording notification actions and dismissal.
11
- */
12
- class RecordingNotificationReceiver : BroadcastReceiver() {
9
+ class RecordingNotificationReceiver(
10
+ private val module: AudioAPIModule,
11
+ ) : BroadcastReceiver() {
13
12
  companion object {
14
- const val ACTION_NOTIFICATION_DISMISSED = "com.swmansion.audioapi.RECORDING_NOTIFICATION_DISMISSED"
13
+ const val NOTIFICATION_RECORDING_STOPPED = "com.swmansion.audioapi.NOTIFICATION_RECORDING_STOPPED"
14
+ const val NOTIFICATION_RECORDING_RESUMED = "com.swmansion.audioapi.NOTIFICATION_RECORDING_RESUMED"
15
15
  private const val TAG = "RecordingNotificationReceiver"
16
-
17
- private var audioAPIModule: AudioAPIModule? = null
18
-
19
- fun setAudioAPIModule(module: AudioAPIModule?) {
20
- audioAPIModule = module
21
- }
22
16
  }
23
17
 
24
18
  override fun onReceive(
@@ -26,19 +20,14 @@ class RecordingNotificationReceiver : BroadcastReceiver() {
26
20
  intent: Intent?,
27
21
  ) {
28
22
  when (intent?.action) {
29
- ACTION_NOTIFICATION_DISMISSED -> {
30
- Log.d(TAG, "Recording notification dismissed by user")
31
- audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationDismissed", mapOf())
32
- }
33
-
34
- RecordingNotification.ACTION_START -> {
35
- Log.d(TAG, "Start recording action received")
36
- audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationStart", mapOf())
23
+ NOTIFICATION_RECORDING_STOPPED -> {
24
+ Log.d(TAG, "Recording stopped via notification")
25
+ module.invokeHandlerWithEventNameAndEventBody("recordingNotificationPause", mapOf())
37
26
  }
38
27
 
39
- RecordingNotification.ACTION_STOP -> {
40
- Log.d(TAG, "Stop recording action received")
41
- audioAPIModule?.invokeHandlerWithEventNameAndEventBody("recordingNotificationStop", mapOf())
28
+ NOTIFICATION_RECORDING_RESUMED -> {
29
+ Log.d(TAG, "Recording resumed via notification")
30
+ module.invokeHandlerWithEventNameAndEventBody("recordingNotificationResume", mapOf())
42
31
  }
43
32
  }
44
33
  }
@@ -0,0 +1,24 @@
1
+ package com.swmansion.audioapi.system.notification.state
2
+
3
+ import android.content.Intent
4
+ import androidx.core.app.NotificationCompat
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.swmansion.audioapi.system.notification.RecordingNotificationReceiver
7
+
8
+ data class RecordingNotificationState(
9
+ var builder: NotificationCompat.Builder? = null,
10
+ var receiver: RecordingNotificationReceiver? = null,
11
+ var initialized: Boolean,
12
+ var pauseIntent: Intent? = null,
13
+ var resumeIntent: Intent? = null,
14
+ var title: String? = null,
15
+ var contentText: String? = null,
16
+ var paused: Boolean = false,
17
+ var smallIconResourceName: String? = null,
18
+ var largeIconResourceName: String? = null,
19
+ var pauseIconResourceName: String? = null,
20
+ var resumeIconResourceName: String? = null,
21
+ var backgroundColor: Int? = null,
22
+ var cachedRNOptions: ReadableMap? = null,
23
+ var darkTheme: Boolean,
24
+ )
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <ripple xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:color="?android:attr/colorControlHighlight">
4
+ <item android:id="@android:id/mask">
5
+ <shape android:shape="oval">
6
+ <solid android:color="#FFFFFF" />
7
+ </shape>
8
+ </item>
9
+ </ripple>