@independo/capacitor-voice-recorder 8.0.2-dev.1 → 8.1.0-dev.2

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 (81) hide show
  1. package/README.md +130 -32
  2. package/android/build.gradle +44 -1
  3. package/android/src/main/java/app/independo/capacitorvoicerecorder/VoiceRecorder.java +146 -0
  4. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/PermissionChecker.java +8 -0
  5. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecordDataMapper.java +32 -0
  6. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderAdapter.java +39 -0
  7. package/android/src/main/java/app/independo/capacitorvoicerecorder/adapters/RecorderPlatform.java +25 -0
  8. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/CurrentRecordingStatus.java +9 -0
  9. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ErrorCodes.java +19 -0
  10. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/Messages.java +2 -1
  11. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/RecordData.java +15 -1
  12. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/RecordOptions.java +4 -0
  13. package/android/src/main/java/app/independo/capacitorvoicerecorder/core/ResponseFormat.java +18 -0
  14. package/android/src/main/java/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/core}/ResponseGenerator.java +7 -1
  15. package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/CustomMediaRecorder.java +281 -0
  16. package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/DefaultRecorderPlatform.java +86 -0
  17. package/android/src/main/java/app/independo/capacitorvoicerecorder/platform/NotSupportedOsVersion.java +4 -0
  18. package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderService.java +144 -0
  19. package/android/src/main/java/app/independo/capacitorvoicerecorder/service/VoiceRecorderServiceException.java +23 -0
  20. package/dist/docs.json +145 -5
  21. package/dist/esm/adapters/VoiceRecorderWebAdapter.d.ts +23 -0
  22. package/dist/esm/adapters/VoiceRecorderWebAdapter.js +41 -0
  23. package/dist/esm/adapters/VoiceRecorderWebAdapter.js.map +1 -0
  24. package/dist/esm/core/error-codes.d.ts +4 -0
  25. package/dist/esm/core/error-codes.js +21 -0
  26. package/dist/esm/core/error-codes.js.map +1 -0
  27. package/dist/esm/core/recording-contract.d.ts +3 -0
  28. package/dist/esm/core/recording-contract.js +15 -0
  29. package/dist/esm/core/recording-contract.js.map +1 -0
  30. package/dist/esm/core/response-format.d.ts +8 -0
  31. package/dist/esm/core/response-format.js +17 -0
  32. package/dist/esm/core/response-format.js.map +1 -0
  33. package/dist/esm/definitions.d.ts +36 -3
  34. package/dist/esm/definitions.js.map +1 -1
  35. package/dist/esm/platform/web/VoiceRecorderImpl.d.ts +45 -0
  36. package/dist/esm/{VoiceRecorderImpl.js → platform/web/VoiceRecorderImpl.js} +20 -2
  37. package/dist/esm/platform/web/VoiceRecorderImpl.js.map +1 -0
  38. package/dist/esm/platform/web/get-blob-duration.js.map +1 -0
  39. package/dist/esm/{predefined-web-responses.d.ts → platform/web/predefined-web-responses.d.ts} +12 -1
  40. package/dist/esm/{predefined-web-responses.js → platform/web/predefined-web-responses.js} +11 -0
  41. package/dist/esm/platform/web/predefined-web-responses.js.map +1 -0
  42. package/dist/esm/service/VoiceRecorderService.d.ts +47 -0
  43. package/dist/esm/service/VoiceRecorderService.js +60 -0
  44. package/dist/esm/service/VoiceRecorderService.js.map +1 -0
  45. package/dist/esm/web.d.ts +12 -1
  46. package/dist/esm/web.js +26 -12
  47. package/dist/esm/web.js.map +1 -1
  48. package/dist/plugin.cjs.js +200 -9
  49. package/dist/plugin.cjs.js.map +1 -1
  50. package/dist/plugin.js +200 -9
  51. package/dist/plugin.js.map +1 -1
  52. package/ios/Sources/VoiceRecorder/Adapters/DefaultRecorderPlatform.swift +33 -0
  53. package/ios/Sources/VoiceRecorder/Adapters/RecordDataMapper.swift +38 -0
  54. package/ios/Sources/VoiceRecorder/Adapters/RecorderAdapter.swift +24 -0
  55. package/ios/Sources/VoiceRecorder/Adapters/RecorderPlatform.swift +11 -0
  56. package/ios/Sources/VoiceRecorder/Bridge/VoiceRecorder.swift +172 -0
  57. package/ios/Sources/VoiceRecorder/{CurrentRecordingStatus.swift → Core/CurrentRecordingStatus.swift} +2 -0
  58. package/ios/Sources/VoiceRecorder/Core/ErrorCodes.swift +16 -0
  59. package/ios/Sources/VoiceRecorder/{Messages.swift → Core/Messages.swift} +2 -0
  60. package/ios/Sources/VoiceRecorder/{RecordData.swift → Core/RecordData.swift} +6 -0
  61. package/ios/Sources/VoiceRecorder/Core/RecordOptions.swift +11 -0
  62. package/ios/Sources/VoiceRecorder/Core/ResponseFormat.swift +22 -0
  63. package/ios/Sources/VoiceRecorder/{ResponseGenerator.swift → Core/ResponseGenerator.swift} +6 -0
  64. package/ios/Sources/VoiceRecorder/Platform/CustomMediaRecorder.swift +359 -0
  65. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderService.swift +128 -0
  66. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderServiceError.swift +14 -0
  67. package/package.json +10 -4
  68. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +0 -7
  69. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CustomMediaRecorder.java +0 -149
  70. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/NotSupportedOsVersion.java +0 -3
  71. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/RecordOptions.java +0 -3
  72. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +0 -203
  73. package/dist/esm/VoiceRecorderImpl.d.ts +0 -27
  74. package/dist/esm/VoiceRecorderImpl.js.map +0 -1
  75. package/dist/esm/helper/get-blob-duration.js.map +0 -1
  76. package/dist/esm/predefined-web-responses.js.map +0 -1
  77. package/ios/Sources/VoiceRecorder/CustomMediaRecorder.swift +0 -113
  78. package/ios/Sources/VoiceRecorder/RecordOptions.swift +0 -8
  79. package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +0 -147
  80. /package/dist/esm/{helper → platform/web}/get-blob-duration.d.ts +0 -0
  81. /package/dist/esm/{helper → platform/web}/get-blob-duration.js +0 -0
