@siteed/expo-audio-stream 2.1.0 → 2.2.1-beta.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 (189) hide show
  1. package/README.md +23 -260
  2. package/build/index.d.ts +11 -15
  3. package/build/index.js +54 -14
  4. package/build/src/index.d.ts +11 -0
  5. package/build/src/index.js +54 -0
  6. package/package.json +49 -110
  7. package/src/index.ts +18 -32
  8. package/CHANGELOG.md +0 -206
  9. package/android/build.gradle +0 -105
  10. package/android/src/main/AndroidManifest.xml +0 -27
  11. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  12. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  13. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  14. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  15. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  16. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
  17. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  18. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
  19. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  20. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
  21. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  22. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
  23. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  24. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  25. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  26. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  27. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  28. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  29. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  30. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  31. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  32. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  33. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  34. package/android/src/main/res/drawable/ic_play.xml +0 -10
  35. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  36. package/android/src/main/res/layout/notification_recording.xml +0 -37
  37. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  38. package/app.plugin.js +0 -1
  39. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
  40. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  41. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  42. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  43. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
  44. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  45. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
  46. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  47. package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
  48. package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  49. package/build/AudioAnalysis/extractAudioData.js +0 -5
  50. package/build/AudioAnalysis/extractAudioData.js.map +0 -1
  51. package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  52. package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  53. package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
  54. package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  55. package/build/AudioAnalysis/extractPreview.d.ts +0 -11
  56. package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
  57. package/build/AudioAnalysis/extractPreview.js +0 -25
  58. package/build/AudioAnalysis/extractPreview.js.map +0 -1
  59. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  60. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  61. package/build/AudioAnalysis/extractWaveform.js +0 -11
  62. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  63. package/build/AudioRecorder.provider.d.ts +0 -11
  64. package/build/AudioRecorder.provider.d.ts.map +0 -1
  65. package/build/AudioRecorder.provider.js +0 -37
  66. package/build/AudioRecorder.provider.js.map +0 -1
  67. package/build/ExpoAudioStream.native.d.ts +0 -3
  68. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  69. package/build/ExpoAudioStream.native.js +0 -6
  70. package/build/ExpoAudioStream.native.js.map +0 -1
  71. package/build/ExpoAudioStream.types.d.ts +0 -532
  72. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  73. package/build/ExpoAudioStream.types.js +0 -2
  74. package/build/ExpoAudioStream.types.js.map +0 -1
  75. package/build/ExpoAudioStream.web.d.ts +0 -59
  76. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  77. package/build/ExpoAudioStream.web.js +0 -285
  78. package/build/ExpoAudioStream.web.js.map +0 -1
  79. package/build/ExpoAudioStreamModule.d.ts +0 -3
  80. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  81. package/build/ExpoAudioStreamModule.js +0 -693
  82. package/build/ExpoAudioStreamModule.js.map +0 -1
  83. package/build/WebRecorder.web.d.ts +0 -119
  84. package/build/WebRecorder.web.d.ts.map +0 -1
  85. package/build/WebRecorder.web.js +0 -436
  86. package/build/WebRecorder.web.js.map +0 -1
  87. package/build/constants.d.ts +0 -11
  88. package/build/constants.d.ts.map +0 -1
  89. package/build/constants.js +0 -14
  90. package/build/constants.js.map +0 -1
  91. package/build/events.d.ts +0 -26
  92. package/build/events.d.ts.map +0 -1
  93. package/build/events.js +0 -21
  94. package/build/events.js.map +0 -1
  95. package/build/index.d.ts.map +0 -1
  96. package/build/index.js.map +0 -1
  97. package/build/trimAudio.d.ts +0 -25
  98. package/build/trimAudio.d.ts.map +0 -1
  99. package/build/trimAudio.js +0 -67
  100. package/build/trimAudio.js.map +0 -1
  101. package/build/useAudioRecorder.d.ts +0 -21
  102. package/build/useAudioRecorder.d.ts.map +0 -1
  103. package/build/useAudioRecorder.js +0 -427
  104. package/build/useAudioRecorder.js.map +0 -1
  105. package/build/utils/BlobFix.d.ts +0 -9
  106. package/build/utils/BlobFix.d.ts.map +0 -1
  107. package/build/utils/BlobFix.js +0 -498
  108. package/build/utils/BlobFix.js.map +0 -1
  109. package/build/utils/audioProcessing.d.ts +0 -24
  110. package/build/utils/audioProcessing.d.ts.map +0 -1
  111. package/build/utils/audioProcessing.js +0 -133
  112. package/build/utils/audioProcessing.js.map +0 -1
  113. package/build/utils/concatenateBuffers.d.ts +0 -8
  114. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  115. package/build/utils/concatenateBuffers.js +0 -21
  116. package/build/utils/concatenateBuffers.js.map +0 -1
  117. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  118. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  119. package/build/utils/convertPCMToFloat32.js +0 -120
  120. package/build/utils/convertPCMToFloat32.js.map +0 -1
  121. package/build/utils/encodingToBitDepth.d.ts +0 -5
  122. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  123. package/build/utils/encodingToBitDepth.js +0 -13
  124. package/build/utils/encodingToBitDepth.js.map +0 -1
  125. package/build/utils/getWavFileInfo.d.ts +0 -26
  126. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  127. package/build/utils/getWavFileInfo.js +0 -92
  128. package/build/utils/getWavFileInfo.js.map +0 -1
  129. package/build/utils/writeWavHeader.d.ts +0 -49
  130. package/build/utils/writeWavHeader.d.ts.map +0 -1
  131. package/build/utils/writeWavHeader.js +0 -91
  132. package/build/utils/writeWavHeader.js.map +0 -1
  133. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  134. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  135. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  136. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  137. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  138. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  139. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  140. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  141. package/expo-module.config.json +0 -9
  142. package/ios/AudioAnalysisData.swift +0 -74
  143. package/ios/AudioNotificationManager.swift +0 -135
  144. package/ios/AudioProcessingHelpers.swift +0 -743
  145. package/ios/AudioProcessor.swift +0 -1313
  146. package/ios/AudioStreamError.swift +0 -7
  147. package/ios/AudioStreamManager.swift +0 -1708
  148. package/ios/AudioStreamManagerDelegate.swift +0 -16
  149. package/ios/DataPoint.swift +0 -54
  150. package/ios/DecodingConfig.swift +0 -47
  151. package/ios/ExpoAudioStream.podspec +0 -27
  152. package/ios/ExpoAudioStreamModule.swift +0 -805
  153. package/ios/FFT.swift +0 -62
  154. package/ios/Features.swift +0 -95
  155. package/ios/Logger.swift +0 -7
  156. package/ios/NotificationExtension.swift +0 -15
  157. package/ios/RecordingResult.swift +0 -22
  158. package/ios/RecordingSettings.swift +0 -265
  159. package/ios/WaveformExtractor.swift +0 -105
  160. package/plugin/build/index.d.ts +0 -21
  161. package/plugin/build/index.js +0 -191
  162. package/plugin/src/index.ts +0 -278
  163. package/plugin/tsconfig.json +0 -10
  164. package/plugin/tsconfig.tsbuildinfo +0 -1
  165. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
  166. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
  167. package/src/AudioAnalysis/extractAudioData.ts +0 -6
  168. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
  169. package/src/AudioAnalysis/extractPreview.ts +0 -34
  170. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  171. package/src/AudioRecorder.provider.tsx +0 -54
  172. package/src/ExpoAudioStream.native.ts +0 -6
  173. package/src/ExpoAudioStream.types.ts +0 -641
  174. package/src/ExpoAudioStream.web.ts +0 -359
  175. package/src/ExpoAudioStreamModule.ts +0 -967
  176. package/src/WebRecorder.web.ts +0 -580
  177. package/src/constants.ts +0 -18
  178. package/src/events.ts +0 -60
  179. package/src/trimAudio.ts +0 -90
  180. package/src/useAudioRecorder.tsx +0 -620
  181. package/src/utils/BlobFix.ts +0 -559
  182. package/src/utils/audioProcessing.ts +0 -205
  183. package/src/utils/concatenateBuffers.ts +0 -24
  184. package/src/utils/convertPCMToFloat32.ts +0 -170
  185. package/src/utils/encodingToBitDepth.ts +0 -18
  186. package/src/utils/getWavFileInfo.ts +0 -132
  187. package/src/utils/writeWavHeader.ts +0 -114
  188. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  189. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,131 +0,0 @@
