@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.
- package/README.md +40 -222
- package/build/index.d.ts +11 -15
- package/build/index.js +44 -14
- package/package.json +49 -110
- package/src/index.ts +18 -32
- package/CHANGELOG.md +0 -206
- package/android/build.gradle +0 -105
- package/android/src/main/AndroidManifest.xml +0 -27
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
- package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
- package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
- package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
- package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
- package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
- package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
- package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
- package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
- package/android/src/main/res/drawable/ic_microphone.xml +0 -13
- package/android/src/main/res/drawable/ic_pause.xml +0 -10
- package/android/src/main/res/drawable/ic_play.xml +0 -10
- package/android/src/main/res/drawable/ic_stop.xml +0 -10
- package/android/src/main/res/layout/notification_recording.xml +0 -37
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/app.plugin.js +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
- package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
- package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioData.js +0 -5
- package/build/AudioAnalysis/extractAudioData.js.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
- package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
- package/build/AudioAnalysis/extractPreview.d.ts +0 -11
- package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
- package/build/AudioAnalysis/extractPreview.js +0 -25
- package/build/AudioAnalysis/extractPreview.js.map +0 -1
- package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
- package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
- package/build/AudioAnalysis/extractWaveform.js +0 -11
- package/build/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/AudioRecorder.provider.d.ts +0 -11
- package/build/AudioRecorder.provider.d.ts.map +0 -1
- package/build/AudioRecorder.provider.js +0 -37
- package/build/AudioRecorder.provider.js.map +0 -1
- package/build/ExpoAudioStream.native.d.ts +0 -3
- package/build/ExpoAudioStream.native.d.ts.map +0 -1
- package/build/ExpoAudioStream.native.js +0 -6
- package/build/ExpoAudioStream.native.js.map +0 -1
- package/build/ExpoAudioStream.types.d.ts +0 -532
- package/build/ExpoAudioStream.types.d.ts.map +0 -1
- package/build/ExpoAudioStream.types.js +0 -2
- package/build/ExpoAudioStream.types.js.map +0 -1
- package/build/ExpoAudioStream.web.d.ts +0 -59
- package/build/ExpoAudioStream.web.d.ts.map +0 -1
- package/build/ExpoAudioStream.web.js +0 -285
- package/build/ExpoAudioStream.web.js.map +0 -1
- package/build/ExpoAudioStreamModule.d.ts +0 -3
- package/build/ExpoAudioStreamModule.d.ts.map +0 -1
- package/build/ExpoAudioStreamModule.js +0 -693
- package/build/ExpoAudioStreamModule.js.map +0 -1
- package/build/WebRecorder.web.d.ts +0 -119
- package/build/WebRecorder.web.d.ts.map +0 -1
- package/build/WebRecorder.web.js +0 -436
- package/build/WebRecorder.web.js.map +0 -1
- package/build/constants.d.ts +0 -11
- package/build/constants.d.ts.map +0 -1
- package/build/constants.js +0 -14
- package/build/constants.js.map +0 -1
- package/build/events.d.ts +0 -26
- package/build/events.d.ts.map +0 -1
- package/build/events.js +0 -21
- package/build/events.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/trimAudio.d.ts +0 -25
- package/build/trimAudio.d.ts.map +0 -1
- package/build/trimAudio.js +0 -67
- package/build/trimAudio.js.map +0 -1
- package/build/useAudioRecorder.d.ts +0 -21
- package/build/useAudioRecorder.d.ts.map +0 -1
- package/build/useAudioRecorder.js +0 -427
- package/build/useAudioRecorder.js.map +0 -1
- package/build/utils/BlobFix.d.ts +0 -9
- package/build/utils/BlobFix.d.ts.map +0 -1
- package/build/utils/BlobFix.js +0 -498
- package/build/utils/BlobFix.js.map +0 -1
- package/build/utils/audioProcessing.d.ts +0 -24
- package/build/utils/audioProcessing.d.ts.map +0 -1
- package/build/utils/audioProcessing.js +0 -133
- package/build/utils/audioProcessing.js.map +0 -1
- package/build/utils/concatenateBuffers.d.ts +0 -8
- package/build/utils/concatenateBuffers.d.ts.map +0 -1
- package/build/utils/concatenateBuffers.js +0 -21
- package/build/utils/concatenateBuffers.js.map +0 -1
- package/build/utils/convertPCMToFloat32.d.ts +0 -13
- package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
- package/build/utils/convertPCMToFloat32.js +0 -120
- package/build/utils/convertPCMToFloat32.js.map +0 -1
- package/build/utils/encodingToBitDepth.d.ts +0 -5
- package/build/utils/encodingToBitDepth.d.ts.map +0 -1
- package/build/utils/encodingToBitDepth.js +0 -13
- package/build/utils/encodingToBitDepth.js.map +0 -1
- package/build/utils/getWavFileInfo.d.ts +0 -26
- package/build/utils/getWavFileInfo.d.ts.map +0 -1
- package/build/utils/getWavFileInfo.js +0 -92
- package/build/utils/getWavFileInfo.js.map +0 -1
- package/build/utils/writeWavHeader.d.ts +0 -49
- package/build/utils/writeWavHeader.d.ts.map +0 -1
- package/build/utils/writeWavHeader.js +0 -91
- package/build/utils/writeWavHeader.js.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.js +0 -828
- package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.js +0 -157
- package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/AudioAnalysisData.swift +0 -74
- package/ios/AudioNotificationManager.swift +0 -135
- package/ios/AudioProcessingHelpers.swift +0 -743
- package/ios/AudioProcessor.swift +0 -1313
- package/ios/AudioStreamError.swift +0 -7
- package/ios/AudioStreamManager.swift +0 -1708
- package/ios/AudioStreamManagerDelegate.swift +0 -16
- package/ios/DataPoint.swift +0 -54
- package/ios/DecodingConfig.swift +0 -47
- package/ios/ExpoAudioStream.podspec +0 -27
- package/ios/ExpoAudioStreamModule.swift +0 -805
- package/ios/FFT.swift +0 -62
- package/ios/Features.swift +0 -95
- package/ios/Logger.swift +0 -7
- package/ios/NotificationExtension.swift +0 -15
- package/ios/RecordingResult.swift +0 -22
- package/ios/RecordingSettings.swift +0 -265
- package/ios/WaveformExtractor.swift +0 -105
- package/plugin/build/index.d.ts +0 -21
- package/plugin/build/index.js +0 -191
- package/plugin/src/index.ts +0 -278
- package/plugin/tsconfig.json +0 -10
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
- package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
- package/src/AudioAnalysis/extractAudioData.ts +0 -6
- package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
- package/src/AudioAnalysis/extractPreview.ts +0 -34
- package/src/AudioAnalysis/extractWaveform.ts +0 -22
- package/src/AudioRecorder.provider.tsx +0 -54
- package/src/ExpoAudioStream.native.ts +0 -6
- package/src/ExpoAudioStream.types.ts +0 -641
- package/src/ExpoAudioStream.web.ts +0 -359
- package/src/ExpoAudioStreamModule.ts +0 -967
- package/src/WebRecorder.web.ts +0 -580
- package/src/constants.ts +0 -18
- package/src/events.ts +0 -60
- package/src/trimAudio.ts +0 -90
- package/src/useAudioRecorder.tsx +0 -620
- package/src/utils/BlobFix.ts +0 -559
- package/src/utils/audioProcessing.ts +0 -205
- package/src/utils/concatenateBuffers.ts +0 -24
- package/src/utils/convertPCMToFloat32.ts +0 -170
- package/src/utils/encodingToBitDepth.ts +0 -18
- package/src/utils/getWavFileInfo.ts +0 -132
- package/src/utils/writeWavHeader.ts +0 -114
- package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
- 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
|
-
}
|