@siteed/audio-studio 3.0.5 → 3.1.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/CHANGELOG.md CHANGED
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [3.1.0] - 2026-05-01
12
+ ### Changed
13
+ - fix(audio-studio): preserve pause intent across interruptions (#375) ([2f0f731](https://github.com/deeeed/audiolab/commit/2f0f731e412f45fc81c4fe46bec2abd3f15b4824))
14
+ - fix(android): avoid recorder crashes from stale system callbacks (#374) ([34a9bc0](https://github.com/deeeed/audiolab/commit/34a9bc0c2f7c4e3e569862e8db43710abbde9043))
15
+ - Document when to avoid retaining live analysis history (#373) ([aa617b0](https://github.com/deeeed/audiolab/commit/aa617b048dd790218dda43ec2ed21e0abaf38daf))
16
+ - Let long-running analysis skip full history retention (#372) ([13c230c](https://github.com/deeeed/audiolab/commit/13c230cde655131a3a7c9472d294b2d432f79d50))
17
+ - Keep low-rate iOS AAC recordings from losing compressed output (#371) ([a689eb0](https://github.com/deeeed/audiolab/commit/a689eb03c7436429bd3bf997430f7f7c842b2e57))
18
+ - chore(audio-studio): release @siteed/audio-studio@3.0.5 ([9dff021](https://github.com/deeeed/audiolab/commit/9dff0219993803d29a03946cf81fe2bedb541cab))
11
19
  ## [3.0.5] - 2026-04-25
12
20
  ### Changed
13
21
  - fix(audio-studio): don't start notification on prepareRecording (Android) (#364) ([5d40d7e](https://github.com/deeeed/audiolab/commit/5d40d7e730f2b74459d319979fd9c112891b10f2))
@@ -349,7 +357,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
349
357
  - Audio features extraction during recording
350
358
  - Consistent WAV PCM recording format across all platforms
351
359
 
352
- [unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.5...HEAD
360
+ [unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.1.0...HEAD
361
+ [3.1.0]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.5...@siteed/audio-studio@3.1.0
353
362
  [3.0.5]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.4...@siteed/audio-studio@3.0.5
354
363
  [3.0.4]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.3...@siteed/audio-studio@3.0.4
355
364
  [3.0.3]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.2...@siteed/audio-studio@3.0.3
package/README.md CHANGED
@@ -113,6 +113,26 @@ await startRecording({
113
113
 
114
114
  ## Audio Analysis
115
115
 
116
+ For live analysis during recording, `useAudioRecorder` keeps a recent analysis
117
+ window in `analysisData` for visualization and, by default, also retains the
118
+ full analysis history so `stopRecording().analysisData` can describe the whole
119
+ recording. This option only matters when `enableProcessing: true`. For
120
+ long-running sessions that only need live callbacks, disable the full-history
121
+ retention to avoid unbounded JS memory growth:
122
+
123
+ ```typescript
124
+ await startRecording({
125
+ sampleRate: 16000,
126
+ channels: 1,
127
+ enableProcessing: true,
128
+ keepFullAnalysis: false,
129
+ onAudioAnalysis: async (analysis) => {
130
+ // Consume each analysis chunk without retaining the full recording history.
131
+ updateVoiceActivity(analysis.dataPoints);
132
+ },
133
+ });
134
+ ```
135
+
116
136
  ```typescript
117
137
  import { extractAudioAnalysis, extractPreview, extractMelSpectrogram, trimAudio } from '@siteed/audio-studio';
118
138
 
@@ -118,6 +118,7 @@ class AudioRecorderManager(
118
118
  private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
119
119
  private var phoneStateListener: PhoneStateListener? = null
120
120
  private var telephonyCallback: Any? = null // TelephonyCallback for API 31+, typed as Any to avoid class verification issues on older APIs
121
+ private val pausedBySystemInterruption = AtomicBoolean(false)
121
122
  private var telephonyManager: TelephonyManager? = null
122
123
  get() {
123
124
  if (field == null) {
@@ -408,7 +409,7 @@ class AudioRecorderManager(
408
409
  if (_isRecording.get() && !isPaused.get()) {
409
410
  LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
410
411
  mainHandler.post {
411
- pauseRecording(object : Promise {
412
+ pauseRecordingForSystemInterruption(object : Promise {
412
413
  override fun resolve(value: Any?) {
413
414
  LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
414
415
  eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
@@ -426,8 +427,14 @@ class AudioRecorderManager(
426
427
  TelephonyManager.CALL_STATE_IDLE -> {
427
428
  if (_isRecording.get() && isPaused.get()) {
428
429
  val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
429
- LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume)")
430
- if (autoResume) {
430
+ val shouldAutoResume = InterruptionAutoResumePolicy.shouldAutoResume(
431
+ autoResumeAfterInterruption = autoResume,
432
+ isRecording = _isRecording.get(),
433
+ isPaused = isPaused.get(),
434
+ pausedBySystemInterruption = pausedBySystemInterruption.get()
435
+ )
436
+ LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume, pausedBySystemInterruption: ${pausedBySystemInterruption.get()})")
437
+ if (shouldAutoResume) {
431
438
  mainHandler.post {
432
439
  resumeRecording(object : Promise {
433
440
  override fun resolve(value: Any?) {
@@ -443,7 +450,7 @@ class AudioRecorderManager(
443
450
  })
444
451
  }
445
452
  } else {
446
- LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
453
+ LogUtils.d(CLASS_NAME, "Auto-resume not permitted, staying paused")
447
454
  eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
448
455
  "reason" to "phoneCallEnded",
449
456
  "isPaused" to true
@@ -954,6 +961,7 @@ class AudioRecorderManager(
954
961
 
955
962
  audioRecord?.startRecording()
956
963
  isPaused.set(false)
964
+ pausedBySystemInterruption.set(false)
957
965
  isFirstChunk = true
958
966
  recordingStartTime = System.currentTimeMillis()
959
967
 
@@ -1174,6 +1182,7 @@ class AudioRecorderManager(
1174
1182
  // Reset the timing variables
1175
1183
  _isRecording.set(false)
1176
1184
  isPaused.set(false)
1185
+ pausedBySystemInterruption.set(false)
1177
1186
  totalRecordedTime = 0
1178
1187
  pausedDuration = 0
1179
1188
  } catch (e: Exception) {
@@ -1258,6 +1267,7 @@ class AudioRecorderManager(
1258
1267
  }
1259
1268
 
1260
1269
  LogUtils.d(CLASS_NAME, "⏺️ Recording resumed successfully")
1270
+ pausedBySystemInterruption.set(false)
1261
1271
  promise.resolve("Recording resumed")
1262
1272
  } catch (e: Exception) {
1263
1273
  LogUtils.e(CLASS_NAME, "⏺️ Failed to resume recording: ${e.message}", e)
@@ -1267,12 +1277,21 @@ class AudioRecorderManager(
1267
1277
  }
1268
1278
 
1269
1279
  fun pauseRecording(promise: Promise) {
1280
+ pauseRecording(promise, isSystemInterruption = false)
1281
+ }
1282
+
1283
+ private fun pauseRecordingForSystemInterruption(promise: Promise) {
1284
+ pauseRecording(promise, isSystemInterruption = true)
1285
+ }
1286
+
1287
+ private fun pauseRecording(promise: Promise, isSystemInterruption: Boolean) {
1270
1288
  if (_isRecording.get() && !isPaused.get()) {
1271
1289
  audioRecord?.stop()
1272
1290
  compressedRecorder?.pause()
1273
1291
 
1274
1292
  lastPauseTime = System.currentTimeMillis()
1275
1293
  isPaused.set(true)
1294
+ pausedBySystemInterruption.set(isSystemInterruption)
1276
1295
 
1277
1296
  if (recordingConfig.showNotification) {
1278
1297
  notificationManager.pauseUpdates()
@@ -1417,17 +1436,7 @@ class AudioRecorderManager(
1417
1436
  "audioManager.mode=${audioManager.mode}, " +
1418
1437
  "audioManager.isBluetoothScoOn=${audioManager.isBluetoothScoOn}")
1419
1438
 
1420
- // Trust phone state more than audio manager state
1421
- if (callState == TelephonyManager.CALL_STATE_RINGING ||
1422
- callState == TelephonyManager.CALL_STATE_OFFHOOK) {
1423
- return true
1424
- }
1425
-
1426
- // Only check audio manager mode as secondary indicator
1427
- return audioManager.mode == AudioManager.MODE_IN_CALL ||
1428
- audioManager.mode == AudioManager.MODE_IN_COMMUNICATION
1429
-
1430
- // Remove audioManager.isBluetoothScoOn check as it can be erroneously true after disconnection
1439
+ return AndroidCallState.isOngoingCall(callState, audioManager.mode)
1431
1440
  } catch (e: Exception) {
1432
1441
  LogUtils.e(CLASS_NAME, "Error checking call state: ${e.message}")
1433
1442
  return false
@@ -1757,6 +1766,7 @@ class AudioRecorderManager(
1757
1766
 
1758
1767
  _isRecording.set(false)
1759
1768
  isPaused.set(false)
1769
+ pausedBySystemInterruption.set(false)
1760
1770
  isPrepared = false // Reset prepared state
1761
1771
 
1762
1772
  if (::recordingConfig.isInitialized && recordingConfig.showNotification) {
@@ -1919,9 +1929,8 @@ class AudioRecorderManager(
1919
1929
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
1920
1930
  if (_isRecording.get() && !isPaused.get()) {
1921
1931
  mainHandler.post {
1922
- pauseRecording(object : Promise {
1932
+ pauseRecordingForSystemInterruption(object : Promise {
1923
1933
  override fun resolve(value: Any?) {
1924
- isPaused.set(true)
1925
1934
  eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1926
1935
  "reason" to "audioFocusLoss",
1927
1936
  "isPaused" to true
@@ -1936,7 +1945,12 @@ class AudioRecorderManager(
1936
1945
  }
1937
1946
  AudioManager.AUDIOFOCUS_GAIN -> {
1938
1947
  val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
1939
- if (_isRecording.get() && isPaused.get() && autoResume) {
1948
+ if (InterruptionAutoResumePolicy.shouldAutoResume(
1949
+ autoResumeAfterInterruption = autoResume,
1950
+ isRecording = _isRecording.get(),
1951
+ isPaused = isPaused.get(),
1952
+ pausedBySystemInterruption = pausedBySystemInterruption.get()
1953
+ )) {
1940
1954
  mainHandler.post {
1941
1955
  resumeRecording(object : Promise {
1942
1956
  override fun resolve(value: Any?) {
@@ -1985,9 +1999,8 @@ class AudioRecorderManager(
1985
1999
  // Only pause for permanent focus loss (like phone calls)
1986
2000
  if (_isRecording.get() && !isPaused.get()) {
1987
2001
  mainHandler.post {
1988
- pauseRecording(object : Promise {
2002
+ pauseRecordingForSystemInterruption(object : Promise {
1989
2003
  override fun resolve(value: Any?) {
1990
- isPaused.set(true)
1991
2004
  eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1992
2005
  "reason" to "audioFocusLoss",
1993
2006
  "isPaused" to true
@@ -2006,7 +2019,12 @@ class AudioRecorderManager(
2006
2019
  }
2007
2020
  AudioManager.AUDIOFOCUS_GAIN -> {
2008
2021
  val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
2009
- if (_isRecording.get() && isPaused.get() && autoResume) {
2022
+ if (InterruptionAutoResumePolicy.shouldAutoResume(
2023
+ autoResumeAfterInterruption = autoResume,
2024
+ isRecording = _isRecording.get(),
2025
+ isPaused = isPaused.get(),
2026
+ pausedBySystemInterruption = pausedBySystemInterruption.get()
2027
+ )) {
2010
2028
  mainHandler.post {
2011
2029
  resumeRecording(object : Promise {
2012
2030
  override fun resolve(value: Any?) {
@@ -2168,4 +2186,38 @@ class AudioRecorderManager(
2168
2186
  return false
2169
2187
  }
2170
2188
  }
2171
- }
2189
+ }
2190
+
2191
+ internal object AndroidCallState {
2192
+ /**
2193
+ * Telephony call state wins when known. AudioManager mode is only a fallback
2194
+ * for unknown state because some Android devices leave it stale after calls.
2195
+ */
2196
+ fun isOngoingCall(callState: Int?, audioMode: Int): Boolean {
2197
+ return when (callState) {
2198
+ TelephonyManager.CALL_STATE_RINGING,
2199
+ TelephonyManager.CALL_STATE_OFFHOOK -> true
2200
+ TelephonyManager.CALL_STATE_IDLE -> false
2201
+ else -> audioMode == AudioManager.MODE_IN_CALL ||
2202
+ audioMode == AudioManager.MODE_IN_COMMUNICATION
2203
+ }
2204
+ }
2205
+ }
2206
+
2207
+ internal object InterruptionAutoResumePolicy {
2208
+ /**
2209
+ * Auto-resume is only allowed when the pause was caused by a system interruption.
2210
+ * User-initiated pauses must stay paused even after phone/audio focus interruptions end.
2211
+ */
2212
+ fun shouldAutoResume(
2213
+ autoResumeAfterInterruption: Boolean,
2214
+ isRecording: Boolean,
2215
+ isPaused: Boolean,
2216
+ pausedBySystemInterruption: Boolean
2217
+ ): Boolean {
2218
+ return autoResumeAfterInterruption &&
2219
+ isRecording &&
2220
+ isPaused &&
2221
+ pausedBySystemInterruption
2222
+ }
2223
+ }
@@ -413,7 +413,7 @@ class AudioStudioModule : Module(), EventSender {
413
413
 
414
414
  val progressListener = object : AudioTrimmer.ProgressListener {
415
415
  override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
416
- sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
416
+ safeSendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
417
417
  "progress" to progress,
418
418
  "bytesProcessed" to bytesProcessed,
419
419
  "totalBytes" to totalBytes
@@ -989,7 +989,7 @@ class AudioStudioModule : Module(), EventSender {
989
989
  }
990
990
 
991
991
  // Notify JS about the disconnection
992
- sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
992
+ safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
993
993
  "type" to "deviceDisconnected",
994
994
  "deviceId" to deviceId
995
995
  ))
@@ -1004,7 +1004,7 @@ class AudioStudioModule : Module(), EventSender {
1004
1004
  audioDeviceManager.onDeviceConnected = { deviceId ->
1005
1005
  LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
1006
1006
  // Notify JS about the connection
1007
- sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
1007
+ safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
1008
1008
  "type" to "deviceConnected",
1009
1009
  "deviceId" to deviceId
1010
1010
  ))
@@ -1014,7 +1014,7 @@ class AudioStudioModule : Module(), EventSender {
1014
1014
  audioDeviceManager.onDeviceDisconnected = { deviceId ->
1015
1015
  LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
1016
1016
  // Notify JS about the disconnection
1017
- sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
1017
+ safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
1018
1018
  "type" to "deviceDisconnected",
1019
1019
  "deviceId" to deviceId
1020
1020
  ))
@@ -1023,6 +1023,18 @@ class AudioStudioModule : Module(), EventSender {
1023
1023
  audioProcessor = AudioProcessor(filesDir)
1024
1024
  }
1025
1025
 
1026
+ private fun safeSendEvent(eventName: String, params: Bundle) {
1027
+ AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
1028
+ sendEvent(eventName, params)
1029
+ }
1030
+ }
1031
+
1032
+ private fun safeSendEvent(eventName: String, params: Map<String, Any?>) {
1033
+ AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
1034
+ sendEvent(eventName, params)
1035
+ }
1036
+ }
1037
+
1026
1038
  /**
1027
1039
  * Handles audio device disconnection based on the recording configuration
1028
1040
  */
@@ -1055,7 +1067,7 @@ class AudioStudioModule : Module(), EventSender {
1055
1067
 
1056
1068
  // Notify JS about fallback
1057
1069
  LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
1058
- sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1070
+ safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1059
1071
  "reason" to "deviceFallback",
1060
1072
  "isPaused" to false,
1061
1073
  "deviceId" to deviceId
@@ -1070,7 +1082,7 @@ class AudioStudioModule : Module(), EventSender {
1070
1082
  // Notify AudioRecorderManager to handle device change while paused
1071
1083
  audioRecorderManager.handleDeviceChange()
1072
1084
 
1073
- sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1085
+ safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1074
1086
  "reason" to "deviceSwitchFailed",
1075
1087
  "isPaused" to true
1076
1088
  ))
@@ -1090,7 +1102,7 @@ class AudioStudioModule : Module(), EventSender {
1090
1102
  // Notify AudioRecorderManager to handle device change while paused
1091
1103
  audioRecorderManager.handleDeviceChange()
1092
1104
 
1093
- sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1105
+ safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1094
1106
  "reason" to "deviceDisconnected",
1095
1107
  "isPaused" to true
1096
1108
  ))
@@ -1112,7 +1124,7 @@ class AudioStudioModule : Module(), EventSender {
1112
1124
  // Notify AudioRecorderManager to handle device change while paused
1113
1125
  audioRecorderManager.handleDeviceChange()
1114
1126
 
1115
- sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1127
+ safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
1116
1128
  "reason" to "deviceDisconnected",
1117
1129
  "isPaused" to true
1118
1130
  ))
@@ -1128,6 +1140,18 @@ class AudioStudioModule : Module(), EventSender {
1128
1140
 
1129
1141
  override fun sendExpoEvent(eventName: String, params: Bundle) {
1130
1142
  LogUtils.d(CLASS_NAME, "Sending event: $eventName")
1131
- this@AudioStudioModule.sendEvent(eventName, params)
1143
+ safeSendEvent(eventName, params)
1144
+ }
1145
+ }
1146
+
1147
+ internal object AndroidEventEmitter {
1148
+ fun safeSend(className: String, eventName: String, send: () -> Unit): Boolean {
1149
+ return try {
1150
+ send()
1151
+ true
1152
+ } catch (e: Exception) {
1153
+ LogUtils.e(className, "Failed to send event $eventName: ${e.message}", e)
1154
+ false
1155
+ }
1132
1156
  }
1133
1157
  }
@@ -3,5 +3,11 @@ package net.siteed.audiostudio
3
3
  import android.os.Bundle
4
4
 
5
5
  interface EventSender {
6
+ /**
7
+ * Best-effort event delivery to JavaScript.
8
+ *
9
+ * Native recording/device callbacks must not crash if Expo rejects an event
10
+ * while the module is not ready to emit.
11
+ */
6
12
  fun sendExpoEvent(eventName: String, params: Bundle)
7
13
  }
@@ -0,0 +1,37 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import android.media.AudioManager
4
+ import android.telephony.TelephonyManager
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ class AndroidCallStateTest {
10
+ @Test
11
+ fun idleCallStateWinsOverStaleInCallAudioMode() {
12
+ val result = AndroidCallState.isOngoingCall(
13
+ TelephonyManager.CALL_STATE_IDLE,
14
+ AudioManager.MODE_IN_CALL
15
+ )
16
+
17
+ assertFalse(result)
18
+ }
19
+
20
+ @Test
21
+ fun ringingAndOffhookAreOngoingCalls() {
22
+ assertTrue(AndroidCallState.isOngoingCall(
23
+ TelephonyManager.CALL_STATE_RINGING,
24
+ AudioManager.MODE_NORMAL
25
+ ))
26
+ assertTrue(AndroidCallState.isOngoingCall(
27
+ TelephonyManager.CALL_STATE_OFFHOOK,
28
+ AudioManager.MODE_NORMAL
29
+ ))
30
+ }
31
+
32
+ @Test
33
+ fun unknownCallStateFallsBackToAudioMode() {
34
+ assertTrue(AndroidCallState.isOngoingCall(null, AudioManager.MODE_IN_COMMUNICATION))
35
+ assertFalse(AndroidCallState.isOngoingCall(null, AudioManager.MODE_NORMAL))
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class AndroidEventEmitterTest {
8
+ @Test
9
+ fun safeSendReturnsTrueWhenEventIsSent() {
10
+ var sent = false
11
+
12
+ val result = AndroidEventEmitter.safeSend("Test", "event") {
13
+ sent = true
14
+ }
15
+
16
+ assertTrue(result)
17
+ assertTrue(sent)
18
+ }
19
+
20
+ @Test
21
+ fun safeSendCatchesEmitterExceptions() {
22
+ val result = AndroidEventEmitter.safeSend("Test", "event") {
23
+ throw IllegalArgumentException("module not ready")
24
+ }
25
+
26
+ assertFalse(result)
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import org.junit.Assert.assertFalse
4
+ import org.junit.Assert.assertTrue
5
+ import org.junit.Test
6
+
7
+ class InterruptionAutoResumePolicyTest {
8
+ @Test
9
+ fun autoResumesOnlyWhenSystemInterruptionPausedRecording() {
10
+ assertTrue(InterruptionAutoResumePolicy.shouldAutoResume(
11
+ autoResumeAfterInterruption = true,
12
+ isRecording = true,
13
+ isPaused = true,
14
+ pausedBySystemInterruption = true
15
+ ))
16
+ }
17
+
18
+ @Test
19
+ fun doesNotAutoResumeUserPausedRecording() {
20
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
21
+ autoResumeAfterInterruption = true,
22
+ isRecording = true,
23
+ isPaused = true,
24
+ pausedBySystemInterruption = false
25
+ ))
26
+ }
27
+
28
+ @Test
29
+ fun requiresAutoResumeAndActivePausedRecording() {
30
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
31
+ autoResumeAfterInterruption = false,
32
+ isRecording = true,
33
+ isPaused = true,
34
+ pausedBySystemInterruption = true
35
+ ))
36
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
37
+ autoResumeAfterInterruption = true,
38
+ isRecording = false,
39
+ isPaused = true,
40
+ pausedBySystemInterruption = true
41
+ ))
42
+ assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
43
+ autoResumeAfterInterruption = true,
44
+ isRecording = true,
45
+ isPaused = false,
46
+ pausedBySystemInterruption = true
47
+ ))
48
+ }
49
+ }