1
- package net.siteed.audiostream
2
-
3
- import android.util.Log
4
- import java.io.File
5
- import java.io.IOException
6
- import java.io.OutputStream
7
- import java.io.RandomAccessFile
8
- import java.util.UUID
9
-
10
- class AudioFileHandler(private val filesDir: File) {
11
- // Method to write WAV file header
12
- fun writeWavHeader(out: OutputStream, sampleRateInHz: Int, channels: Int, bitDepth: Int) {
13
- val header = ByteArray(44)
14
- val byteRate = sampleRateInHz * channels * bitDepth / 8
15
- val blockAlign = channels * bitDepth / 8
16
-
17
- // RIFF/WAVE header
18
- "RIFF".toByteArray().copyInto(header, 0)
19
- // (file size - 8) to be updated later
20
- header[4] = 0 // Placeholder
21
- header[5] = 0 // Placeholder
22
- header[6] = 0 // Placeholder
23
- header[7] = 0 // Placeholder
24
- "WAVE".toByteArray().copyInto(header, 8)
25
- "fmt ".toByteArray().copyInto(header, 12)
26
-
27
- // 16 for PCM
28
- header[16] = 16
29
- header[17] = 0
30
- header[18] = 0
31
- header[19] = 0
32
-
33
- // PCM format ID
34
- header[20] = 1 // Audio format 1 for PCM (not compressed)
35
- header[21] = 0
36
-
37
- // Number of channels
38
- header[22] = (channels and 0xff).toByte()
39
- header[23] = (channels shr 8 and 0xff).toByte()
40
-
41
- // Sample rate
42
- header[24] = (sampleRateInHz and 0xff).toByte()
43
- header[25] = (sampleRateInHz shr 8 and 0xff).toByte()
44
- header[26] = (sampleRateInHz shr 16 and 0xff).toByte()
45
- header[27] = (sampleRateInHz shr 24 and 0xff).toByte()
46
-
47
- // Byte rate
48
- header[28] = (byteRate and 0xff).toByte()
49
- header[29] = (byteRate shr 8 and 0xff).toByte()
50
- header[30] = (byteRate shr 16 and 0xff).toByte()
51
- header[31] = (byteRate shr 24 and 0xff).toByte()
52
-
53
- // Block align
54
- header[32] = (blockAlign and 0xff).toByte()
55
- header[33] = (blockAlign shr 8 and 0xff).toByte()
56
-
57
- // Bits per sample
58
- header[34] = (bitDepth and 0xff).toByte()
59
- header[35] = (bitDepth shr 8 and 0xff).toByte()
60
-
61
- // Data chunk
62
- "data".toByteArray().copyInto(header, 36)
63
- // Data size to be updated later
64
- header[40] = 0 // Placeholder
65
- header[41] = 0 // Placeholder
66
- header[42] = 0 // Placeholder
67
- header[43] = 0 // Placeholder
68
-
69
- out.write(header, 0, 44)
70
- }
71
-
72
- fun updateWavHeader(file: File) {
73
- try {
74
- RandomAccessFile(file, "rw").use { raf ->
75
- val fileSize = raf.length()
76
- val dataSize = fileSize - 44 // Subtract the header size
77
-
78
- raf.seek(4) // Write correct file size, excluding the first 8 bytes of the RIFF header
79
- raf.writeInt(Integer.reverseBytes((dataSize + 36).toInt()))
80
-
81
- raf.seek(40) // Go to the data size position
82
- raf.writeInt(Integer.reverseBytes(dataSize.toInt())) // Write the size of the data segment
83
- }
84
- } catch (e: IOException) {
85
- println("Could not update WAV header: ${e.message}")
86
- }
87
- }
88
-
89
- fun clearAudioStorage() {
90
- filesDir.listFiles()?.forEach {
91
- it.delete()
92
- }
93
- }
94
-
95
- fun createAudioFile(extension: String): File {
96
- val timestamp = System.currentTimeMillis()
97
- val uuid = UUID.randomUUID().toString()
98
- val filename = "recording_${timestamp}_${uuid}.${extension}"
99
-
100
- return try {
101
- File(filesDir, filename).apply {
102
- parentFile?.mkdirs() // Create directories if they don't exist
103
- createNewFile() // Create the file
104
- }
105
- } catch (e: Exception) {
106
- Log.e(Constants.TAG, "Failed to create audio file", e)
107
- throw e
108
- }
109
- }
110
-
111
- fun deleteFile(file: File?): Boolean {
112
- return try {
113
- if (file == null) {
114
- Log.w(Constants.TAG, "Attempted to delete null file")
115
- false
116
- } else if (!file.exists()) {
117
- Log.w(Constants.TAG, "File does not exist: ${file.absolutePath}")
118
- false
119
- } else {
120
- val wasDeleted = file.delete()
121
- if (!wasDeleted) {
122
- Log.w(Constants.TAG, "Failed to delete file: ${file.absolutePath}")
123
- }
124
- wasDeleted
125
- }
126
- } catch (e: Exception) {
127
- Log.e(Constants.TAG, "Error deleting file: ${file?.absolutePath}", e)
128
- false
129
- }
130
- }
131
- }
@@ -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
- }