react-native-audio-api 0.13.4 → 0.13.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.
- package/android/src/main/cpp/audioapi/android/OnLoad.cpp +5 -1
- package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +26 -17
- package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.cpp +41 -0
- package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.h +23 -0
- package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
- package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +136 -30
- package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +47 -12
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +23 -0
- package/android/src/main/java/com/swmansion/audioapi/system/NativeRecorderControl.kt +39 -0
- package/android/src/main/java/com/swmansion/audioapi/system/RecorderState.kt +17 -0
- package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +48 -20
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +221 -227
- package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +122 -9
- package/android/src/main/java/com/swmansion/audioapi/system/notification/state/RecordingNotificationState.kt +17 -11
- package/common/cpp/audioapi/AudioAPIModuleInstaller.h +44 -0
- package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +18 -11
- package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h +1 -0
- package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
- package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.cpp +117 -0
- package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.h +85 -0
- package/common/cpp/audioapi/core/inputs/AudioRecorder.cpp +8 -0
- package/common/cpp/audioapi/core/inputs/AudioRecorder.h +3 -1
- package/common/cpp/audioapi/core/inputs/RecorderState.h +13 -0
- package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +0 -1
- package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +4 -4
- package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +2 -4
- package/common/cpp/audioapi/events/AudioEvent.h +1 -0
- package/ios/audioapi/ios/AudioAPIModule.mm +30 -23
- package/ios/audioapi/ios/core/IOSAudioRecorder.mm +27 -29
- package/ios/audioapi/ios/core/NativeAudioRecorder.h +1 -1
- package/ios/audioapi/ios/core/NativeAudioRecorder.m +9 -7
- package/ios/audioapi/ios/core/utils/FileOptions.mm +18 -5
- package/ios/audioapi/ios/core/utils/IOSFileWriter.h +5 -0
- package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +50 -20
- package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +2 -7
- package/ios/audioapi/ios/system/SystemNotificationManager.mm +4 -1
- package/lib/commonjs/core/AudioRecorder.js +12 -4
- package/lib/commonjs/core/AudioRecorder.js.map +1 -1
- package/lib/commonjs/mock/index.js +11 -1
- package/lib/commonjs/mock/index.js.map +1 -1
- package/lib/commonjs/plugin/withAudioAPI.js +4 -2
- package/lib/commonjs/plugin/withAudioAPI.js.map +1 -1
- package/lib/module/core/AudioRecorder.js +12 -4
- package/lib/module/core/AudioRecorder.js.map +1 -1
- package/lib/module/mock/index.js +11 -1
- package/lib/module/mock/index.js.map +1 -1
- package/lib/module/plugin/withAudioAPI.js +4 -2
- package/lib/module/plugin/withAudioAPI.js.map +1 -1
- package/lib/typescript/core/AudioRecorder.d.ts +2 -0
- package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
- package/lib/typescript/mock/index.d.ts +3 -0
- package/lib/typescript/mock/index.d.ts.map +1 -1
- package/lib/typescript/plugin/withAudioAPI.d.ts +1 -0
- package/lib/typescript/plugin/withAudioAPI.d.ts.map +1 -1
- package/lib/typescript/system/notification/types.d.ts +32 -2
- package/lib/typescript/system/notification/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AudioAPIModule/globals.d.ts +5 -0
- package/src/core/AudioRecorder.ts +17 -4
- package/src/mock/index.ts +14 -1
- package/src/plugin/withAudioAPI.ts +4 -2
- package/src/system/notification/types.ts +32 -2
- package/android/src/main/res/layout/btn_round_ripple.xml +0 -9
- package/android/src/main/res/layout/notification_collapsed.xml +0 -45
- package/android/src/main/res/layout/notification_expanded.xml +0 -44
package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.swmansion.audioapi.system.notification
|
|
2
2
|
|
|
3
|
+
import android.annotation.SuppressLint
|
|
3
4
|
import android.app.Notification
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import androidx.annotation.RequiresPermission
|
|
@@ -8,6 +9,7 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
8
9
|
import com.facebook.react.bridge.ReadableMap
|
|
9
10
|
import com.swmansion.audioapi.system.ForegroundServiceManager
|
|
10
11
|
import java.lang.ref.WeakReference
|
|
12
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Central notification registry that manages multiple notification instances.
|
|
@@ -20,8 +22,9 @@ class NotificationRegistry(
|
|
|
20
22
|
companion object {
|
|
21
23
|
private const val TAG = "NotificationRegistry"
|
|
22
24
|
|
|
23
|
-
// Store last built notifications for foreground service access
|
|
24
|
-
|
|
25
|
+
// Store last built notifications for foreground service access. Concurrent because
|
|
26
|
+
// CentralizedForegroundService reads it on its main thread, outside the registry monitor.
|
|
27
|
+
private val builtNotifications = ConcurrentHashMap<Int, Notification>()
|
|
25
28
|
|
|
26
29
|
fun getBuiltNotification(notificationId: Int): Notification? = builtNotifications[notificationId]
|
|
27
30
|
}
|
|
@@ -39,6 +42,7 @@ class NotificationRegistry(
|
|
|
39
42
|
* @param type The type of notification (only used for first creation)
|
|
40
43
|
* @param options Configuration options from JavaScript
|
|
41
44
|
*/
|
|
45
|
+
@Synchronized
|
|
42
46
|
@RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS)
|
|
43
47
|
fun showNotification(
|
|
44
48
|
key: String,
|
|
@@ -76,11 +80,7 @@ class NotificationRegistry(
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
* Hide a notification.
|
|
81
|
-
*
|
|
82
|
-
* @param key The unique identifier of the notification
|
|
83
|
-
*/
|
|
83
|
+
@Synchronized
|
|
84
84
|
fun hideNotification(key: String) {
|
|
85
85
|
val notification = notifications[key]
|
|
86
86
|
if (notification == null) {
|
|
@@ -88,23 +88,46 @@ class NotificationRegistry(
|
|
|
88
88
|
return
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
notification.hide()
|
|
96
|
-
activeNotifications[key] = false
|
|
97
|
-
|
|
98
|
-
// Unsubscribe from foreground service
|
|
99
|
-
ForegroundServiceManager.unsubscribe(notification)
|
|
91
|
+
// Only hide if currently active
|
|
92
|
+
if (!activeNotifications.getOrDefault(key, false)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
100
95
|
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
try {
|
|
97
|
+
cancelNotification(notification.getNotificationId())
|
|
98
|
+
notification.hide()
|
|
103
99
|
} catch (e: Exception) {
|
|
104
100
|
Log.e(TAG, "Error hiding notification $key: ${e.message}", e)
|
|
101
|
+
} finally {
|
|
102
|
+
activeNotifications[key] = false
|
|
103
|
+
ForegroundServiceManager.unsubscribe(notification)
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
|
|
107
|
+
@Synchronized
|
|
108
|
+
fun hideNotification(id: Int) {
|
|
109
|
+
notifications.entries
|
|
110
|
+
.firstOrNull { it.value.getNotificationId() == id }
|
|
111
|
+
?.let { hideNotification(it.key) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Rebuild and re-post the recording notification with a new paused state.
|
|
116
|
+
*/
|
|
117
|
+
@Synchronized
|
|
118
|
+
@SuppressLint("MissingPermission")
|
|
119
|
+
fun updateRecordingNotificationPausedState(paused: Boolean) {
|
|
120
|
+
val entry =
|
|
121
|
+
notifications.entries.firstOrNull {
|
|
122
|
+
it.value.getNotificationId() == RecordingNotification.ID
|
|
123
|
+
} ?: return
|
|
124
|
+
if (!activeNotifications.getOrDefault(entry.key, false)) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
val recordingNotification = entry.value as? RecordingNotification ?: return
|
|
128
|
+
displayNotification(RecordingNotification.ID, recordingNotification.rebuildWithPausedState(paused))
|
|
129
|
+
}
|
|
130
|
+
|
|
108
131
|
/**
|
|
109
132
|
* Create a notification instance.
|
|
110
133
|
*
|
|
@@ -149,6 +172,7 @@ class NotificationRegistry(
|
|
|
149
172
|
*
|
|
150
173
|
* @param key The unique identifier of the notification
|
|
151
174
|
*/
|
|
175
|
+
@Synchronized
|
|
152
176
|
fun destroyNotification(key: String) {
|
|
153
177
|
hideNotification(key)
|
|
154
178
|
notifications.remove(key)
|
|
@@ -159,16 +183,19 @@ class NotificationRegistry(
|
|
|
159
183
|
/**
|
|
160
184
|
* Check if a notification is currently active.
|
|
161
185
|
*/
|
|
186
|
+
@Synchronized
|
|
162
187
|
fun isNotificationActive(key: String): Boolean = activeNotifications.getOrDefault(key, false)
|
|
163
188
|
|
|
164
189
|
/**
|
|
165
190
|
* Get all registered notification keys.
|
|
166
191
|
*/
|
|
192
|
+
@Synchronized
|
|
167
193
|
fun getRegisteredKeys(): Set<String> = notifications.keys.toSet()
|
|
168
194
|
|
|
169
195
|
/**
|
|
170
196
|
* Cleanup all notifications.
|
|
171
197
|
*/
|
|
198
|
+
@Synchronized
|
|
172
199
|
fun cleanup() {
|
|
173
200
|
notifications.keys.toList().forEach { key ->
|
|
174
201
|
hideNotification(key)
|
|
@@ -202,9 +229,10 @@ class NotificationRegistry(
|
|
|
202
229
|
}
|
|
203
230
|
|
|
204
231
|
private fun cancelNotification(id: Int) {
|
|
232
|
+
// Drop the stored notification first so the foreground service can no longer pick
|
|
233
|
+
// it up, even when the released React context prevents the system-side cancel.
|
|
234
|
+
builtNotifications.remove(id)
|
|
205
235
|
val context = reactContext.get() ?: return
|
|
206
236
|
NotificationManagerCompat.from(context).cancel(id)
|
|
207
|
-
// Clean up stored notification
|
|
208
|
-
builtNotifications.remove(id)
|
|
209
237
|
}
|
|
210
238
|
}
|
package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt
CHANGED
|
@@ -4,23 +4,18 @@ 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
|
|
8
7
|
import android.content.Context
|
|
9
8
|
import android.content.Intent
|
|
10
9
|
import android.content.IntentFilter
|
|
11
|
-
import android.content.res.Configuration
|
|
12
|
-
import android.graphics.Color
|
|
13
10
|
import android.graphics.drawable.Icon
|
|
11
|
+
import android.net.Uri
|
|
14
12
|
import android.os.Build
|
|
15
13
|
import android.util.Log
|
|
16
|
-
import android.widget.RemoteViews
|
|
17
|
-
import androidx.annotation.RequiresApi
|
|
18
14
|
import androidx.core.app.NotificationCompat
|
|
19
15
|
import androidx.core.content.ContextCompat
|
|
20
16
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
21
17
|
import com.facebook.react.bridge.ReadableMap
|
|
22
18
|
import com.swmansion.audioapi.AudioAPIModule
|
|
23
|
-
import com.swmansion.audioapi.R
|
|
24
19
|
import com.swmansion.audioapi.system.notification.state.RecordingNotificationState
|
|
25
20
|
import java.lang.ref.WeakReference
|
|
26
21
|
|
|
@@ -29,196 +24,242 @@ class RecordingNotification(
|
|
|
29
24
|
private val audioAPIModule: WeakReference<AudioAPIModule>,
|
|
30
25
|
private val notificationId: Int,
|
|
31
26
|
private val channelId: String,
|
|
32
|
-
) : BaseNotification
|
|
33
|
-
ComponentCallbacks {
|
|
27
|
+
) : BaseNotification {
|
|
34
28
|
companion object {
|
|
35
29
|
private const val TAG = "RecordingNotification"
|
|
36
30
|
const val ID = 200
|
|
31
|
+
|
|
32
|
+
private const val REQUEST_CODE_CONTENT = 2000
|
|
33
|
+
private const val REQUEST_CODE_PAUSE = 2001
|
|
34
|
+
private const val REQUEST_CODE_RESUME = 2002
|
|
35
|
+
private const val REQUEST_CODE_STOP = 2003
|
|
36
|
+
private const val REQUEST_CODE_DISMISSED = 2004
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
private
|
|
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
|
-
)
|
|
39
|
+
private val state = RecordingNotificationState()
|
|
47
40
|
|
|
48
41
|
private fun initializeNotification() {
|
|
49
42
|
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
50
|
-
if (
|
|
51
|
-
|
|
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
|
-
)
|
|
66
|
-
|
|
67
|
-
state.pauseIntent =
|
|
68
|
-
Intent(RecordingNotificationReceiver.NOTIFICATION_RECORDING_STOPPED).apply {
|
|
69
|
-
`package` = context.packageName
|
|
70
|
-
}
|
|
71
|
-
|
|
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
|
|
43
|
+
if (state.initialized) {
|
|
44
|
+
return
|
|
78
45
|
}
|
|
46
|
+
|
|
47
|
+
createNotificationChannel(context)
|
|
48
|
+
state.receiver = RecordingNotificationReceiver(audioAPIModule.get()!!)
|
|
49
|
+
val filter =
|
|
50
|
+
IntentFilter().apply {
|
|
51
|
+
addAction(RecordingNotificationReceiver.ACTION_PAUSE)
|
|
52
|
+
addAction(RecordingNotificationReceiver.ACTION_RESUME)
|
|
53
|
+
addAction(RecordingNotificationReceiver.ACTION_STOP)
|
|
54
|
+
addAction(RecordingNotificationReceiver.ACTION_DISMISSED)
|
|
55
|
+
}
|
|
56
|
+
ContextCompat.registerReceiver(
|
|
57
|
+
context,
|
|
58
|
+
state.receiver,
|
|
59
|
+
filter,
|
|
60
|
+
ContextCompat.RECEIVER_NOT_EXPORTED,
|
|
61
|
+
)
|
|
62
|
+
state.initialized = true
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
override fun show(options: ReadableMap?): Notification {
|
|
82
66
|
initializeNotification()
|
|
83
67
|
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
84
|
-
if (options
|
|
68
|
+
if (options !== state.cachedRNOptions) {
|
|
85
69
|
state.cachedRNOptions = options
|
|
86
70
|
parseMapFromRN(options)
|
|
87
71
|
}
|
|
88
|
-
|
|
72
|
+
return buildNotification(context)
|
|
73
|
+
}
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Rebuilds with an updated paused flag, leaving the sticky RN options untouched.
|
|
77
|
+
*/
|
|
78
|
+
fun rebuildWithPausedState(paused: Boolean): Notification {
|
|
79
|
+
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
80
|
+
state.paused = paused
|
|
81
|
+
return buildNotification(context)
|
|
82
|
+
}
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
private fun buildNotification(context: ReactApplicationContext): Notification {
|
|
85
|
+
val builder =
|
|
86
|
+
NotificationCompat
|
|
87
|
+
.Builder(context, channelId)
|
|
88
|
+
.setOngoing(!state.dismissible)
|
|
89
|
+
.setOnlyAlertOnce(true)
|
|
90
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
91
|
+
.setContentTitle(state.title)
|
|
92
|
+
.setContentText(state.contentText)
|
|
93
|
+
.setSmallIcon(
|
|
94
|
+
resolveDrawable(context, state.smallIconResourceName) ?: android.R.drawable.ic_btn_speak_now,
|
|
99
95
|
)
|
|
100
|
-
builder.setLargeIcon(icon)
|
|
101
|
-
}
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
builder.
|
|
97
|
+
resolveDrawable(context, state.largeIconResourceName)?.let {
|
|
98
|
+
builder.setLargeIcon(Icon.createWithResource(context, it))
|
|
105
99
|
}
|
|
100
|
+
state.backgroundColor?.let { builder.setColor(it) }
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
setupContentIntent(context, builder)
|
|
103
|
+
setupActions(context, builder)
|
|
104
|
+
setupDeleteIntent(context, builder)
|
|
105
|
+
setupChronometer(builder)
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
return builder.build()
|
|
108
|
+
}
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
private fun setupContentIntent(
|
|
111
|
+
context: Context,
|
|
112
|
+
builder: NotificationCompat.Builder,
|
|
113
|
+
) {
|
|
114
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
|
115
|
+
state.deepLinkUri?.let {
|
|
116
|
+
launchIntent.action = Intent.ACTION_VIEW
|
|
117
|
+
launchIntent.removeCategory(Intent.CATEGORY_LAUNCHER)
|
|
118
|
+
launchIntent.data = Uri.parse(it)
|
|
119
|
+
}
|
|
120
|
+
builder.setContentIntent(
|
|
121
|
+
PendingIntent.getActivity(
|
|
122
|
+
context,
|
|
123
|
+
REQUEST_CODE_CONTENT,
|
|
124
|
+
launchIntent,
|
|
125
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.
|
|
130
|
+
private fun setupActions(
|
|
131
|
+
context: Context,
|
|
132
|
+
builder: NotificationCompat.Builder,
|
|
133
|
+
) {
|
|
134
|
+
if (state.paused) {
|
|
135
|
+
builder.addAction(
|
|
136
|
+
createAction(
|
|
137
|
+
context,
|
|
138
|
+
RecordingNotificationReceiver.ACTION_RESUME,
|
|
139
|
+
REQUEST_CODE_RESUME,
|
|
140
|
+
state.resumeActionTitle ?: "Resume",
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
builder.addAction(
|
|
145
|
+
createAction(
|
|
146
|
+
context,
|
|
147
|
+
RecordingNotificationReceiver.ACTION_PAUSE,
|
|
148
|
+
REQUEST_CODE_PAUSE,
|
|
149
|
+
state.pauseActionTitle ?: "Pause",
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
120
153
|
|
|
121
|
-
if (state.
|
|
122
|
-
builder.
|
|
154
|
+
if (state.showStopAction) {
|
|
155
|
+
builder.addAction(
|
|
156
|
+
createAction(
|
|
157
|
+
context,
|
|
158
|
+
RecordingNotificationReceiver.ACTION_STOP,
|
|
159
|
+
REQUEST_CODE_STOP,
|
|
160
|
+
state.stopActionTitle ?: "Stop",
|
|
161
|
+
),
|
|
162
|
+
)
|
|
123
163
|
}
|
|
164
|
+
}
|
|
124
165
|
|
|
125
|
-
|
|
166
|
+
private fun setupDeleteIntent(
|
|
167
|
+
context: Context,
|
|
168
|
+
builder: NotificationCompat.Builder,
|
|
169
|
+
) {
|
|
170
|
+
val dismissed =
|
|
171
|
+
Intent(RecordingNotificationReceiver.ACTION_DISMISSED)
|
|
172
|
+
.putExtra(RecordingNotificationReceiver.EXTRA_DISMISS_STOPS_RECORDING, state.dismissible)
|
|
173
|
+
builder.setDeleteIntent(createBroadcast(context, dismissed, REQUEST_CODE_DISMISSED))
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
private fun
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
176
|
+
private fun createAction(
|
|
177
|
+
context: Context,
|
|
178
|
+
action: String,
|
|
179
|
+
requestCode: Int,
|
|
180
|
+
title: String,
|
|
181
|
+
): NotificationCompat.Action = NotificationCompat.Action(null, title, createBroadcast(context, Intent(action), requestCode))
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
183
|
+
private fun createBroadcast(
|
|
184
|
+
context: Context,
|
|
185
|
+
intent: Intent,
|
|
186
|
+
requestCode: Int,
|
|
187
|
+
): PendingIntent =
|
|
188
|
+
PendingIntent.getBroadcast(
|
|
189
|
+
context,
|
|
190
|
+
requestCode,
|
|
191
|
+
intent.apply { `package` = context.packageName },
|
|
192
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
// The system chronometer always ticks against wall time, so the recording's paused
|
|
196
|
+
// spans are carved out by shifting the base (`startedAtMs`) forward on each resume.
|
|
197
|
+
private fun setupChronometer(builder: NotificationCompat.Builder) {
|
|
198
|
+
val now = System.currentTimeMillis()
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
if (state.
|
|
146
|
-
|
|
147
|
-
} else {
|
|
148
|
-
android.R.drawable.ic_media_pause
|
|
200
|
+
if (state.usesChronometer && !state.paused) {
|
|
201
|
+
if (state.startedAtMs == null) {
|
|
202
|
+
state.startedAtMs = now
|
|
149
203
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} else {
|
|
154
|
-
android.R.drawable.ic_media_play
|
|
204
|
+
state.pausedAtMs?.let { pausedAt ->
|
|
205
|
+
state.startedAtMs = state.startedAtMs!! + (now - pausedAt)
|
|
206
|
+
state.pausedAtMs = null
|
|
155
207
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
208
|
+
builder
|
|
209
|
+
.setWhen(state.startedAtMs!!)
|
|
210
|
+
.setShowWhen(true)
|
|
211
|
+
.setUsesChronometer(true)
|
|
212
|
+
} else {
|
|
213
|
+
if (state.usesChronometer && state.paused && state.pausedAtMs == null) {
|
|
214
|
+
state.pausedAtMs = now
|
|
215
|
+
}
|
|
216
|
+
builder
|
|
217
|
+
.setUsesChronometer(false)
|
|
218
|
+
.setShowWhen(false)
|
|
219
|
+
}
|
|
159
220
|
}
|
|
160
221
|
|
|
161
|
-
private fun
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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)
|
|
222
|
+
private fun resolveDrawable(
|
|
223
|
+
context: Context,
|
|
224
|
+
resourceName: String?,
|
|
225
|
+
): Int? {
|
|
226
|
+
if (resourceName == null) {
|
|
227
|
+
return null
|
|
178
228
|
}
|
|
229
|
+
val resourceId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
|
|
230
|
+
return if (resourceId != 0) resourceId else null
|
|
179
231
|
}
|
|
180
232
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
// }
|
|
233
|
+
private fun parseMapFromRN(options: ReadableMap?) {
|
|
234
|
+
state.title = options.stringOr("title", state.title ?: "Recording Audio")
|
|
235
|
+
state.contentText = options.stringOr("contentText", state.contentText ?: "Audio recording is in progress/paused")
|
|
236
|
+
state.smallIconResourceName = options.stringOr("smallIconResourceName", state.smallIconResourceName)
|
|
237
|
+
state.largeIconResourceName = options.stringOr("largeIconResourceName", state.largeIconResourceName)
|
|
238
|
+
state.backgroundColor = options.intOr("color", state.backgroundColor)
|
|
239
|
+
state.showStopAction = options.boolOr("showStopAction", state.showStopAction)
|
|
240
|
+
state.dismissible = options.boolOr("dismissible", state.dismissible)
|
|
241
|
+
state.pauseActionTitle = options.stringOr("pauseActionTitle", state.pauseActionTitle)
|
|
242
|
+
state.resumeActionTitle = options.stringOr("resumeActionTitle", state.resumeActionTitle)
|
|
243
|
+
state.stopActionTitle = options.stringOr("stopActionTitle", state.stopActionTitle)
|
|
244
|
+
state.deepLinkUri = options.stringOr("deepLinkUri", state.deepLinkUri)
|
|
245
|
+
state.usesChronometer = options.boolOr("usesChronometer", state.usesChronometer)
|
|
246
|
+
state.paused = options.boolOr("paused", state.paused)
|
|
247
|
+
}
|
|
204
248
|
|
|
205
|
-
private fun
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
val pendingIntent = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_IMMUTABLE)
|
|
249
|
+
private fun ReadableMap?.stringOr(
|
|
250
|
+
key: String,
|
|
251
|
+
fallback: String?,
|
|
252
|
+
): String? = if (this?.hasKey(key) == true) getString(key) else fallback
|
|
210
253
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return state.builder!!
|
|
221
|
-
}
|
|
254
|
+
private fun ReadableMap?.boolOr(
|
|
255
|
+
key: String,
|
|
256
|
+
fallback: Boolean,
|
|
257
|
+
): Boolean = if (this?.hasKey(key) == true) getBoolean(key) else fallback
|
|
258
|
+
|
|
259
|
+
private fun ReadableMap?.intOr(
|
|
260
|
+
key: String,
|
|
261
|
+
fallback: Int?,
|
|
262
|
+
): Int? = if (this?.hasKey(key) == true) getInt(key) else fallback
|
|
222
263
|
|
|
223
264
|
private fun createNotificationChannel(context: ReactApplicationContext) {
|
|
224
265
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -238,84 +279,37 @@ class RecordingNotification(
|
|
|
238
279
|
Log.d(TAG, "Notification channel created: $channelId")
|
|
239
280
|
}
|
|
240
281
|
|
|
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")
|
|
270
|
-
} else {
|
|
271
|
-
state.pauseIconResourceName ?: null
|
|
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
|
-
}
|
|
284
|
-
|
|
285
282
|
override fun hide() {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
try {
|
|
284
|
+
val context = reactContext.get() ?: throw IllegalStateException("React context is null")
|
|
285
|
+
state.receiver?.let {
|
|
286
|
+
context.unregisterReceiver(it)
|
|
287
|
+
state.receiver = null
|
|
288
|
+
}
|
|
289
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
290
|
+
notificationManager.cancel(notificationId)
|
|
291
|
+
} finally {
|
|
292
|
+
state.initialized = false
|
|
293
|
+
state.cachedRNOptions = null
|
|
294
|
+
state.title = null
|
|
295
|
+
state.contentText = null
|
|
296
|
+
state.smallIconResourceName = null
|
|
297
|
+
state.largeIconResourceName = null
|
|
298
|
+
state.backgroundColor = null
|
|
299
|
+
state.showStopAction = false
|
|
300
|
+
state.dismissible = false
|
|
301
|
+
state.pauseActionTitle = null
|
|
302
|
+
state.resumeActionTitle = null
|
|
303
|
+
state.stopActionTitle = null
|
|
304
|
+
state.deepLinkUri = null
|
|
305
|
+
state.usesChronometer = false
|
|
306
|
+
state.startedAtMs = null
|
|
307
|
+
state.pausedAtMs = null
|
|
308
|
+
state.paused = false
|
|
291
309
|
}
|
|
292
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
293
|
-
notificationManager.cancel(notificationId)
|
|
294
|
-
state.initialized = false
|
|
295
|
-
state.builder = null
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
override fun getNotificationId(): Int = notificationId
|
|
299
313
|
|
|
300
314
|
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)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
@Deprecated("Deprecated in Java")
|
|
318
|
-
override fun onLowMemory() {
|
|
319
|
-
// left to listen for ui mode changes
|
|
320
|
-
}
|
|
321
315
|
}
|