@independo/capacitor-voice-recorder 8.1.0-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 (76) hide show
  1. package/README.md +40 -30
  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/{com/tchvu3/capacitorvoicerecorder → app/independo/capacitorvoicerecorder/platform}/CustomMediaRecorder.java +33 -2
  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/esm/adapters/VoiceRecorderWebAdapter.d.ts +23 -0
  21. package/dist/esm/adapters/VoiceRecorderWebAdapter.js +41 -0
  22. package/dist/esm/adapters/VoiceRecorderWebAdapter.js.map +1 -0
  23. package/dist/esm/core/error-codes.d.ts +4 -0
  24. package/dist/esm/core/error-codes.js +21 -0
  25. package/dist/esm/core/error-codes.js.map +1 -0
  26. package/dist/esm/core/recording-contract.d.ts +3 -0
  27. package/dist/esm/core/recording-contract.js +15 -0
  28. package/dist/esm/core/recording-contract.js.map +1 -0
  29. package/dist/esm/core/response-format.d.ts +8 -0
  30. package/dist/esm/core/response-format.js +17 -0
  31. package/dist/esm/core/response-format.js.map +1 -0
  32. package/dist/esm/platform/web/VoiceRecorderImpl.d.ts +45 -0
  33. package/dist/esm/{VoiceRecorderImpl.js → platform/web/VoiceRecorderImpl.js} +20 -2
  34. package/dist/esm/platform/web/VoiceRecorderImpl.js.map +1 -0
  35. package/dist/esm/platform/web/get-blob-duration.js.map +1 -0
  36. package/dist/esm/{predefined-web-responses.d.ts → platform/web/predefined-web-responses.d.ts} +12 -1
  37. package/dist/esm/{predefined-web-responses.js → platform/web/predefined-web-responses.js} +11 -0
  38. package/dist/esm/platform/web/predefined-web-responses.js.map +1 -0
  39. package/dist/esm/service/VoiceRecorderService.d.ts +47 -0
  40. package/dist/esm/service/VoiceRecorderService.js +60 -0
  41. package/dist/esm/service/VoiceRecorderService.js.map +1 -0
  42. package/dist/esm/web.d.ts +12 -1
  43. package/dist/esm/web.js +26 -12
  44. package/dist/esm/web.js.map +1 -1
  45. package/dist/plugin.cjs.js +200 -9
  46. package/dist/plugin.cjs.js.map +1 -1
  47. package/dist/plugin.js +200 -9
  48. package/dist/plugin.js.map +1 -1
  49. package/ios/Sources/VoiceRecorder/Adapters/DefaultRecorderPlatform.swift +33 -0
  50. package/ios/Sources/VoiceRecorder/Adapters/RecordDataMapper.swift +38 -0
  51. package/ios/Sources/VoiceRecorder/Adapters/RecorderAdapter.swift +24 -0
  52. package/ios/Sources/VoiceRecorder/Adapters/RecorderPlatform.swift +11 -0
  53. package/ios/Sources/VoiceRecorder/Bridge/VoiceRecorder.swift +172 -0
  54. package/ios/Sources/VoiceRecorder/{CurrentRecordingStatus.swift → Core/CurrentRecordingStatus.swift} +1 -0
  55. package/ios/Sources/VoiceRecorder/Core/ErrorCodes.swift +16 -0
  56. package/ios/Sources/VoiceRecorder/{Messages.swift → Core/Messages.swift} +1 -0
  57. package/ios/Sources/VoiceRecorder/{RecordData.swift → Core/RecordData.swift} +6 -0
  58. package/ios/Sources/VoiceRecorder/Core/RecordOptions.swift +11 -0
  59. package/ios/Sources/VoiceRecorder/Core/ResponseFormat.swift +22 -0
  60. package/ios/Sources/VoiceRecorder/{ResponseGenerator.swift → Core/ResponseGenerator.swift} +6 -0
  61. package/ios/Sources/VoiceRecorder/{CustomMediaRecorder.swift → Platform/CustomMediaRecorder.swift} +25 -1
  62. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderService.swift +128 -0
  63. package/ios/Sources/VoiceRecorder/Service/VoiceRecorderServiceError.swift +14 -0
  64. package/package.json +10 -4
  65. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/CurrentRecordingStatus.java +0 -8
  66. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/NotSupportedOsVersion.java +0 -3
  67. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/RecordOptions.java +0 -3
  68. package/android/src/main/java/com/tchvu3/capacitorvoicerecorder/VoiceRecorder.java +0 -205
  69. package/dist/esm/VoiceRecorderImpl.d.ts +0 -27
  70. package/dist/esm/VoiceRecorderImpl.js.map +0 -1
  71. package/dist/esm/helper/get-blob-duration.js.map +0 -1
  72. package/dist/esm/predefined-web-responses.js.map +0 -1
  73. package/ios/Sources/VoiceRecorder/RecordOptions.swift +0 -8
  74. package/ios/Sources/VoiceRecorder/VoiceRecorder.swift +0 -170
  75. /package/dist/esm/{helper → platform/web}/get-blob-duration.d.ts +0 -0
  76. /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
 