package/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  <br>
12
12
  <a href="https://www.npmjs.com/package/@independo/capacitor-voice-recorder"><img src="https://img.shields.io/npm/dw/@independo/capacitor-voice-recorder" alt="" role="presentation" /></a>
13
13
  <a href="https://www.npmjs.com/package/@independo/capacitor-voice-recorder"><img src="https://img.shields.io/npm/v/@independo/capacitor-voice-recorder" alt="" role="presentation" /></a>
14
+ <a href="https://codecov.io/gh/independo-gmbh/capacitor-voice-recorder/branch/main"><img src="https://codecov.io/gh/independo-gmbh/capacitor-voice-recorder/branch/main/graph/badge.svg" alt="Coverage Badge: main" /></a>
14
15
  </p>
15
16
 
16
17
  ## Installation
@@ -24,8 +25,8 @@ npx cap sync
24
25
 
25
26
  - Capacitor 8+
26
27
  - iOS 15+
27
- - Android minSdk 24+
28
- - Android builds require Java 21 (recommended). `npm run verify:android` requires a Java version supported by the bundled Gradle wrapper (currently Java 21–24, with Java 21 recommended).
28
+ - Android minSdk 24+; builds require Java 21 (recommended). `npm run verify:android` requires a Java version supported
29
+ by the bundled Gradle wrapper (currently Java 21–24, with Java 21 recommended).
29
30
 
30
31
  ## iOS Package Manager Support
31
32
 
@@ -41,7 +42,7 @@ This plugin supports both CocoaPods and Swift Package Manager (SPM) on iOS.
41
42
  Add the following to your `AndroidManifest.xml`:
42
43
 
43
44
  ```xml
44
- <uses-permission android:name="android.permission.RECORD_AUDIO" />
45
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
45
46
  ```
46
47
 
47
48
  ### Using with iOS
@@ -49,7 +50,6 @@ Add the following to your `AndroidManifest.xml`:
49
50
  Add the following to your `Info.plist`:
50
51
 
51
52
  ```xml
52
-
53
53
  <key>NSMicrophoneUsageDescription</key>
54
54
  <string>This app uses the microphone to record audio.</string>
55
55
  ```
@@ -60,7 +60,8 @@ The `@independo/capacitor-voice-recorder` plugin allows you to record audio on A
60
60
 
61
61
  ## API
62
62
 
63
- Below is an index of all available methods. Run `npm run docgen` after updating any JSDoc comments to refresh this section.
63
+ Below is an index of all available methods. Run `npm run docgen` after updating any JSDoc comments to refresh this
64
+ section.
64
65
 
65
66
  <docgen-index>
66
67
 
