@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 +11 -1
- package/package.json +2 -1
- package/plugin/build/index.cjs +79 -47
- package/plugin/build/index.d.cts +15 -0
- package/plugin/build/index.js +79 -47
- package/plugin/src/index.test.ts +78 -0
- package/plugin/src/index.ts +141 -59
- package/plugin/tsconfig.json +6 -1
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.
|
|
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.
|
|
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",
|
package/plugin/build/index.cjs
CHANGED
|
@@ -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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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`);
|
package/plugin/build/index.d.cts
CHANGED
|
@@ -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;
|
package/plugin/build/index.js
CHANGED
|
@@ -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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
})
|
package/plugin/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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`
|