@@ -371,36 +372,44 @@ Event payload for voiceRecordingInterruptionEnded event (empty - no data).
371
372
 
372
373
  ## Audio interruption handling
373
374
 
374
- On iOS and Android, the plugin listens for system audio interruptions (phone calls, other apps taking audio focus). When an interruption begins, the recording is paused, the status becomes `INTERRUPTED`, and the `voiceRecordingInterrupted` event fires. When the interruption ends, the `voiceRecordingInterruptionEnded` event fires, and the status stays `INTERRUPTED` until you call `resumeRecording()` or `stopRecording()`. Web does not provide interruption handling.
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.
375
379
 
376
- If interruptions occur on iOS, recordings are segmented and merged when you stop. The merged file is M4A with MIME type `audio/mp4`. Recordings without interruptions remain AAC with MIME type `audio/aac`.
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`.
377
382
 
383
+ ## Format and MIME type
378
384
 
379
- ## Format and Mime type
385
+ The plugin returns the recording in one of several possible formats. The actual MIME type depends on the platform and
386
+ browser capabilities.
380
387
 
381
- The plugin will return the recording in one of several possible formats.
382
- the format is dependent on the os / web browser that the user uses.
383
- on android and ios the mime type will be `audio/aac`, while on chrome and firefox it
384
- will be `audio/webm;codecs=opus` and on safari it will be `audio/mp4`.
385
- note that these 3 browsers has been tested on. the plugin should still work on
386
- other browsers, as there is a list of mime types that the plugin checks against the
387
- user's browser.
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`
388
392
 
389
- Note that this fact might cause unexpected behavior in case you'll try to play recordings
390
- between several devices or browsers - as they not all support the same set of audio formats.
391
- it is recommended to convert the recordings to a format that all your target devices supports.
392
- as this plugin focuses on the recording aspect, it does not provide any conversion between formats.
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.
393
396
 
394
397
  ## Playback
395
398
 
396
- 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.
397
401
 
398
402
  ```typescript
399
- const base64Sound = '...' // from plugin
400
- const mimeType = '...' // from plugin
401
- const audioRef = new Audio(`data:${mimeType};base64,${base64Sound}`)
402
- audioRef.oncanplaythrough = () => audioRef.play()
403
- 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();
404
413
  ```
405
414
 
406
415
  ## Compatibility
@@ -414,9 +423,10 @@ Versioning follows Capacitor versioning. Major versions of the plugin are compat
414
423
  | 7.* | 7 |
415
424
  | 8.* | 8 |
416
425
 
417
- ## Collaborators
426
+ ## Origins and credit
418
427
 
419
- | Collaborators | | GitHub | Donation |
420
- |--------------------|-------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
421
- | 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) |
422
- | 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";
@@ -1,12 +1,17 @@
1
- package com.tchvu3.capacitorvoicerecorder;
1
+ package app.independo.capacitorvoicerecorder.core;
2
2
 
3
3
  import com.getcapacitor.JSObject;
4
4
 