@@ -72,6 +73,9 @@ Below is an index of all available methods. Run `npm run docgen` after updating
72
73
  * [`pauseRecording()`](#pauserecording)
73
74
  * [`resumeRecording()`](#resumerecording)
74
75
  * [`getCurrentStatus()`](#getcurrentstatus)
76
+ * [`addListener('voiceRecordingInterrupted', ...)`](#addlistenervoicerecordinginterrupted-)
77
+ * [`addListener('voiceRecordingInterruptionEnded', ...)`](#addlistenervoicerecordinginterruptionended-)
78
+ * [`removeAllListeners()`](#removealllisteners)
75
79
  * [Interfaces](#interfaces)
76
80
  * [Type Aliases](#type-aliases)
77
81
  * [Enums](#enums)
@@ -162,6 +166,7 @@ Will stop the recording that has been previously started.
162
166
  If the function `startRecording` has not been called beforehand, the promise will reject with `RECORDING_HAS_NOT_STARTED`.
163
167
  If the recording has been stopped immediately after it has been started, the promise will reject with `EMPTY_RECORDING`.
164
168
  In a case of unknown error, the promise will reject with `FAILED_TO_FETCH_RECORDING`.
169
+ On iOS, if a recording interrupted by the system cannot be merged, the promise will reject with `FAILED_TO_MERGE_RECORDING`.
165
170
  In case of success, the promise resolves to <a href="#recordingdata">RecordingData</a> containing the recording in base-64, the duration of the recording in milliseconds, and the MIME type.
166
171
 
167
172
  **Returns:** <code>Promise&lt;<a href="#recordingdata">RecordingData</a>&gt;</code>
@@ -191,7 +196,7 @@ On certain mobile OS versions, this function is not supported and will reject wi
191
196
  resumeRecording() => Promise<GenericResponse>
192
197
  ```
193
198
 
194
- Resumes a paused audio recording.
199
+ Resumes a paused or interrupted audio recording.
195
200
  If the recording has not started yet, the promise will reject with an error code `RECORDING_HAS_NOT_STARTED`.
196
201
  On success, the promise will resolve to { value: true } if the resume was successful or { value: false } if the recording is already running.
197
202
  On certain mobile OS versions, this function is not supported and will reject with `NOT_SUPPORTED_OS_VERSION`.
@@ -212,12 +217,62 @@ Will resolve with one of the following values:
212
217
  `{ status: "NONE" }` if the plugin is idle and waiting to start a new recording.
213
218
  `{ status: "RECORDING" }` if the plugin is in the middle of recording.
214
219
  `{ status: "PAUSED" }` if the recording is paused.
220
+ `{ status: "INTERRUPTED" }` if the recording was paused due to a system interruption.
215
221
 
216
222
  **Returns:** <code>Promise&lt;<a href="#currentrecordingstatus">CurrentRecordingStatus</a>&gt;</code>
217
223
 
218
224
  --------------------
219
225
 
220
226
 
227
+ ### addListener('voiceRecordingInterrupted', ...)
228
+
229
+ ```typescript
230
+ addListener(eventName: 'voiceRecordingInterrupted', listenerFunc: (event: VoiceRecordingInterruptedEvent) => void) => Promise<PluginListenerHandle>
231
+ ```
232
+
233
+ Listen for audio recording interruptions (e.g., phone calls, other apps using microphone).
234
+ Available on iOS and Android only.
235
+
236
+ | Param | Type | Description |
237
+ | ------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
238
+ | **`eventName`** | <code>'voiceRecordingInterrupted'</code> | The name of the event to listen for. |
239
+ | **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptedevent">VoiceRecordingInterruptedEvent</a>) =&gt; void</code> | The callback function to invoke when the event occurs. |
240
+
241
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
242
+
243
+ --------------------
244
+
245
+
246
+ ### addListener('voiceRecordingInterruptionEnded', ...)
247
+
248
+ ```typescript
249
+ addListener(eventName: 'voiceRecordingInterruptionEnded', listenerFunc: (event: VoiceRecordingInterruptionEndedEvent) => void) => Promise<PluginListenerHandle>
250
+ ```
251
+
252
+ Listen for audio recording interruption end events.
253
+ Available on iOS and Android only.
254
+
255
+ | Param | Type | Description |
256
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
257
+ | **`eventName`** | <code>'voiceRecordingInterruptionEnded'</code> | The name of the event to listen for. |
258
+ | **`listenerFunc`** | <code>(event: <a href="#voicerecordinginterruptionendedevent">VoiceRecordingInterruptionEndedEvent</a>) =&gt; void</code> | The callback function to invoke when the event occurs. |
259
+
260
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
261
+
262
+ --------------------
263
+
264
+
265
+ ### removeAllListeners()
266
+
267
+ ```typescript
268
+ removeAllListeners() => Promise<void>
269
+ ```
270
+
271
+ Remove all listeners for this plugin.
272
+
273
+ --------------------
274
+
275
+
221
276
  ### Interfaces
222
277
 
223
278
 
@@ -253,9 +308,16 @@ Interface representing the data of a recording.
253
308
 
254
309
  Interface representing the current status of the voice recorder.
255
310
 
256
- | Prop | Type | Description |
257
- | ------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
258
- | **`status`** | <code>'NONE' \| 'RECORDING' \| 'PAUSED'</code> | The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'NONE'. |
311
+ | Prop | Type | Description |
312
+ | ------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
313
+ | **`status`** | <code>'NONE' \| 'RECORDING' \| 'PAUSED' \| 'INTERRUPTED'</code> | The current status of the recorder, which can be one of the following values: 'RECORDING', 'PAUSED', 'INTERRUPTED', 'NONE'. |
314
+
315
+
316
+ #### PluginListenerHandle
317
+
318
+ | Prop | Type |
319
+ | ------------ | ----------------------------------------- |
320
+ | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
259
321
 
260
322
 
261
323
  ### Type Aliases
@@ -268,6 +330,27 @@ Represents a Base64 encoded string.
268
330
  <code>string</code>
269
331
 
270
332
 
333
+ #### VoiceRecordingInterruptedEvent
334
+
335
+ Event payload for voiceRecordingInterrupted event (empty - no data).
336
+
337
+ <code><a href="#record">Record</a>&lt;string, never&gt;</code>
338
+
339
+
340
+ #### Record
341
+
342
+ Construct a type with a set of properties K of type T
343
+
344
+ <code>{
271
345
  [P in K]: T;
272
346
  }</code>
347
+
348
+
349
+ #### VoiceRecordingInterruptionEndedEvent
350
+
351
+ Event payload for voiceRecordingInterruptionEnded event (empty - no data).
352
+
353
+ <code><a href="#record">Record</a>&lt;string, never&gt;</code>
354
+
355
+
273
356
  ### Enums
274
357
 
275
358
 
@@ -287,32 +370,46 @@ Represents a Base64 encoded string.
287
370
 
288
371
  </docgen-api>
289
372
 
373
+ ## Audio interruption handling
374
+
375
+ On iOS and Android, the plugin listens for system audio interruptions (phone calls, other apps taking audio focus). When
376
+ an interruption begins, the recording is paused, the status becomes `INTERRUPTED`, and the `voiceRecordingInterrupted`
377
+ event fires. When the interruption ends, the `voiceRecordingInterruptionEnded` event fires, and the status stays
378
+ `INTERRUPTED` until you call `resumeRecording()` or `stopRecording()`. Web does not provide interruption handling.
379
+
380
+ If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type
381
+ `audio/mp4`. Recordings without interruptions remain AAC with MIME type `audio/aac`.
290
382
 
291
- ## Format and Mime type
383
+ ## Format and MIME type
292
384
 
293
- The plugin will return the recording in one of several possible formats.
294
- the format is dependent on the os / web browser that the user uses.
295
- on android and ios the mime type will be `audio/aac`, while on chrome and firefox it
296
- will be `audio/webm;codecs=opus` and on safari it will be `audio/mp4`.
297
- note that these 3 browsers has been tested on. the plugin should still work on
298
- other browsers, as there is a list of mime types that the plugin checks against the
299
- user's browser.
385
+ The plugin returns the recording in one of several possible formats. The actual MIME type depends on the platform and
386
+ browser capabilities.
300
387
 
301
- Note that this fact might cause unexpected behavior in case you'll try to play recordings
302
- between several devices or browsers - as they not all support the same set of audio formats.
303
- it is recommended to convert the recordings to a format that all your target devices supports.
304
- as this plugin focuses on the recording aspect, it does not provide any conversion between formats.
388
+ - Android: `audio/aac`
389
+ - iOS: `audio/aac` (or `audio/mp4` when interrupted recordings are merged)
390
+ - Web: first supported type from `audio/aac`, `audio/webm;codecs=opus`, `audio/ogg;codecs=opus`, `audio/webm`,
391
+ `audio/mp4`
392
+
393
+ Because not all devices and browsers support the same formats, recordings may not be playable everywhere. If you need
394
+ consistent playback across targets, convert recordings to a single format outside this plugin. The plugin focuses on
395
+ recording only and does not perform format conversion.
305
396
 
306
397
  ## Playback
307
398
 
308
- To play the recorded file you can use plain javascript:
399
+ To play a recording, prefer `uri` when available. On native platforms, pass it through
400
+ `Capacitor.convertFileSrc` before using it in the web view.
309
401
 
310
402
  ```typescript
311
- const base64Sound = '...' // from plugin
312
- const mimeType = '...' // from plugin
313
- const audioRef = new Audio(`data:${mimeType};base64,${base64Sound}`)
314
- audioRef.oncanplaythrough = () => audioRef.play()
315
- audioRef.load()
403
+ import {Capacitor} from '@capacitor/core';
404
+
405
+ const {recordDataBase64, mimeType, uri} = result.value;
406
+ const source = uri
407
+ ? Capacitor.convertFileSrc(uri)
408
+ : `data:${mimeType};base64,${recordDataBase64}`;
409
+
410
+ const audioRef = new Audio(source);
411
+ audioRef.oncanplaythrough = () => audioRef.play();
412
+ audioRef.load();
316
413
  ```
317
414
 
318
415
  ## Compatibility
@@ -326,9 +423,10 @@ Versioning follows Capacitor versioning. Major versions of the plugin are compat
326
423
  | 7.* | 7 |
327
424
  | 8.* | 8 |
328
425
 
329
- ## Collaborators
426
+ ## Origins and credit
330
427
 
331
- | Collaborators | | GitHub | Donation |
332
- |--------------------|-------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
333
- | Avihu Harush | Original Author | [tchvu3](https://github.com/tchvu3) | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/tchvu3) |
334
- | Konstantin Strümpf | Contributor for [Independo GmbH](https://www.independo.app) | [kstruempf](https://github.com/kstruempf) | |
428
+ This project started as a fork of [
429
+ `tchvu3/capacitor-voice-recorder`](https://github.com/tchvu3/capacitor-voice-recorder).
430
+ Thanks to Avihu Harush for the original implementation and community groundwork. Since then, the plugin has been
431
+ re-architected for improved performance, reliability, and testability (service/adapters split, contract tests, and a
432
+ normalized response path). The codebase now diverges substantially, which is why this repo left the fork network.
@@ -16,9 +16,10 @@ buildscript {
16
16
  }
17
17
 
18
18
  apply plugin: 'com.android.library'
19
+ apply plugin: 'jacoco'
19
20
 
20
21
  android {
21
- namespace = "com.tchvu3.capacitorvoicerecorder"
22
+ namespace = "app.independo.capacitorvoicerecorder"
22
23
  compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
23
24
  defaultConfig {
24
25
  minSdk = project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
@@ -33,6 +34,14 @@ android {
33
34
  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
34
35
  }
35
36
  }
37
+ testOptions {
38
+ unitTests.all {
39
+ jacoco {
40
+ includeNoLocationClasses = true
41
+ excludes = ['jdk.internal.*']
42
+ }
43
+ }
44
+ }
36
45
  lint {
37
46
  abortOnError = false
38
47
  }
@@ -42,6 +51,10 @@ android {
42
51
  }
43
52
  }
44
53
 
54
+ jacoco {
55
+ toolVersion = '0.8.11'
56
+ }
57
+
45
58
  repositories {
46
59
  google()
47
60
  mavenCentral()
@@ -53,6 +66,36 @@ dependencies {
53
66
  implementation project(':capacitor-android')
54
67
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55
68
  testImplementation "junit:junit:$junitVersion"
69
+ testImplementation "org.json:json:20240303"
56
70
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
57
71
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
58
72
  }
73
+
74
+ tasks.register('jacocoTestReport', JacocoReport) {
75
+ dependsOn tasks.named('testDebugUnitTest')
76
+
77
+ def fileFilter = [
78
+ '**/R.class',
79
+ '**/R$*.class',
80
+ '**/BuildConfig.*',
81
+ '**/Manifest*.*',
82
+ '**/*Test*.*',
83
+ 'android/**/*.*',
84
+ ]
85
+ def javaClasses = fileTree(dir: "$buildDir/intermediates/javac/debug/classes", excludes: fileFilter)
86
+ def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
87
+ classDirectories.setFrom(files([javaClasses, kotlinClasses]))
88
+ sourceDirectories.setFrom(files(['src/main/java', 'src/main/kotlin']))
89
+ executionData.setFrom(
90
+ fileTree(dir: buildDir, includes: [
91
+ 'jacoco/testDebugUnitTest.exec',
92
+ 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec',
93
+ ]),
94
+ )
95
+
96
+ reports {
97
+ xml.required = true
98
+ html.required = true
99
+ csv.required = false
100
+ }
101
+ }
@@ -0,0 +1,146 @@
1
+ package app.independo.capacitorvoicerecorder;
2
+
3
+ import android.Manifest;
4
+ import app.independo.capacitorvoicerecorder.adapters.PermissionChecker;
5
+ import app.independo.capacitorvoicerecorder.adapters.RecordDataMapper;
6
+ import app.independo.capacitorvoicerecorder.adapters.RecorderPlatform;
7
+ import app.independo.capacitorvoicerecorder.core.ErrorCodes;
8
+ import app.independo.capacitorvoicerecorder.core.Messages;
9
+ import app.independo.capacitorvoicerecorder.core.RecordData;
10
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
11
+ import app.independo.capacitorvoicerecorder.core.ResponseFormat;
12
+ import app.independo.capacitorvoicerecorder.core.ResponseGenerator;
13
+ import app.independo.capacitorvoicerecorder.platform.DefaultRecorderPlatform;
14
+ import app.independo.capacitorvoicerecorder.service.VoiceRecorderService;
15
+ import app.independo.capacitorvoicerecorder.service.VoiceRecorderServiceException;
16
+ import com.getcapacitor.PermissionState;
17
+ import com.getcapacitor.Plugin;
18
+ import com.getcapacitor.PluginCall;
19
+ import com.getcapacitor.PluginMethod;
20
+ import com.getcapacitor.annotation.CapacitorPlugin;
21
+ import com.getcapacitor.annotation.Permission;
22
+ import com.getcapacitor.annotation.PermissionCallback;
23
+
24
+ @CapacitorPlugin(
25
+ name = "VoiceRecorder",
26
+ permissions = { @Permission(alias = VoiceRecorder.RECORD_AUDIO_ALIAS, strings = { Manifest.permission.RECORD_AUDIO }) }
27
+ )
28
+ /** Capacitor bridge for the VoiceRecorder plugin. */
29
+ public class VoiceRecorder extends Plugin {
30
+
31
+ /** Permission alias used by the Capacitor permission API. */
32
+ static final String RECORD_AUDIO_ALIAS = "voice recording";
33
+ /** Service layer that owns recording flows and validation. */
34
+ private VoiceRecorderService service;
35
+ /** Response format derived from plugin configuration. */
36
+ private ResponseFormat responseFormat;
37
+
38
+ @Override
39
+ public void load() {
40
+ super.load();
41
+ responseFormat = ResponseFormat.fromConfig(getConfig());
42
+ RecorderPlatform platform = new DefaultRecorderPlatform(getContext());
43
+ PermissionChecker permissionChecker = this::doesUserGaveAudioRecordingPermission;
44
+ service = new VoiceRecorderService(platform, permissionChecker);
45
+ }
46
+
47
+ /** Checks whether the device can record audio. */
48
+ @PluginMethod
49
+ public void canDeviceVoiceRecord(PluginCall call) {
50
+ call.resolve(ResponseGenerator.fromBoolean(service.canDeviceVoiceRecord()));
51
+ }
52
+
53
+ /** Requests microphone permission or returns success if already granted. */
54
+ @PluginMethod
55
+ public void requestAudioRecordingPermission(PluginCall call) {
56
+ if (service.hasAudioRecordingPermission()) {
57
+ call.resolve(ResponseGenerator.successResponse());
58
+ } else {
59
+ requestPermissionForAlias(RECORD_AUDIO_ALIAS, call, "recordAudioPermissionCallback");
60
+ }
61
+ }
62
+
63
+ /** Forwards permission results back to the JS call. */
64
+ @PermissionCallback
65
+ private void recordAudioPermissionCallback(PluginCall call) {
66
+ this.hasAudioRecordingPermission(call);
67
+ }
68
+
69
+ /** Returns whether the app has microphone permission. */
70
+ @PluginMethod
71
+ public void hasAudioRecordingPermission(PluginCall call) {
72
+ call.resolve(ResponseGenerator.fromBoolean(service.hasAudioRecordingPermission()));
73
+ }
74
+
75
+ /** Starts a recording session. */
76
+ @PluginMethod
77
+ public void startRecording(PluginCall call) {
78
+ try {
79
+ String directory = call.getString("directory");
80
+ String subDirectory = call.getString("subDirectory");
81
+ RecordOptions options = new RecordOptions(directory, subDirectory);
82
+ service.startRecording(
83
+ options,
84
+ () -> notifyListeners("voiceRecordingInterrupted", null),
85
+ () -> notifyListeners("voiceRecordingInterruptionEnded", null)
86
+ );
87
+ call.resolve(ResponseGenerator.successResponse());
88
+ } catch (VoiceRecorderServiceException exp) {
89
+ call.reject(toLegacyMessage(exp.getCode()), exp.getCode(), exp);
90
+ }
91
+ }
92
+
93
+ /** Stops recording and returns the recording payload. */
94
+ @PluginMethod
95
+ public void stopRecording(PluginCall call) {
96
+ try {
97
+ RecordData recordData = service.stopRecording();
98
+ if (responseFormat == ResponseFormat.NORMALIZED) {
99
+ call.resolve(ResponseGenerator.dataResponse(RecordDataMapper.toNormalizedJSObject(recordData)));
100
+ } else {
101
+ call.resolve(ResponseGenerator.dataResponse(RecordDataMapper.toLegacyJSObject(recordData)));
102
+ }
103
+ } catch (VoiceRecorderServiceException exp) {
104
+ call.reject(toLegacyMessage(exp.getCode()), exp.getCode(), exp);
105
+ }
106
+ }
107
+
108
+ /** Pauses an active recording session if supported. */
109
+ @PluginMethod
110
+ public void pauseRecording(PluginCall call) {
111
+ try {
112
+ call.resolve(ResponseGenerator.fromBoolean(service.pauseRecording()));
113
+ } catch (VoiceRecorderServiceException exception) {
114
+ call.reject(toLegacyMessage(exception.getCode()), exception.getCode(), exception);
115
+ }
116
+ }
117
+
118
+ /** Resumes a paused recording session if supported. */
119
+ @PluginMethod
120
+ public void resumeRecording(PluginCall call) {
121
+ try {
122
+ call.resolve(ResponseGenerator.fromBoolean(service.resumeRecording()));
123
+ } catch (VoiceRecorderServiceException exception) {
124
+ call.reject(toLegacyMessage(exception.getCode()), exception.getCode(), exception);
125
+ }
126
+ }
127
+
128
+ /** Returns the current recording status. */
129
+ @PluginMethod
130
+ public void getCurrentStatus(PluginCall call) {
131
+ call.resolve(ResponseGenerator.statusResponse(service.getCurrentStatus()));
132
+ }
133
+
134
+ /** Checks whether the app has the RECORD_AUDIO permission. */
135
+ private boolean doesUserGaveAudioRecordingPermission() {
136
+ return getPermissionState(VoiceRecorder.RECORD_AUDIO_ALIAS).equals(PermissionState.GRANTED);
137
+ }
138
+
139
+ /** Maps canonical error codes back to legacy error messages. */
140
+ private String toLegacyMessage(String canonicalCode) {
141
+ if (ErrorCodes.DEVICE_CANNOT_VOICE_RECORD.equals(canonicalCode)) {
142
+ return Messages.CANNOT_RECORD_ON_THIS_PHONE;
143
+ }
144
+ return canonicalCode;
145
+ }
146
+ }
@@ -0,0 +1,8 @@
1
+ package app.independo.capacitorvoicerecorder.adapters;
2
+
3
+ /** Functional interface used to check microphone permission. */
4
+ @FunctionalInterface
5
+ public interface PermissionChecker {
6
+ /** Returns whether the app currently has microphone permission. */
7
+ boolean hasAudioPermission();
8
+ }
@@ -0,0 +1,32 @@
1
+ package app.independo.capacitorvoicerecorder.adapters;
2
+
3
+ import app.independo.capacitorvoicerecorder.core.RecordData;
4
+ import com.getcapacitor.JSObject;
5
+
6
+ /** Maps internal record data into legacy or normalized JS payloads. */
7
+ public final class RecordDataMapper {
8
+
9
+ private RecordDataMapper() {}
10
+
11
+ /** Converts record data to the legacy payload shape. */
12
+ public static JSObject toLegacyJSObject(RecordData recordData) {
13
+ return recordData.toJSObject();
14
+ }
15
+
16
+ /** Converts record data to the normalized payload shape. */
17
+ public static JSObject toNormalizedJSObject(RecordData recordData) {
18
+ JSObject normalized = new JSObject();
19
+ normalized.put("msDuration", recordData.getMsDuration());
20
+ normalized.put("mimeType", recordData.getMimeType());
21
+
22
+ String uri = recordData.getUri();
23
+ String recordDataBase64 = recordData.getRecordDataBase64();
24
+ if (uri != null && !uri.isEmpty()) {
25
+ normalized.put("uri", uri);
26
+ } else if (recordDataBase64 != null && !recordDataBase64.isEmpty()) {
27
+ normalized.put("recordDataBase64", recordDataBase64);
28
+ }
29
+
30
+ return normalized;
31
+ }
32
+ }
@@ -0,0 +1,39 @@
1
+ package app.independo.capacitorvoicerecorder.adapters;
2
+
3
+ import app.independo.capacitorvoicerecorder.core.CurrentRecordingStatus;
4
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
5
+ import app.independo.capacitorvoicerecorder.platform.NotSupportedOsVersion;
6
+ import java.io.File;
7
+
8
+ /** Recorder abstraction used by the service layer. */
9
+ public interface RecorderAdapter {
10
+ /** Sets a callback invoked when interruptions begin. */
11
+ void setOnInterruptionBegan(Runnable callback);
12
+
13
+ /** Sets a callback invoked when interruptions end. */
14
+ void setOnInterruptionEnded(Runnable callback);
15
+
16
+ /** Starts recording audio. */
17
+ void startRecording();
18
+
19
+ /** Stops recording audio. */
20
+ void stopRecording();
21
+
22
+ /** Pauses recording if supported. */
23
+ boolean pauseRecording() throws NotSupportedOsVersion;
24
+
25
+ /** Resumes recording if supported. */
26
+ boolean resumeRecording() throws NotSupportedOsVersion;
27
+
28
+ /** Returns the current recording status. */
29
+ CurrentRecordingStatus getCurrentStatus();
30
+
31
+ /** Returns the output file for the recording. */
32
+ File getOutputFile();
33
+
34
+ /** Returns the options used to start recording. */
35
+ RecordOptions getRecordOptions();
36
+
37
+ /** Deletes the output file from disk. */
38
+ boolean deleteOutputFile();
39
+ }
@@ -0,0 +1,25 @@
1
+ package app.independo.capacitorvoicerecorder.adapters;
2
+
3
+ import app.independo.capacitorvoicerecorder.core.RecordOptions;
4
+ import java.io.File;
5
+
6
+ /** Platform abstraction for device and file operations. */
7
+ public interface RecorderPlatform {
8
+ /** Returns whether the device can record audio. */
9
+ boolean canDeviceVoiceRecord();
10
+
11
+ /** Returns true when the microphone is in use elsewhere. */
12
+ boolean isMicrophoneOccupied();
13
+
14
+ /** Creates a recorder instance for the given options. */
15
+ RecorderAdapter createRecorder(RecordOptions options) throws Exception;
16
+
17
+ /** Reads the recording file as base64, or null on failure. */
18
+ String readFileAsBase64(File recordedFile);
19
+
20
+ /** Returns the recording duration in milliseconds. */
21
+ int getDurationMs(File recordedFile);
22
+
23
+ /** Returns the URI for the recording file. */
24
+ String toUri(File recordedFile);
25
+ }
@@ -0,0 +1,9 @@
1
+ package app.independo.capacitorvoicerecorder.core;
2
+
3
+ /** Represents the current recording state. */
4
+ public enum CurrentRecordingStatus {
5
+ RECORDING,
6
+ PAUSED,
7
+ INTERRUPTED,
8
+ NONE
9
+ }
@@ -0,0 +1,19 @@
1
+ package app.independo.capacitorvoicerecorder.core;
2
+
3
+ /** Canonical error codes returned by the plugin. */
4
+ public final class ErrorCodes {
5
+
6
+ public static final String MISSING_PERMISSION = "MISSING_PERMISSION";
7
+ public static final String ALREADY_RECORDING = "ALREADY_RECORDING";
8
+ public static final String MICROPHONE_BEING_USED = "MICROPHONE_BEING_USED";
9
+ public static final String DEVICE_CANNOT_VOICE_RECORD = "DEVICE_CANNOT_VOICE_RECORD";
10
+ public static final String FAILED_TO_RECORD = "FAILED_TO_RECORD";
11
+ public static final String EMPTY_RECORDING = "EMPTY_RECORDING";
12
+ public static final String RECORDING_HAS_NOT_STARTED = "RECORDING_HAS_NOT_STARTED";
13
+ public static final String FAILED_TO_FETCH_RECORDING = "FAILED_TO_FETCH_RECORDING";
14
+ public static final String FAILED_TO_MERGE_RECORDING = "FAILED_TO_MERGE_RECORDING";
15
+ public static final String NOT_SUPPORTED_OS_VERSION = "NOT_SUPPORTED_OS_VERSION";
16
+ public static final String COULD_NOT_QUERY_PERMISSION_STATUS = "COULD_NOT_QUERY_PERMISSION_STATUS";
17
+
18
+ private ErrorCodes() {}
19
+ }
@@ -1,5 +1,6 @@
1
- package com.tchvu3.capacitorvoicerecorder;
1
+ package app.independo.capacitorvoicerecorder.core;
2
2
 
3
+ /** Legacy error messages preserved for backward compatibility. */
3
4
  public abstract class Messages {
4
5
 
5
6
  public static final String MISSING_PERMISSION = "MISSING_PERMISSION";