@siteed/audio-studio 3.2.1-beta.2 → 3.2.1-beta.3

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
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.2.1-beta.3] - 2026-05-31
11
+
12
+ ### Fixed
13
+
14
+ - Android Expo plugin foreground-only configuration now removes the recording
15
+ service and receiver manifest entries when `enableBackgroundAudio` is
16
+ disabled. Regenerate native projects with Expo prebuild or rebuild Android
17
+ after changing this option.
18
+
10
19
  ## [3.2.1-beta.2] - 2026-05-29
11
20
 
12
21
  Beta release for validating max-duration recording controls after auto-stop result handling fixes.
@@ -745,7 +754,8 @@ Beta release for client validation of the progressive decode API.
745
754
  - Audio features extraction during recording
746
755
  - Consistent WAV PCM recording format across all platforms
747
756
 
748
- [unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.2.1-beta.2...HEAD
757
+ [unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.2.1-beta.3...HEAD
758
+ [3.2.1-beta.3]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.2.1-beta.2...@siteed/audio-studio@3.2.1-beta.3
749
759
  [3.2.1-beta.2]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.2.0...@siteed/audio-studio@3.2.1-beta.2
750
760
  [3.2.0]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.1.1...@siteed/audio-studio@3.2.0
751
761
  [3.1.1]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.1.0...@siteed/audio-studio@3.1.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/audio-studio",
3
- "version": "3.2.1-beta.2",
3
+ "version": "3.2.1-beta.3",
4
4
  "description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -93,6 +93,7 @@
93
93
  "lint": "expo-module lint",
94
94
  "lint:fix": "expo-module lint --fix",
95
95
  "test": "expo-module test",
96
+ "test:plugin": "jest --runInBand plugin/src/index.test.ts",
96
97
  "test:android": "yarn test:android:unit && yarn test:android:instrumented",
97
98
  "test:android:unit": "cd ../../apps/playground/android && ./gradlew :siteed-audio-studio:test",
98
99
  "test:android:instrumented": "cd ../../apps/playground/android && ./gradlew :siteed-audio-studio:connectedAndroidTest",
@@ -1,9 +1,83 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__testing = void 0;
3
4
  const config_plugins_1 = require("@expo/config-plugins");
4
5
  const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
5
6
  const NOTIFICATION_USAGE = 'Show recording notifications and controls';
6
7
  const LOG_PREFIX = '[@siteed/expo-audio-studio]';
8
+ const AUDIO_STUDIO_ANDROID_PACKAGE = 'net.siteed.audiostudio';
9
+ const RECORDING_ACTION_RECEIVER = `${AUDIO_STUDIO_ANDROID_PACKAGE}.RecordingActionReceiver`;
10
+ const AUDIO_RECORDING_SERVICE = `${AUDIO_STUDIO_ANDROID_PACKAGE}.AudioRecordingService`;
11
+ const LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER = '.RecordingActionReceiver';
12
+ const LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE = '.AudioRecordingService';
13
+ function removeComponentsByName(components, names) {
14
+ if (!components) {
15
+ return components;
16
+ }
17
+ const filtered = components.filter((component) => !names.includes(String(component.$?.['android:name'])));
18
+ return filtered.length > 0 ? filtered : undefined;
19
+ }
20
+ function upsertComponent(components, componentConfig) {
21
+ const name = String(componentConfig.$?.['android:name']);
22
+ const nextComponents = (components || []).filter((component) => String(component.$?.['android:name']) !== name);
23
+ nextComponents.push(componentConfig);
24
+ return nextComponents;
25
+ }
26
+ function addAndroidRemovalMarker(components, componentName) {
27
+ return upsertComponent(components, {
28
+ $: {
29
+ 'android:name': componentName,
30
+ 'tools:node': 'remove',
31
+ },
32
+ });
33
+ }
34
+ function configureAndroidBackgroundRecordingComponents(mainApplication, enableBackgroundAudio) {
35
+ const receiverNames = [
36
+ LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER,
37
+ RECORDING_ACTION_RECEIVER,
38
+ ];
39
+ const serviceNames = [
40
+ LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE,
41
+ AUDIO_RECORDING_SERVICE,
42
+ ];
43
+ mainApplication.receiver = removeComponentsByName(mainApplication.receiver, receiverNames);
44
+ mainApplication.service = removeComponentsByName(mainApplication.service, serviceNames);
45
+ if (enableBackgroundAudio) {
46
+ const receiverConfig = {
47
+ $: {
48
+ 'android:name': RECORDING_ACTION_RECEIVER,
49
+ 'android:exported': 'false',
50
+ },
51
+ 'intent-filter': [
52
+ {
53
+ action: [
54
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
55
+ { $: { 'android:name': 'RESUME_RECORDING' } },
56
+ { $: { 'android:name': 'STOP_RECORDING' } },
57
+ ],
58
+ },
59
+ ],
60
+ };
61
+ const serviceConfig = {
62
+ $: {
63
+ 'android:name': AUDIO_RECORDING_SERVICE,
64
+ 'android:enabled': 'true',
65
+ 'android:exported': 'false',
66
+ 'android:foregroundServiceType': 'microphone',
67
+ },
68
+ };
69
+ mainApplication.receiver = upsertComponent(mainApplication.receiver, receiverConfig);
70
+ mainApplication.service = upsertComponent(mainApplication.service, serviceConfig);
71
+ return;
72
+ }
73
+ mainApplication.receiver = addAndroidRemovalMarker(mainApplication.receiver, RECORDING_ACTION_RECEIVER);
74
+ mainApplication.service = addAndroidRemovalMarker(mainApplication.service, AUDIO_RECORDING_SERVICE);
75
+ }
76
+ exports.__testing = {
77
+ AUDIO_RECORDING_SERVICE,
78
+ RECORDING_ACTION_RECEIVER,
79
+ configureAndroidBackgroundRecordingComponents,
80
+ };
7
81
  function debugLog(message, ...args) {
8
82
  if (process.env.EXPO_DEBUG) {
9
83
  console.log(`${LOG_PREFIX} ${message}`, ...args);
@@ -130,57 +204,15 @@ const withRecordingPermission = (config, props) => {
130
204
  permissionsToAdd.forEach((permission) => {
131
205
  config_plugins_1.AndroidConfig.Permissions.addPermission(config.modResults, permission);
132
206
  });
207
+ config_plugins_1.AndroidConfig.Manifest.ensureToolsAvailable(config.modResults);
133
208
  // Get the main application node
134
209
  const mainApplication = config.modResults.manifest.application?.[0];
135
210
  if (mainApplication) {
136
211
  debugLog('📱 Configuring Android application components...');
137
- // Add RecordingActionReceiver
138
- if (!mainApplication.receiver) {
139
- mainApplication.receiver = [];
140
- }
141
- const receiverConfig = {
142
- $: {
143
- 'android:name': '.RecordingActionReceiver',
144
- 'android:exported': 'false',
145
- },
146
- 'intent-filter': [
147
- {
148
- action: [
149
- { $: { 'android:name': 'PAUSE_RECORDING' } },
150
- { $: { 'android:name': 'RESUME_RECORDING' } },
151
- { $: { 'android:name': 'STOP_RECORDING' } },
152
- ],
153
- },
154
- ],
155
- };
156
- const receiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$?.['android:name'] === '.RecordingActionReceiver');
157
- if (receiverIndex >= 0) {
158
- mainApplication.receiver[receiverIndex] = receiverConfig;
159
- }
160
- else {
161
- mainApplication.receiver.push(receiverConfig);
162
- }
163
- debugLog('✅ RecordingActionReceiver configured');
164
- // Add AudioRecordingService
165
- if (!mainApplication.service) {
166
- mainApplication.service = [];
167
- }
168
- const serviceConfig = {
169
- $: {
170
- 'android:name': '.AudioRecordingService',
171
- 'android:enabled': 'true',
172
- 'android:exported': 'false',
173
- 'android:foregroundServiceType': 'microphone',
174
- },
175
- };
176
- const serviceIndex = mainApplication.service.findIndex((service) => service.$?.['android:name'] === '.AudioRecordingService');
177
- if (serviceIndex >= 0) {
178
- mainApplication.service[serviceIndex] = serviceConfig;
179
- }
180
- else {
181
- mainApplication.service.push(serviceConfig);
182
- }
183
- debugLog('✅ AudioRecordingService configured');
212
+ configureAndroidBackgroundRecordingComponents(mainApplication, enableBackgroundAudio);
213
+ debugLog(enableBackgroundAudio
214
+ ? '✅ Android background recording components configured'
215
+ : '✅ Android background recording components disabled');
184
216
  }
185
217
  else {
186
218
  console.error(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`);
@@ -1,4 +1,19 @@
1
1
  import { ConfigPlugin } from '@expo/config-plugins';
2
+ type AndroidComponent = {
3
+ $?: Record<string, string | boolean>;
4
+ [key: string]: unknown;
5
+ };
6
+ type AndroidApplication = {
7
+ receiver?: AndroidComponent[];
8
+ service?: AndroidComponent[];
9
+ [key: string]: unknown;
10
+ };
11
+ declare function configureAndroidBackgroundRecordingComponents(mainApplication: AndroidApplication, enableBackgroundAudio: boolean | undefined): void;
12
+ export declare const __testing: {
13
+ AUDIO_RECORDING_SERVICE: string;
14
+ RECORDING_ACTION_RECEIVER: string;
15
+ configureAndroidBackgroundRecordingComponents: typeof configureAndroidBackgroundRecordingComponents;
16
+ };
2
17
  interface AudioStreamPluginOptions {
3
18
  enablePhoneStateHandling?: boolean;
4
19
  enableNotifications?: boolean;
@@ -1,9 +1,83 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__testing = void 0;
3
4
  const config_plugins_1 = require("@expo/config-plugins");
4
5
  const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
5
6
  const NOTIFICATION_USAGE = 'Show recording notifications and controls';
6
7
  const LOG_PREFIX = '[@siteed/expo-audio-studio]';
8
+ const AUDIO_STUDIO_ANDROID_PACKAGE = 'net.siteed.audiostudio';
9
+ const RECORDING_ACTION_RECEIVER = `${AUDIO_STUDIO_ANDROID_PACKAGE}.RecordingActionReceiver`;
10
+ const AUDIO_RECORDING_SERVICE = `${AUDIO_STUDIO_ANDROID_PACKAGE}.AudioRecordingService`;
11
+ const LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER = '.RecordingActionReceiver';
12
+ const LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE = '.AudioRecordingService';
13
+ function removeComponentsByName(components, names) {
14
+ if (!components) {
15
+ return components;
16
+ }
17
+ const filtered = components.filter((component) => !names.includes(String(component.$?.['android:name'])));
18
+ return filtered.length > 0 ? filtered : undefined;
19
+ }
20
+ function upsertComponent(components, componentConfig) {
21
+ const name = String(componentConfig.$?.['android:name']);
22
+ const nextComponents = (components || []).filter((component) => String(component.$?.['android:name']) !== name);
23
+ nextComponents.push(componentConfig);
24
+ return nextComponents;
25
+ }
26
+ function addAndroidRemovalMarker(components, componentName) {
27
+ return upsertComponent(components, {
28
+ $: {
29
+ 'android:name': componentName,
30
+ 'tools:node': 'remove',
31
+ },
32
+ });
33
+ }
34
+ function configureAndroidBackgroundRecordingComponents(mainApplication, enableBackgroundAudio) {
35
+ const receiverNames = [
36
+ LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER,
37
+ RECORDING_ACTION_RECEIVER,
38
+ ];
39
+ const serviceNames = [
40
+ LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE,
41
+ AUDIO_RECORDING_SERVICE,
42
+ ];
43
+ mainApplication.receiver = removeComponentsByName(mainApplication.receiver, receiverNames);
44
+ mainApplication.service = removeComponentsByName(mainApplication.service, serviceNames);
45
+ if (enableBackgroundAudio) {
46
+ const receiverConfig = {
47
+ $: {
48
+ 'android:name': RECORDING_ACTION_RECEIVER,
49
+ 'android:exported': 'false',
50
+ },
51
+ 'intent-filter': [
52
+ {
53
+ action: [
54
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
55
+ { $: { 'android:name': 'RESUME_RECORDING' } },
56
+ { $: { 'android:name': 'STOP_RECORDING' } },
57
+ ],
58
+ },
59
+ ],
60
+ };
61
+ const serviceConfig = {
62
+ $: {
63
+ 'android:name': AUDIO_RECORDING_SERVICE,
64
+ 'android:enabled': 'true',
65
+ 'android:exported': 'false',
66
+ 'android:foregroundServiceType': 'microphone',
67
+ },
68
+ };
69
+ mainApplication.receiver = upsertComponent(mainApplication.receiver, receiverConfig);
70
+ mainApplication.service = upsertComponent(mainApplication.service, serviceConfig);
71
+ return;
72
+ }
73
+ mainApplication.receiver = addAndroidRemovalMarker(mainApplication.receiver, RECORDING_ACTION_RECEIVER);
74
+ mainApplication.service = addAndroidRemovalMarker(mainApplication.service, AUDIO_RECORDING_SERVICE);
75
+ }
76
+ exports.__testing = {
77
+ AUDIO_RECORDING_SERVICE,
78
+ RECORDING_ACTION_RECEIVER,
79
+ configureAndroidBackgroundRecordingComponents,
80
+ };
7
81
  function debugLog(message, ...args) {
8
82
  if (process.env.EXPO_DEBUG) {
9
83
  console.log(`${LOG_PREFIX} ${message}`, ...args);
@@ -130,57 +204,15 @@ const withRecordingPermission = (config, props) => {
130
204
  permissionsToAdd.forEach((permission) => {
131
205
  config_plugins_1.AndroidConfig.Permissions.addPermission(config.modResults, permission);
132
206
  });
207
+ config_plugins_1.AndroidConfig.Manifest.ensureToolsAvailable(config.modResults);
133
208
  // Get the main application node
134
209
  const mainApplication = config.modResults.manifest.application?.[0];
135
210
  if (mainApplication) {
136
211
  debugLog('📱 Configuring Android application components...');
137
- // Add RecordingActionReceiver
138
- if (!mainApplication.receiver) {
139
- mainApplication.receiver = [];
140
- }
141
- const receiverConfig = {
142
- $: {
143
- 'android:name': '.RecordingActionReceiver',
144
- 'android:exported': 'false',
145
- },
146
- 'intent-filter': [
147
- {
148
- action: [
149
- { $: { 'android:name': 'PAUSE_RECORDING' } },
150
- { $: { 'android:name': 'RESUME_RECORDING' } },
151
- { $: { 'android:name': 'STOP_RECORDING' } },
152
- ],
153
- },
154
- ],
155
- };
156
- const receiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$?.['android:name'] === '.RecordingActionReceiver');
157
- if (receiverIndex >= 0) {
158
- mainApplication.receiver[receiverIndex] = receiverConfig;
159
- }
160
- else {
161
- mainApplication.receiver.push(receiverConfig);
162
- }
163
- debugLog('✅ RecordingActionReceiver configured');
164
- // Add AudioRecordingService
165
- if (!mainApplication.service) {
166
- mainApplication.service = [];
167
- }
168
- const serviceConfig = {
169
- $: {
170
- 'android:name': '.AudioRecordingService',
171
- 'android:enabled': 'true',
172
- 'android:exported': 'false',
173
- 'android:foregroundServiceType': 'microphone',
174
- },
175
- };
176
- const serviceIndex = mainApplication.service.findIndex((service) => service.$?.['android:name'] === '.AudioRecordingService');
177
- if (serviceIndex >= 0) {
178
- mainApplication.service[serviceIndex] = serviceConfig;
179
- }
180
- else {
181
- mainApplication.service.push(serviceConfig);
182
- }
183
- debugLog('✅ AudioRecordingService configured');
212
+ configureAndroidBackgroundRecordingComponents(mainApplication, enableBackgroundAudio);
213
+ debugLog(enableBackgroundAudio
214
+ ? '✅ Android background recording components configured'
215
+ : '✅ Android background recording components disabled');
184
216
  }
185
217
  else {
186
218
  console.error(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`);
@@ -0,0 +1,78 @@
1
+ /// <reference types="jest" />
2
+
3
+ import { __testing } from './index'
4
+
5
+ describe('audio-studio Expo plugin Android background recording components', () => {
6
+ it('adds fully-qualified foreground service components when background audio is enabled', () => {
7
+ const application: any = {
8
+ receiver: [{ $: { 'android:name': '.RecordingActionReceiver' } }],
9
+ service: [{ $: { 'android:name': '.AudioRecordingService' } }],
10
+ }
11
+
12
+ __testing.configureAndroidBackgroundRecordingComponents(
13
+ application,
14
+ true
15
+ )
16
+
17
+ expect(application.receiver).toHaveLength(1)
18
+ expect(application.receiver[0].$['android:name']).toBe(
19
+ __testing.RECORDING_ACTION_RECEIVER
20
+ )
21
+ expect(application.receiver[0].$['android:exported']).toBe('false')
22
+ expect(application.receiver[0]['intent-filter'][0].action).toHaveLength(
23
+ 3
24
+ )
25
+
26
+ expect(application.service).toHaveLength(1)
27
+ expect(application.service[0].$['android:name']).toBe(
28
+ __testing.AUDIO_RECORDING_SERVICE
29
+ )
30
+ expect(application.service[0].$['android:foregroundServiceType']).toBe(
31
+ 'microphone'
32
+ )
33
+ })
34
+
35
+ it('removes foreground service components when background audio is disabled', () => {
36
+ const application: any = {
37
+ receiver: [
38
+ { $: { 'android:name': '.RecordingActionReceiver' } },
39
+ {
40
+ $: {
41
+ 'android:name': __testing.RECORDING_ACTION_RECEIVER,
42
+ },
43
+ },
44
+ ],
45
+ service: [
46
+ { $: { 'android:name': '.AudioRecordingService' } },
47
+ {
48
+ $: {
49
+ 'android:name': __testing.AUDIO_RECORDING_SERVICE,
50
+ 'android:foregroundServiceType': 'microphone',
51
+ },
52
+ },
53
+ ],
54
+ }
55
+
56
+ __testing.configureAndroidBackgroundRecordingComponents(
57
+ application,
58
+ false
59
+ )
60
+
61
+ expect(application.receiver).toEqual([
62
+ {
63
+ $: {
64
+ 'android:name': __testing.RECORDING_ACTION_RECEIVER,
65
+ 'tools:node': 'remove',
66
+ },
67
+ },
68
+ ])
69
+ expect(application.service).toEqual([
70
+ {
71
+ $: {
72
+ 'android:name': __testing.AUDIO_RECORDING_SERVICE,
73
+ 'tools:node': 'remove',
74
+ },
75
+ },
76
+ ])
77
+ })
78
+ })
@@ -10,6 +10,138 @@ const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'
10
10
  const NOTIFICATION_USAGE = 'Show recording notifications and controls'
11
11
  const LOG_PREFIX = '[@siteed/expo-audio-studio]'
12
12
 
13
+ const AUDIO_STUDIO_ANDROID_PACKAGE = 'net.siteed.audiostudio'
14
+ const RECORDING_ACTION_RECEIVER = `${AUDIO_STUDIO_ANDROID_PACKAGE}.RecordingActionReceiver`
15
+ const AUDIO_RECORDING_SERVICE = `${AUDIO_STUDIO_ANDROID_PACKAGE}.AudioRecordingService`
16
+ const LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER = '.RecordingActionReceiver'
17
+ const LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE = '.AudioRecordingService'
18
+
19
+ type AndroidComponent = {
20
+ $?: Record<string, string | boolean>
21
+ [key: string]: unknown
22
+ }
23
+
24
+ type AndroidApplication = {
25
+ receiver?: AndroidComponent[]
26
+ service?: AndroidComponent[]
27
+ [key: string]: unknown
28
+ }
29
+
30
+ function removeComponentsByName(
31
+ components: AndroidComponent[] | undefined,
32
+ names: string[]
33
+ ): AndroidComponent[] | undefined {
34
+ if (!components) {
35
+ return components
36
+ }
37
+
38
+ const filtered = components.filter(
39
+ (component) => !names.includes(String(component.$?.['android:name']))
40
+ )
41
+
42
+ return filtered.length > 0 ? filtered : undefined
43
+ }
44
+
45
+ function upsertComponent(
46
+ components: AndroidComponent[] | undefined,
47
+ componentConfig: AndroidComponent
48
+ ): AndroidComponent[] {
49
+ const name = String(componentConfig.$?.['android:name'])
50
+ const nextComponents = (components || []).filter(
51
+ (component) => String(component.$?.['android:name']) !== name
52
+ )
53
+
54
+ nextComponents.push(componentConfig)
55
+ return nextComponents
56
+ }
57
+
58
+ function addAndroidRemovalMarker(
59
+ components: AndroidComponent[] | undefined,
60
+ componentName: string
61
+ ): AndroidComponent[] {
62
+ return upsertComponent(components, {
63
+ $: {
64
+ 'android:name': componentName,
65
+ 'tools:node': 'remove',
66
+ },
67
+ })
68
+ }
69
+
70
+ function configureAndroidBackgroundRecordingComponents(
71
+ mainApplication: AndroidApplication,
72
+ enableBackgroundAudio: boolean | undefined
73
+ ): void {
74
+ const receiverNames = [
75
+ LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER,
76
+ RECORDING_ACTION_RECEIVER,
77
+ ]
78
+ const serviceNames = [
79
+ LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE,
80
+ AUDIO_RECORDING_SERVICE,
81
+ ]
82
+
83
+ mainApplication.receiver = removeComponentsByName(
84
+ mainApplication.receiver,
85
+ receiverNames
86
+ )
87
+ mainApplication.service = removeComponentsByName(
88
+ mainApplication.service,
89
+ serviceNames
90
+ )
91
+
92
+ if (enableBackgroundAudio) {
93
+ const receiverConfig = {
94
+ $: {
95
+ 'android:name': RECORDING_ACTION_RECEIVER,
96
+ 'android:exported': 'false' as const,
97
+ },
98
+ 'intent-filter': [
99
+ {
100
+ action: [
101
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
102
+ { $: { 'android:name': 'RESUME_RECORDING' } },
103
+ { $: { 'android:name': 'STOP_RECORDING' } },
104
+ ],
105
+ },
106
+ ],
107
+ }
108
+
109
+ const serviceConfig = {
110
+ $: {
111
+ 'android:name': AUDIO_RECORDING_SERVICE,
112
+ 'android:enabled': 'true' as const,
113
+ 'android:exported': 'false' as const,
114
+ 'android:foregroundServiceType': 'microphone',
115
+ },
116
+ }
117
+
118
+ mainApplication.receiver = upsertComponent(
119
+ mainApplication.receiver,
120
+ receiverConfig
121
+ )
122
+ mainApplication.service = upsertComponent(
123
+ mainApplication.service,
124
+ serviceConfig
125
+ )
126
+ return
127
+ }
128
+
129
+ mainApplication.receiver = addAndroidRemovalMarker(
130
+ mainApplication.receiver,
131
+ RECORDING_ACTION_RECEIVER
132
+ )
133
+ mainApplication.service = addAndroidRemovalMarker(
134
+ mainApplication.service,
135
+ AUDIO_RECORDING_SERVICE
136
+ )
137
+ }
138
+
139
+ export const __testing = {
140
+ AUDIO_RECORDING_SERVICE,
141
+ RECORDING_ACTION_RECEIVER,
142
+ configureAndroidBackgroundRecordingComponents,
143
+ }
144
+
13
145
  function debugLog(message: string, ...args: unknown[]): void {
14
146
  if (process.env.EXPO_DEBUG) {
15
147
  console.log(`${LOG_PREFIX} ${message}`, ...args)
@@ -203,71 +335,21 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
203
335
  )
204
336
  })
205
337
 
338
+ AndroidConfig.Manifest.ensureToolsAvailable(config.modResults)
339
+
206
340
  // Get the main application node
207
341
  const mainApplication = config.modResults.manifest.application?.[0]
208
342
  if (mainApplication) {
209
343
  debugLog('📱 Configuring Android application components...')
210
-
211
- // Add RecordingActionReceiver
212
- if (!mainApplication.receiver) {
213
- mainApplication.receiver = []
214
- }
215
-
216
- const receiverConfig = {
217
- $: {
218
- 'android:name': '.RecordingActionReceiver',
219
- 'android:exported': 'false' as const,
220
- },
221
- 'intent-filter': [
222
- {
223
- action: [
224
- { $: { 'android:name': 'PAUSE_RECORDING' } },
225
- { $: { 'android:name': 'RESUME_RECORDING' } },
226
- { $: { 'android:name': 'STOP_RECORDING' } },
227
- ],
228
- },
229
- ],
230
- }
231
-
232
- const receiverIndex = mainApplication.receiver.findIndex(
233
- (receiver: any) =>
234
- receiver.$?.['android:name'] === '.RecordingActionReceiver'
344
+ configureAndroidBackgroundRecordingComponents(
345
+ mainApplication,
346
+ enableBackgroundAudio
235
347
  )
236
-
237
- if (receiverIndex >= 0) {
238
- mainApplication.receiver[receiverIndex] = receiverConfig
239
- } else {
240
- mainApplication.receiver.push(receiverConfig)
241
- }
242
-
243
- debugLog('✅ RecordingActionReceiver configured')
244
-
245
- // Add AudioRecordingService
246
- if (!mainApplication.service) {
247
- mainApplication.service = []
248
- }
249
-
250
- const serviceConfig = {
251
- $: {
252
- 'android:name': '.AudioRecordingService',
253
- 'android:enabled': 'true' as const,
254
- 'android:exported': 'false' as const,
255
- 'android:foregroundServiceType': 'microphone',
256
- },
257
- }
258
-
259
- const serviceIndex = mainApplication.service.findIndex(
260
- (service: any) =>
261
- service.$?.['android:name'] === '.AudioRecordingService'
348
+ debugLog(
349
+ enableBackgroundAudio
350
+ ? '✅ Android background recording components configured'
351
+ : '✅ Android background recording components disabled'
262
352
  )
263
-
264
- if (serviceIndex >= 0) {
265
- mainApplication.service[serviceIndex] = serviceConfig
266
- } else {
267
- mainApplication.service.push(serviceConfig)
268
- }
269
-
270
- debugLog('✅ AudioRecordingService configured')
271
353
  } else {
272
354
  console.error(
273
355
  `${LOG_PREFIX} ❌ Main application node not found in Android Manifest`
@@ -12,5 +12,10 @@
12
12
  }
13
13
  },
14
14
  "include": ["./src"],
15
- "exclude": ["**/__mocks__/*", "**/__tests__/*"]
15
+ "exclude": [
16
+ "**/__mocks__/*",
17
+ "**/__tests__/*",
18
+ "**/*.test.ts",
19
+ "**/*.spec.ts"
20
+ ]
16
21
  }