5
+ /** Recording payload returned to the bridge layer. */
5
6
  public class RecordData {
6
7
 
8
+ /** File URI when returning recordings by reference. */
7
9
  private String uri;
10
+ /** Base64 payload for inline recording data. */
8
11
  private String recordDataBase64;
12
+ /** MIME type of the audio payload. */
9
13
  private String mimeType;
14
+ /** Recording duration in milliseconds. */
10
15
  private int msDuration;
11
16
 
12
17
  public RecordData() {}
@@ -18,6 +23,7 @@ public class RecordData {
18
23
  this.uri = uri;
19
24
  }
20
25
 
26
+ /** Returns the base64 payload, if present. */
21
27
  public String getRecordDataBase64() {
22
28
  return recordDataBase64;
23
29
  }
@@ -26,6 +32,7 @@ public class RecordData {
26
32
  this.recordDataBase64 = recordDataBase64;
27
33
  }
28
34
 
35
+ /** Returns the recording duration in milliseconds. */
29
36
  public int getMsDuration() {
30
37
  return msDuration;
31
38
  }
@@ -34,6 +41,7 @@ public class RecordData {
34
41
  this.msDuration = msDuration;
35
42
  }
36
43
 
44
+ /** Returns the MIME type of the recording. */
37
45
  public String getMimeType() {
38
46
  return mimeType;
39
47
  }
@@ -42,6 +50,12 @@ public class RecordData {
42
50
  this.mimeType = mimeType;
43
51
  }
44
52
 
53
+ /** Returns the file URI, if present. */
54
+ public String getUri() {
55
+ return uri;
56
+ }
57
+
58
+ /** Serializes the record data into the legacy JS payload shape. */
45
59
  public JSObject toJSObject() {
46
60
  JSObject toReturn = new JSObject();
47
61
  toReturn.put("recordDataBase64", recordDataBase64);
@@ -0,0 +1,4 @@
1
+ package app.independo.capacitorvoicerecorder.core;
2
+
3
+ /** Optional output configuration for recordings. */
4
+ public record RecordOptions(String directory, String subDirectory) {}
@@ -0,0 +1,18 @@
1
+ package app.independo.capacitorvoicerecorder.core;
2
+
3
+ import com.getcapacitor.PluginConfig;
4
+
5
+ /** Supported response payload shapes. */
6
+ public enum ResponseFormat {
7
+ LEGACY,
8
+ NORMALIZED;
9
+
10
+ /** Reads the response format from plugin configuration. */
11
+ public static ResponseFormat fromConfig(PluginConfig config) {
12
+ String value = config.getString("responseFormat", "legacy");
13
+ if ("normalized".equalsIgnoreCase(value)) {
14
+ return NORMALIZED;
15
+ }
16
+ return LEGACY;
17
+ }
18
+ }
@@ -1,34 +1,40 @@
1
- package com.tchvu3.capacitorvoicerecorder;
1
+ package app.independo.capacitorvoicerecorder.core;
2
2
 
3
3
  import com.getcapacitor.JSObject;
4
4
 
5
+ /** Helper for building JS payloads in the legacy response shape. */
5
6
  public class ResponseGenerator {
6
7
 
7
8
  private static final String VALUE_RESPONSE_KEY = "value";
8
9
  private static final String STATUS_RESPONSE_KEY = "status";
9
10
 
11
+ /** Wraps a boolean value into the response shape. */
10
12
  public static JSObject fromBoolean(boolean value) {
11
13
  return value ? successResponse() : failResponse();
12
14
  }
13
15
 
16
+ /** Returns a success response with value=true. */
14
17
  public static JSObject successResponse() {
15
18
  JSObject success = new JSObject();
16
19
  success.put(VALUE_RESPONSE_KEY, true);
17
20
  return success;
18
21
  }
19
22
 
23
+ /** Returns a failure response with value=false. */
20
24
  public static JSObject failResponse() {
21
25
  JSObject success = new JSObject();
22
26
  success.put(VALUE_RESPONSE_KEY, false);
23
27
  return success;
24
28
  }
25
29
 
30
+ /** Wraps arbitrary data into the response shape. */
26
31
  public static JSObject dataResponse(Object data) {
27
32
  JSObject success = new JSObject();
28
33
  success.put(VALUE_RESPONSE_KEY, data);
29
34
  return success;
30
35
  }
31
36
 
37
+ /** Wraps the recording status into the response shape. */
32
38
  public static JSObject statusResponse(CurrentRecordingStatus status) {
33
39
  JSObject success = new JSObject();
34
40
  success.put(STATUS_RESPONSE_KEY, status.name());