@siteed/expo-audio-stream 2.1.0 → 2.2.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 (187) hide show
  1. package/README.md +40 -222
  2. package/build/index.d.ts +11 -15
  3. package/build/index.js +44 -14
  4. package/package.json +49 -110
  5. package/src/index.ts +18 -32
  6. package/CHANGELOG.md +0 -206
  7. package/android/build.gradle +0 -105
  8. package/android/src/main/AndroidManifest.xml +0 -27
  9. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  10. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  11. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  12. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  13. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  14. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
  15. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  16. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
  17. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  18. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
  19. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  20. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
  21. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  22. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  23. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  24. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  25. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  26. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  27. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  28. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  29. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  30. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  31. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  32. package/android/src/main/res/drawable/ic_play.xml +0 -10
  33. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  34. package/android/src/main/res/layout/notification_recording.xml +0 -37
  35. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  36. package/app.plugin.js +0 -1
  37. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
  38. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  39. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  40. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  41. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
  42. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  43. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
  44. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  45. package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
  46. package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  47. package/build/AudioAnalysis/extractAudioData.js +0 -5
  48. package/build/AudioAnalysis/extractAudioData.js.map +0 -1
  49. package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  50. package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  51. package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
  52. package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  53. package/build/AudioAnalysis/extractPreview.d.ts +0 -11
  54. package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
  55. package/build/AudioAnalysis/extractPreview.js +0 -25
  56. package/build/AudioAnalysis/extractPreview.js.map +0 -1
  57. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  58. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  59. package/build/AudioAnalysis/extractWaveform.js +0 -11
  60. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  61. package/build/AudioRecorder.provider.d.ts +0 -11
  62. package/build/AudioRecorder.provider.d.ts.map +0 -1
  63. package/build/AudioRecorder.provider.js +0 -37
  64. package/build/AudioRecorder.provider.js.map +0 -1
  65. package/build/ExpoAudioStream.native.d.ts +0 -3
  66. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  67. package/build/ExpoAudioStream.native.js +0 -6
  68. package/build/ExpoAudioStream.native.js.map +0 -1
  69. package/build/ExpoAudioStream.types.d.ts +0 -532
  70. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  71. package/build/ExpoAudioStream.types.js +0 -2
  72. package/build/ExpoAudioStream.types.js.map +0 -1
  73. package/build/ExpoAudioStream.web.d.ts +0 -59
  74. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  75. package/build/ExpoAudioStream.web.js +0 -285
  76. package/build/ExpoAudioStream.web.js.map +0 -1
  77. package/build/ExpoAudioStreamModule.d.ts +0 -3
  78. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.js +0 -693
  80. package/build/ExpoAudioStreamModule.js.map +0 -1
  81. package/build/WebRecorder.web.d.ts +0 -119
  82. package/build/WebRecorder.web.d.ts.map +0 -1
  83. package/build/WebRecorder.web.js +0 -436
  84. package/build/WebRecorder.web.js.map +0 -1
  85. package/build/constants.d.ts +0 -11
  86. package/build/constants.d.ts.map +0 -1
  87. package/build/constants.js +0 -14
  88. package/build/constants.js.map +0 -1
  89. package/build/events.d.ts +0 -26
  90. package/build/events.d.ts.map +0 -1
  91. package/build/events.js +0 -21
  92. package/build/events.js.map +0 -1
  93. package/build/index.d.ts.map +0 -1
  94. package/build/index.js.map +0 -1
  95. package/build/trimAudio.d.ts +0 -25
  96. package/build/trimAudio.d.ts.map +0 -1
  97. package/build/trimAudio.js +0 -67
  98. package/build/trimAudio.js.map +0 -1
  99. package/build/useAudioRecorder.d.ts +0 -21
  100. package/build/useAudioRecorder.d.ts.map +0 -1
  101. package/build/useAudioRecorder.js +0 -427
  102. package/build/useAudioRecorder.js.map +0 -1
  103. package/build/utils/BlobFix.d.ts +0 -9
  104. package/build/utils/BlobFix.d.ts.map +0 -1
  105. package/build/utils/BlobFix.js +0 -498
  106. package/build/utils/BlobFix.js.map +0 -1
  107. package/build/utils/audioProcessing.d.ts +0 -24
  108. package/build/utils/audioProcessing.d.ts.map +0 -1
  109. package/build/utils/audioProcessing.js +0 -133
  110. package/build/utils/audioProcessing.js.map +0 -1
  111. package/build/utils/concatenateBuffers.d.ts +0 -8
  112. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  113. package/build/utils/concatenateBuffers.js +0 -21
  114. package/build/utils/concatenateBuffers.js.map +0 -1
  115. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  116. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  117. package/build/utils/convertPCMToFloat32.js +0 -120
  118. package/build/utils/convertPCMToFloat32.js.map +0 -1
  119. package/build/utils/encodingToBitDepth.d.ts +0 -5
  120. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  121. package/build/utils/encodingToBitDepth.js +0 -13
  122. package/build/utils/encodingToBitDepth.js.map +0 -1
  123. package/build/utils/getWavFileInfo.d.ts +0 -26
  124. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  125. package/build/utils/getWavFileInfo.js +0 -92
  126. package/build/utils/getWavFileInfo.js.map +0 -1
  127. package/build/utils/writeWavHeader.d.ts +0 -49
  128. package/build/utils/writeWavHeader.d.ts.map +0 -1
  129. package/build/utils/writeWavHeader.js +0 -91
  130. package/build/utils/writeWavHeader.js.map +0 -1
  131. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  132. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  133. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  134. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  135. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  136. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  137. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  138. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  139. package/expo-module.config.json +0 -9
  140. package/ios/AudioAnalysisData.swift +0 -74
  141. package/ios/AudioNotificationManager.swift +0 -135
  142. package/ios/AudioProcessingHelpers.swift +0 -743
  143. package/ios/AudioProcessor.swift +0 -1313
  144. package/ios/AudioStreamError.swift +0 -7
  145. package/ios/AudioStreamManager.swift +0 -1708
  146. package/ios/AudioStreamManagerDelegate.swift +0 -16
  147. package/ios/DataPoint.swift +0 -54
  148. package/ios/DecodingConfig.swift +0 -47
  149. package/ios/ExpoAudioStream.podspec +0 -27
  150. package/ios/ExpoAudioStreamModule.swift +0 -805
  151. package/ios/FFT.swift +0 -62
  152. package/ios/Features.swift +0 -95
  153. package/ios/Logger.swift +0 -7
  154. package/ios/NotificationExtension.swift +0 -15
  155. package/ios/RecordingResult.swift +0 -22
  156. package/ios/RecordingSettings.swift +0 -265
  157. package/ios/WaveformExtractor.swift +0 -105
  158. package/plugin/build/index.d.ts +0 -21
  159. package/plugin/build/index.js +0 -191
  160. package/plugin/src/index.ts +0 -278
  161. package/plugin/tsconfig.json +0 -10
  162. package/plugin/tsconfig.tsbuildinfo +0 -1
  163. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
  164. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
  165. package/src/AudioAnalysis/extractAudioData.ts +0 -6
  166. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
  167. package/src/AudioAnalysis/extractPreview.ts +0 -34
  168. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  169. package/src/AudioRecorder.provider.tsx +0 -54
  170. package/src/ExpoAudioStream.native.ts +0 -6
  171. package/src/ExpoAudioStream.types.ts +0 -641
  172. package/src/ExpoAudioStream.web.ts +0 -359
  173. package/src/ExpoAudioStreamModule.ts +0 -967
  174. package/src/WebRecorder.web.ts +0 -580
  175. package/src/constants.ts +0 -18
  176. package/src/events.ts +0 -60
  177. package/src/trimAudio.ts +0 -90
  178. package/src/useAudioRecorder.tsx +0 -620
  179. package/src/utils/BlobFix.ts +0 -559
  180. package/src/utils/audioProcessing.ts +0 -205
  181. package/src/utils/concatenateBuffers.ts +0 -24
  182. package/src/utils/convertPCMToFloat32.ts +0 -170
  183. package/src/utils/encodingToBitDepth.ts +0 -18
  184. package/src/utils/getWavFileInfo.ts +0 -132
  185. package/src/utils/writeWavHeader.ts +0 -114
  186. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  187. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,103 +0,0 @@
1
- package net.siteed.audiostream
2
-
3
- import android.media.AudioFormat
4
- import java.nio.ByteBuffer
5
- import java.nio.ByteOrder
6
-
7
- object AudioFormatUtils {
8
- /**
9
- * Converts a byte array of audio data to a float array based on the given encoding.
10
- * @param audioData The raw audio data in bytes.
11
- * @param encoding The encoding format (e.g., "pcm_8bit", "pcm_16bit", "pcm_32bit").
12
- * @return A float array with normalized audio samples in the range [-1.0, 1.0].
13
- */
14
- fun convertByteArrayToFloatArray(audioData: ByteArray, encoding: String): FloatArray {
15
- return when (encoding) {
16
- "pcm_8bit" -> {
17
- val floatArray = FloatArray(audioData.size)
18
- for (i in audioData.indices) {
19
- // Convert unsigned 8-bit to float in range [-1.0, 1.0]
20
- floatArray[i] = ((audioData[i].toInt() and 0xFF) - 128) / 128.0f
21
- }
22
- floatArray
23
- }
24
- "pcm_16bit" -> {
25
- val floatArray = FloatArray(audioData.size / 2)
26
- val buffer = ByteBuffer.wrap(audioData).order(ByteOrder.LITTLE_ENDIAN)
27
- for (i in floatArray.indices) {
28
- floatArray[i] = buffer.short / 32768.0f // Normalize to [-1.0, 1.0]
29
- }
30
- floatArray
31
- }
32
- "pcm_32bit" -> {
33
- val floatArray = FloatArray(audioData.size / 4)
34
- val buffer = ByteBuffer.wrap(audioData).order(ByteOrder.LITTLE_ENDIAN)
35
- for (i in floatArray.indices) {
36
- floatArray[i] = buffer.int / 2_147_483_648.0f // Normalize to [-1.0, 1.0]
37
- }
38
- floatArray
39
- }
40
- else -> {
41
- // Default to 16-bit PCM if encoding is not recognized
42
- val floatArray = FloatArray(audioData.size / 2)
43
- val buffer = ByteBuffer.wrap(audioData).order(ByteOrder.LITTLE_ENDIAN)
44
- for (i in floatArray.indices) {
45
- floatArray[i] = buffer.short / 32768.0f
46
- }
47
- floatArray
48
- }
49
- }
50
- }
51
-
52
- /**
53
- * Calculates the bit depth (number of bits per sample) based on the encoding string.
54
- * @param encoding The encoding format (e.g., "pcm_8bit", "pcm_16bit", "pcm_32bit").
55
- * @return The bit depth as an integer.
56
- */
57
- fun getBitDepth(encoding: String): Int {
58
- return when (encoding) {
59
- "pcm_8bit" -> 8
60
- "pcm_16bit" -> 16
61
- "pcm_32bit" -> 32
62
- else -> 16 // Default to 16-bit if not recognized
63
- }
64
- }
65
-
66
- /**
67
- * Determines the AudioFormat encoding constant based on the encoding string.
68
- * @param encoding The encoding format (e.g., "pcm_8bit", "pcm_16bit", "pcm_32bit").
69
- * @return The corresponding AudioFormat constant.
70
- */
71
- fun getAudioFormat(encoding: String): Int {
72
- return when (encoding) {
73
- "pcm_8bit" -> AudioFormat.ENCODING_PCM_8BIT
74
- "pcm_16bit" -> AudioFormat.ENCODING_PCM_16BIT
75
- "pcm_32bit" -> AudioFormat.ENCODING_PCM_FLOAT
76
- else -> AudioFormat.ENCODING_PCM_16BIT // Default to 16-bit PCM
77
- }
78
- }
79
-
80
- /**
81
- * Converts audio data between different bit depths
82
- * @param audioData The raw audio data
83
- * @param sourceBitDepth The original bit depth
84
- * @param targetBitDepth The desired bit depth
85
- * @return The converted audio data
86
- */
87
- fun convertBitDepth(audioData: ByteArray, sourceBitDepth: Int, targetBitDepth: Int): ByteArray {
88
- // First convert to float array for normalization
89
- val floatArray = convertByteArrayToFloatArray(audioData, "pcm_${sourceBitDepth}bit")
90
-
91
- // Convert back to bytes with new bit depth
92
- return when (targetBitDepth) {
93
- 8 -> floatArray.map { ((it + 1.0f) * 127.5f).toInt().toByte() }.toByteArray()
94
- 16 -> ByteBuffer.allocate(floatArray.size * 2).order(ByteOrder.LITTLE_ENDIAN).apply {
95
- floatArray.forEach { asShortBuffer().put((it * 32767f).toInt().toShort()) }
96
- }.array()
97
- 32 -> ByteBuffer.allocate(floatArray.size * 4).order(ByteOrder.LITTLE_ENDIAN).apply {
98
- floatArray.forEach { putFloat(it) }
99
- }.array()
100
- else -> throw IllegalArgumentException("Unsupported target bit depth: $targetBitDepth")
101
- }
102
- }
103
- }
@@ -1,435 +0,0 @@
1
- package net.siteed.audiostream
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.graphics.Color
10
- import android.os.Build
11
- import android.os.Handler
12
- import android.os.Looper
13
- import android.os.SystemClock
14
- import android.util.Log
15
- import android.view.View
16
- import android.widget.RemoteViews
17
- import androidx.core.app.NotificationCompat
18
- import java.lang.ref.WeakReference
19
- import java.util.Locale
20
- import java.util.concurrent.atomic.AtomicBoolean
21
- import java.util.Objects
22
-
23
- class AudioNotificationManager private constructor(context: Context) {
24
- private val contextRef = WeakReference(context.applicationContext)
25
- private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
26
- private val mainHandler = Handler(Looper.getMainLooper())
27
- private val isUpdating = AtomicBoolean(false)
28
- private val isPaused = AtomicBoolean(false)
29
-
30
- private var lastRemoteViewsUpdate = 0L
31
- private var consecutiveUpdateFailures = 0
32
- private var lastSuccessfulUpdate: Long = 0
33
- private val maxUpdateFailures = 3
34
- private val remoteViewsRefreshInterval = 10000L // Refresh RemoteViews every 10 seconds
35
-
36
- private lateinit var notificationBuilder: NotificationCompat.Builder
37
- private lateinit var remoteViews: RemoteViews
38
- private lateinit var recordingConfig: RecordingConfig
39
- private var recordingStartTime: Long = 0
40
- private var pausedDuration: Long = 0
41
- private var lastPauseTime: Long = 0
42
- private var lastWaveformUpdate: Long = 0
43
- private val waveformRenderer = WaveformRenderer()
44
- private var lastNotificationHash: Int? = null
45
-
46
- companion object {
47
- private const val WAVEFORM_UPDATE_INTERVAL = 100L
48
- private const val UPDATE_INTERVAL = 1000L
49
-
50
- @Volatile
51
- private var instance: AudioNotificationManager? = null
52
-
53
- fun getInstance(context: Context): AudioNotificationManager {
54
- return instance ?: synchronized(this) {
55
- instance ?: AudioNotificationManager(context).also { instance = it }
56
- }
57
- }
58
- }
59
-
60
- fun initialize(config: RecordingConfig) {
61
- recordingConfig = config
62
- createNotificationChannel()
63
- initializeNotification()
64
- }
65
-
66
- private fun createNotificationChannel() {
67
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
68
- val channel = NotificationChannel(
69
- recordingConfig.notification.channelId,
70
- recordingConfig.notification.channelName,
71
- NotificationManager.IMPORTANCE_LOW
72
- ).apply {
73
- description = recordingConfig.notification.channelDescription
74
- enableLights(false)
75
- enableVibration(false)
76
- setSound(null, null)
77
- setShowBadge(false)
78
- vibrationPattern = null
79
- }
80
-
81
- notificationManager.createNotificationChannel(channel)
82
- }
83
- }
84
-
85
- private fun initializeNotification() {
86
- val context = contextRef.get() ?: return
87
- try {
88
- remoteViews = RemoteViews(context.packageName, R.layout.notification_recording)
89
- remoteViews.apply {
90
- setTextViewText(R.id.notification_title, recordingConfig.notification.title)
91
- setTextViewText(R.id.notification_text, recordingConfig.notification.text)
92
- setTextViewText(R.id.notification_duration, formatDuration(0))
93
- setViewVisibility(
94
- R.id.notification_waveform,
95
- if (recordingConfig.showWaveformInNotification &&
96
- recordingConfig.notification.waveform != null) View.VISIBLE else View.GONE
97
- )
98
- }
99
-
100
- buildNotification(context)
101
- } catch (e: Exception) {
102
- Log.e(Constants.TAG, "Failed to initialize notification", e)
103
- }
104
- }
105
-
106
- private fun buildNotification(context: Context) {
107
- val iconResId = recordingConfig.notification.icon?.let {
108
- getResourceIdByName(it)
109
- } ?: R.drawable.ic_microphone
110
-
111
- val pendingIntent = PendingIntent.getActivity(
112
- context,
113
- 0,
114
- context.packageManager.getLaunchIntentForPackage(context.packageName),
115
- PendingIntent.FLAG_IMMUTABLE
116
- )
117
-
118
- // Configure notification builder with settings optimized for recording service
119
- // and wearable device compatibility
120
- notificationBuilder = NotificationCompat.Builder(context, recordingConfig.notification.channelId)
121
- .setSmallIcon(iconResId)
122
- .setContentIntent(pendingIntent)
123
- .setOngoing(true) // Notification cannot be dismissed by user
124
- .setPriority(NotificationCompat.PRIORITY_HIGH)
125
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
126
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
127
- .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
128
- .setCustomContentView(remoteViews)
129
- .setCustomBigContentView(remoteViews)
130
- .setStyle(NotificationCompat.DecoratedCustomViewStyle())
131
- // Prevent repeated alerts and vibrations
132
- .setOnlyAlertOnce(true) // Only alert on first notification
133
- .setVibrate(null) // Disable vibration
134
- .setDefaults(0) // Clear all default notification behaviors
135
- .setLocalOnly(true) // Prevent notification from appearing on wearable devices
136
-
137
- addNotificationActions(context)
138
- }
139
-
140
- private fun addNotificationActions(context: Context) {
141
- // Clear existing actions first
142
- notificationBuilder.clearActions()
143
-
144
- // Create pause action
145
- val pauseIntent = Intent(context, RecordingActionReceiver::class.java).apply {
146
- action = RecordingActionReceiver.ACTION_PAUSE_RECORDING
147
- }
148
- val pausePendingIntent = PendingIntent.getBroadcast(
149
- context,
150
- 0,
151
- pauseIntent,
152
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
153
- )
154
-
155
- // Create resume action
156
- val resumeIntent = Intent(context, RecordingActionReceiver::class.java).apply {
157
- action = RecordingActionReceiver.ACTION_RESUME_RECORDING
158
- }
159
- val resumePendingIntent = PendingIntent.getBroadcast(
160
- context,
161
- 1,
162
- resumeIntent,
163
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
164
- )
165
-
166
- // Add only one pause/resume action based on current state
167
- if (isPaused.get()) {
168
- notificationBuilder.addAction(
169
- R.drawable.ic_play,
170
- "Resume",
171
- resumePendingIntent
172
- )
173
- } else {
174
- notificationBuilder.addAction(
175
- R.drawable.ic_pause,
176
- "Pause",
177
- pausePendingIntent
178
- )
179
- }
180
-
181
- // Add configured custom actions (only if they don't already exist)
182
- val existingActions = mutableSetOf<String>()
183
- recordingConfig.notification.actions.forEach { action ->
184
- if (existingActions.add(action.intentAction)) { // Only add if action is unique
185
- val intent = Intent(context, RecordingActionReceiver::class.java).apply {
186
- this.action = action.intentAction
187
- }
188
- val pendingIntent = PendingIntent.getBroadcast(
189
- context,
190
- action.intentAction.hashCode(),
191
- intent,
192
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
193
- )
194
- val actionIconResId = action.icon?.let { getResourceIdByName(it) }
195
- ?: R.drawable.ic_default_action_icon
196
- notificationBuilder.addAction(actionIconResId, action.title, pendingIntent)
197
- }
198
- }
199
- }
200
-
201
- private fun updateNotificationActions() {
202
- val context = contextRef.get() ?: return
203
- try {
204
- // Clear existing actions
205
- notificationBuilder.clearActions()
206
- // Add updated actions
207
- addNotificationActions(context)
208
-
209
- // Update the notification
210
- val updatedNotification = notificationBuilder
211
- .setCustomContentView(remoteViews)
212
- .setCustomBigContentView(remoteViews)
213
- .build()
214
-
215
- notificationManager.notify(recordingConfig.notification.notificationId, updatedNotification)
216
- } catch (e: Exception) {
217
- Log.e(Constants.TAG, "Failed to update notification actions", e)
218
- }
219
- }
220
-
221
- fun startUpdates(startTime: Long) {
222
- recordingStartTime = startTime
223
- pausedDuration = 0
224
- isPaused.set(false)
225
- updateNotificationActions() // Update actions when starting
226
- if (!isUpdating.getAndSet(true)) {
227
- scheduleUpdate()
228
- }
229
- }
230
-
231
- private fun scheduleUpdate() {
232
- mainHandler.postDelayed({
233
- if (isUpdating.get() && !isPaused.get()) {
234
- updateNotification()
235
- scheduleUpdate()
236
- }
237
- }, UPDATE_INTERVAL)
238
- }
239
-
240
- fun updateNotification(audioData: FloatArray? = null) {
241
- val context = contextRef.get() ?: return
242
-
243
- try {
244
- val currentTime = SystemClock.elapsedRealtime()
245
-
246
- // Calculate current notification state
247
- val recordingDuration = if (isPaused.get()) {
248
- lastPauseTime - recordingStartTime - pausedDuration
249
- } else {
250
- System.currentTimeMillis() - recordingStartTime - pausedDuration
251
- }
252
-
253
- // Create a hash of the current notification state
254
- val currentHash = Objects.hash(
255
- recordingConfig.notification.title,
256
- recordingConfig.notification.text,
257
- formatDuration(recordingDuration),
258
- isPaused.get()
259
- )
260
-
261
- val needsRemoteViewsRefresh = currentTime - lastRemoteViewsUpdate >= remoteViewsRefreshInterval ||
262
- consecutiveUpdateFailures >= maxUpdateFailures
263
-
264
- // Only update if content changed or refresh needed
265
- if (currentHash == lastNotificationHash && !needsRemoteViewsRefresh) {
266
- // Update waveform only if needed
267
- if (shouldUpdateWaveform(audioData, currentTime)) {
268
- updateWaveformOnly(audioData)
269
- }
270
- return
271
- }
272
-
273
- lastNotificationHash = currentHash
274
-
275
- // Only recreate RemoteViews periodically or after failures
276
- if (needsRemoteViewsRefresh) {
277
- remoteViews = RemoteViews(context.packageName, R.layout.notification_recording)
278
- lastRemoteViewsUpdate = currentTime
279
- consecutiveUpdateFailures = 0
280
- }
281
-
282
- // Update RemoteViews content
283
- remoteViews.apply {
284
- setTextViewText(R.id.notification_title, recordingConfig.notification.title)
285
- setTextViewText(R.id.notification_text, recordingConfig.notification.text)
286
- setTextViewText(R.id.notification_duration, formatDuration(recordingDuration))
287
-
288
- // Update waveform if needed
289
- if (recordingConfig.showWaveformInNotification &&
290
- audioData != null &&
291
- audioData.isNotEmpty() &&
292
- currentTime - lastWaveformUpdate >= WAVEFORM_UPDATE_INTERVAL
293
- ) {
294
- try {
295
- val waveformBitmap = waveformRenderer.generateWaveform(audioData, recordingConfig.notification.waveform)
296
- setImageViewBitmap(R.id.notification_waveform, waveformBitmap)
297
- lastWaveformUpdate = currentTime
298
- } catch (e: Exception) {
299
- Log.e(Constants.TAG, "Error generating waveform", e)
300
- }
301
- }
302
- }
303
-
304
- // Only rebuild notification if RemoteViews was refreshed
305
- if (needsRemoteViewsRefresh) {
306
- notificationBuilder
307
- .setCustomContentView(remoteViews)
308
- .setCustomBigContentView(remoteViews)
309
- .setOnlyAlertOnce(true)
310
- .setOngoing(true)
311
- addNotificationActions(context)
312
- }
313
-
314
- // Update the notification with disabled alerts
315
- notificationManager.notify(
316
- recordingConfig.notification.notificationId,
317
- notificationBuilder
318
- .setOnlyAlertOnce(true)
319
- .setVibrate(null)
320
- .setDefaults(0)
321
- .build()
322
- )
323
-
324
- lastSuccessfulUpdate = currentTime
325
- consecutiveUpdateFailures = 0
326
-
327
- } catch (e: Exception) {
328
- Log.e(Constants.TAG, "Error updating notification", e)
329
- consecutiveUpdateFailures++
330
-
331
- if (consecutiveUpdateFailures >= maxUpdateFailures) {
332
- reinitializeNotification()
333
- }
334
- }
335
- }
336
-
337
- private fun shouldUpdateWaveform(audioData: FloatArray?, currentTime: Long): Boolean {
338
- return recordingConfig.showWaveformInNotification &&
339
- audioData != null &&
340
- audioData.isNotEmpty() &&
341
- currentTime - lastWaveformUpdate >= WAVEFORM_UPDATE_INTERVAL
342
- }
343
-
344
- private fun updateWaveformOnly(audioData: FloatArray?) {
345
- if (audioData == null) return
346
-
347
- try {
348
- val waveformBitmap = waveformRenderer.generateWaveform(audioData, recordingConfig.notification.waveform)
349
- remoteViews.setImageViewBitmap(R.id.notification_waveform, waveformBitmap)
350
- lastWaveformUpdate = SystemClock.elapsedRealtime()
351
-
352
- notificationManager.notify(
353
- recordingConfig.notification.notificationId,
354
- notificationBuilder
355
- .setCustomContentView(remoteViews)
356
- .setCustomBigContentView(remoteViews)
357
- .setOnlyAlertOnce(true)
358
- .setVibrate(null)
359
- .setDefaults(0)
360
- .build()
361
- )
362
- } catch (e: Exception) {
363
- Log.e(Constants.TAG, "Error updating waveform", e)
364
- }
365
- }
366
-
367
- private fun reinitializeNotification() {
368
- try {
369
- val context = contextRef.get() ?: return
370
-
371
- // Force a RemoteViews refresh
372
- remoteViews = RemoteViews(context.packageName, R.layout.notification_recording)
373
- lastRemoteViewsUpdate = SystemClock.elapsedRealtime()
374
-
375
- buildNotification(context)
376
-
377
- notificationManager.notify(
378
- recordingConfig.notification.notificationId,
379
- notificationBuilder.build()
380
- )
381
-
382
- consecutiveUpdateFailures = 0
383
- Log.d(Constants.TAG, "Successfully reinitialized notification")
384
- } catch (e: Exception) {
385
- Log.e(Constants.TAG, "Failed to reinitialize notification", e)
386
- }
387
- }
388
-
389
- fun getNotification(): Notification = notificationBuilder.build()
390
-
391
- fun pauseUpdates() {
392
- isPaused.set(true)
393
- lastPauseTime = System.currentTimeMillis()
394
- updateNotificationActions() // Update actions when pausing
395
- }
396
-
397
- fun resumeUpdates() {
398
- pausedDuration += System.currentTimeMillis() - lastPauseTime
399
- isPaused.set(false)
400
- updateNotificationActions() // Update actions when resuming
401
- scheduleUpdate()
402
- }
403
-
404
- fun stopUpdates() {
405
- isUpdating.set(false)
406
- mainHandler.removeCallbacksAndMessages(null)
407
- notificationManager.cancel(recordingConfig.notification.notificationId)
408
- cleanup()
409
- }
410
-
411
- private fun cleanup() {
412
- recordingStartTime = 0
413
- pausedDuration = 0
414
- lastPauseTime = 0
415
- lastWaveformUpdate = 0
416
- lastSuccessfulUpdate = 0
417
- lastRemoteViewsUpdate = 0
418
- consecutiveUpdateFailures = 0
419
- isPaused.set(false)
420
- isUpdating.set(false)
421
- }
422
-
423
- private fun getResourceIdByName(resourceName: String): Int {
424
- val context = contextRef.get() ?: return R.drawable.ic_default_action_icon
425
- return context.resources.getIdentifier(resourceName, "drawable", context.packageName)
426
- .takeIf { it != 0 } ?: R.drawable.ic_default_action_icon
427
- }
428
-
429
- private fun formatDuration(durationMs: Long): String {
430
- val totalSeconds = durationMs / 1000
431
- val minutes = totalSeconds / 60
432
- val seconds = totalSeconds % 60
433
- return String.format(Locale.US, "%02d:%02d", minutes, seconds)
434
- }
435
- }