Fix Android speaker output + keyboard-aware modal
Android Audio:
- Use inCommunication mode with forceHandleAudioRouting
- Explicit selectAudioOutput('speaker') after session start
- Keeps echo cancellation while forcing speaker output
Profile Modal:
- Add KeyboardAvoidingView for deployment ID input
- Prevents modal buttons from being hidden by keyboard
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
85896f442f
commit
8240e51bc5
@ -8,6 +8,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
TextInput,
|
TextInput,
|
||||||
Modal,
|
Modal,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
@ -223,7 +225,10 @@ export default function ProfileScreen() {
|
|||||||
animationType="fade"
|
animationType="fade"
|
||||||
onRequestClose={() => setShowDeploymentModal(false)}
|
onRequestClose={() => setShowDeploymentModal(false)}
|
||||||
>
|
>
|
||||||
<View style={styles.modalOverlay}>
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.modalOverlay}
|
||||||
|
>
|
||||||
<View style={styles.modalContent}>
|
<View style={styles.modalContent}>
|
||||||
<Text style={styles.modalTitle}>Deployment ID</Text>
|
<Text style={styles.modalTitle}>Deployment ID</Text>
|
||||||
<Text style={styles.modalDescription}>
|
<Text style={styles.modalDescription}>
|
||||||
@ -264,7 +269,7 @@ export default function ProfileScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -50,102 +50,93 @@ export async function configureAudioForVoiceCall(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// iOS-specific configuration with fallback strategies
|
// iOS-specific configuration - FORCE SPEAKER OUTPUT
|
||||||
// Try multiple configurations in order of preference
|
// Using videoChat mode + defaultSpeakerOutput option for guaranteed speaker
|
||||||
|
console.log('[AudioSession] Configuring iOS for SPEAKER output...');
|
||||||
|
|
||||||
const configs = [
|
try {
|
||||||
// Strategy 1: videoChat mode (speaker by default, no problematic options)
|
// Primary config: videoChat mode with defaultSpeakerOutput
|
||||||
{
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
name: 'videoChat',
|
audioCategory: 'playAndRecord',
|
||||||
config: {
|
audioCategoryOptions: [
|
||||||
audioCategory: 'playAndRecord',
|
'allowBluetooth',
|
||||||
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
'mixWithOthers',
|
||||||
audioMode: 'videoChat',
|
'defaultToSpeaker', // KEY: Forces speaker as default output
|
||||||
},
|
],
|
||||||
},
|
audioMode: 'videoChat', // videoChat mode uses speaker by default
|
||||||
// Strategy 2: voiceChat mode (more compatible, but earpiece by default)
|
});
|
||||||
{
|
console.log('[AudioSession] iOS videoChat + defaultToSpeaker configured!');
|
||||||
name: 'voiceChat',
|
} catch (err) {
|
||||||
config: {
|
console.warn('[AudioSession] Primary iOS config failed, trying fallback:', err);
|
||||||
audioCategory: 'playAndRecord',
|
// Fallback: just videoChat without defaultToSpeaker option
|
||||||
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
audioMode: 'voiceChat',
|
audioCategory: 'playAndRecord',
|
||||||
},
|
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
||||||
},
|
audioMode: 'videoChat',
|
||||||
// Strategy 3: Minimal config (most compatible)
|
});
|
||||||
{
|
|
||||||
name: 'minimal',
|
|
||||||
config: {
|
|
||||||
audioCategory: 'playAndRecord',
|
|
||||||
audioCategoryOptions: [],
|
|
||||||
audioMode: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let configSuccess = false;
|
|
||||||
let lastError: any = null;
|
|
||||||
|
|
||||||
for (const { name, config } of configs) {
|
|
||||||
try {
|
|
||||||
console.log(`[AudioSession] Trying ${name} configuration...`);
|
|
||||||
await AudioSession.setAppleAudioConfiguration(config);
|
|
||||||
console.log(`[AudioSession] ${name} configuration succeeded!`);
|
|
||||||
configSuccess = true;
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`[AudioSession] ${name} config failed:`, err);
|
|
||||||
lastError = err;
|
|
||||||
// Continue to next strategy
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configSuccess) {
|
console.log('[AudioSession] Starting iOS audio session...');
|
||||||
console.error('[AudioSession] All iOS configurations failed!');
|
|
||||||
throw lastError || new Error('All audio configurations failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[AudioSession] Starting audio session...');
|
|
||||||
await AudioSession.startAudioSession();
|
await AudioSession.startAudioSession();
|
||||||
|
|
||||||
// Try to set speaker output (non-critical, don't throw on failure)
|
// Additionally set default output to speaker (belt and suspenders)
|
||||||
try {
|
try {
|
||||||
console.log('[AudioSession] Setting default output to speaker...');
|
console.log('[AudioSession] Setting iOS default output to speaker...');
|
||||||
await AudioSession.configureAudio({
|
await AudioSession.configureAudio({
|
||||||
ios: {
|
ios: {
|
||||||
defaultOutput: 'speaker',
|
defaultOutput: 'speaker',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log('[AudioSession] iOS speaker output set!');
|
||||||
} catch (outputErr) {
|
} catch (outputErr) {
|
||||||
console.warn('[AudioSession] Could not set speaker output:', outputErr);
|
console.warn('[AudioSession] Could not set speaker output:', outputErr);
|
||||||
// Continue anyway - audio will work, just maybe on earpiece
|
|
||||||
}
|
}
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
// Android-specific configuration
|
// Android-specific configuration - FORCE SPEAKER OUTPUT
|
||||||
// IMPORTANT: Using 'music' stream type to force output to speaker
|
// SOLUTION: Use 'inCommunication' for echo cancellation + forceHandleAudioRouting + explicit speaker selection
|
||||||
// 'voiceCall' stream type defaults to earpiece on many Android devices
|
console.log('[AudioSession] Configuring Android audio for SPEAKER with echo cancellation...');
|
||||||
console.log('[AudioSession] Configuring Android audio for SPEAKER...');
|
|
||||||
await AudioSession.configureAudio({
|
await AudioSession.configureAudio({
|
||||||
android: {
|
android: {
|
||||||
// Use MEDIA mode to ensure speaker output
|
// Force speaker as preferred output
|
||||||
|
preferredOutputList: ['speaker'],
|
||||||
|
// CRITICAL: This flag forces audio routing even in communication mode
|
||||||
|
forceHandleAudioRouting: true,
|
||||||
audioTypeOptions: {
|
audioTypeOptions: {
|
||||||
manageAudioFocus: true,
|
manageAudioFocus: true,
|
||||||
audioMode: 'normal',
|
// Use 'inCommunication' for echo cancellation (important for voice calls!)
|
||||||
|
audioMode: 'inCommunication',
|
||||||
audioFocusMode: 'gain',
|
audioFocusMode: 'gain',
|
||||||
// Use 'music' stream - goes to speaker by default
|
// Voice call stream type for proper routing
|
||||||
audioStreamType: 'music',
|
audioStreamType: 'voiceCall',
|
||||||
audioAttributesUsageType: 'media',
|
audioAttributesUsageType: 'voiceCommunication',
|
||||||
audioAttributesContentType: 'music',
|
audioAttributesContentType: 'speech',
|
||||||
},
|
},
|
||||||
// Force speaker as output
|
|
||||||
preferredOutputList: ['speaker'],
|
|
||||||
// Allow us to control audio routing
|
|
||||||
forceHandleAudioRouting: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[AudioSession] Starting Android audio session...');
|
console.log('[AudioSession] Starting Android audio session...');
|
||||||
await AudioSession.startAudioSession();
|
await AudioSession.startAudioSession();
|
||||||
|
|
||||||
|
// CRITICAL: Explicitly select speaker AFTER session starts
|
||||||
|
// This overrides the default earpiece routing of inCommunication mode
|
||||||
|
try {
|
||||||
|
console.log('[AudioSession] Explicitly selecting speaker output...');
|
||||||
|
await AudioSession.selectAudioOutput('speaker');
|
||||||
|
console.log('[AudioSession] Speaker output explicitly selected!');
|
||||||
|
} catch (speakerErr) {
|
||||||
|
console.warn('[AudioSession] selectAudioOutput failed, trying showAudioRoutePicker:', speakerErr);
|
||||||
|
// Fallback: try to show audio route picker or use alternative method
|
||||||
|
try {
|
||||||
|
if (AudioSession.showAudioRoutePicker) {
|
||||||
|
await AudioSession.showAudioRoutePicker();
|
||||||
|
}
|
||||||
|
} catch (pickerErr) {
|
||||||
|
console.warn('[AudioSession] showAudioRoutePicker also failed:', pickerErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[AudioSession] Android speaker mode with echo cancellation configured!');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AudioSession] Configuration complete!');
|
console.log('[AudioSession] Configuration complete!');
|
||||||
@ -191,7 +182,7 @@ export async function reconfigureAudioForPlayback(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[AudioSession] Reconfiguring for playback on ${Platform.OS}...`);
|
console.log(`[AudioSession] Reconfiguring for playback (SPEAKER) on ${Platform.OS}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const AudioSession = await getAudioSession();
|
const AudioSession = await getAudioSession();
|
||||||
@ -200,33 +191,50 @@ export async function reconfigureAudioForPlayback(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// Reconfigure with same safe settings - this "refreshes" the audio routing
|
// Reconfigure iOS - force speaker output
|
||||||
await AudioSession.setAppleAudioConfiguration({
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
audioCategory: 'playAndRecord',
|
audioCategory: 'playAndRecord',
|
||||||
audioCategoryOptions: [
|
audioCategoryOptions: [
|
||||||
'allowBluetooth',
|
'allowBluetooth',
|
||||||
'mixWithOthers',
|
'mixWithOthers',
|
||||||
|
'defaultToSpeaker', // Force speaker
|
||||||
],
|
],
|
||||||
// Use 'videoChat' - defaults to speaker
|
audioMode: 'videoChat', // videoChat = speaker by default
|
||||||
audioMode: 'videoChat',
|
|
||||||
});
|
});
|
||||||
} else if (Platform.OS === 'android') {
|
|
||||||
// Reconfigure Android audio to ensure speaker output
|
// Also set default output to speaker
|
||||||
// Using 'music' stream type to force speaker
|
|
||||||
await AudioSession.configureAudio({
|
await AudioSession.configureAudio({
|
||||||
android: {
|
ios: {
|
||||||
audioTypeOptions: {
|
defaultOutput: 'speaker',
|
||||||
manageAudioFocus: true,
|
|
||||||
audioMode: 'normal',
|
|
||||||
audioFocusMode: 'gain',
|
|
||||||
audioStreamType: 'music',
|
|
||||||
audioAttributesUsageType: 'media',
|
|
||||||
audioAttributesContentType: 'music',
|
|
||||||
},
|
|
||||||
preferredOutputList: ['speaker'],
|
|
||||||
forceHandleAudioRouting: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log('[AudioSession] iOS reconfigured for speaker playback');
|
||||||
|
} else if (Platform.OS === 'android') {
|
||||||
|
// Reconfigure Android - force speaker while keeping echo cancellation
|
||||||
|
await AudioSession.configureAudio({
|
||||||
|
android: {
|
||||||
|
preferredOutputList: ['speaker'],
|
||||||
|
forceHandleAudioRouting: true,
|
||||||
|
audioTypeOptions: {
|
||||||
|
manageAudioFocus: true,
|
||||||
|
audioMode: 'inCommunication', // Keep for echo cancellation
|
||||||
|
audioFocusMode: 'gain',
|
||||||
|
audioStreamType: 'voiceCall',
|
||||||
|
audioAttributesUsageType: 'voiceCommunication',
|
||||||
|
audioAttributesContentType: 'speech',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Explicitly select speaker output
|
||||||
|
try {
|
||||||
|
await AudioSession.selectAudioOutput('speaker');
|
||||||
|
console.log('[AudioSession] Android speaker explicitly selected');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[AudioSession] selectAudioOutput failed in reconfigure:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[AudioSession] Android reconfigured for speaker playback');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AudioSession] Reconfigured successfully');
|
console.log('[AudioSession] Reconfigured successfully');
|
||||||
@ -252,11 +260,12 @@ export async function setAudioOutput(useSpeaker: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// iOS: Update configuration based on desired output
|
// iOS: Use videoChat mode + defaultToSpeaker for speaker, voiceChat for earpiece
|
||||||
// Use 'videoChat' mode for speaker, 'voiceChat' for earpiece
|
|
||||||
await AudioSession.setAppleAudioConfiguration({
|
await AudioSession.setAppleAudioConfiguration({
|
||||||
audioCategory: 'playAndRecord',
|
audioCategory: 'playAndRecord',
|
||||||
audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'],
|
audioCategoryOptions: useSpeaker
|
||||||
|
? ['allowBluetooth', 'mixWithOthers', 'defaultToSpeaker']
|
||||||
|
: ['allowBluetooth', 'mixWithOthers'],
|
||||||
audioMode: useSpeaker ? 'videoChat' : 'voiceChat',
|
audioMode: useSpeaker ? 'videoChat' : 'voiceChat',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -267,25 +276,29 @@ export async function setAudioOutput(useSpeaker: boolean): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (Platform.OS === 'android') {
|
} else if (Platform.OS === 'android') {
|
||||||
// Android: Switch stream type to control speaker/earpiece
|
// Android: Keep inCommunication mode for echo cancellation, use explicit output selection
|
||||||
// - 'music' stream goes to speaker by default
|
|
||||||
// - 'voiceCall' stream goes to earpiece by default
|
|
||||||
await AudioSession.configureAudio({
|
await AudioSession.configureAudio({
|
||||||
android: {
|
android: {
|
||||||
audioTypeOptions: {
|
|
||||||
manageAudioFocus: true,
|
|
||||||
audioMode: useSpeaker ? 'normal' : 'inCommunication',
|
|
||||||
audioFocusMode: 'gain',
|
|
||||||
// Key difference: music→speaker, voiceCall→earpiece
|
|
||||||
audioStreamType: useSpeaker ? 'music' : 'voiceCall',
|
|
||||||
audioAttributesUsageType: useSpeaker ? 'media' : 'voiceCommunication',
|
|
||||||
audioAttributesContentType: useSpeaker ? 'music' : 'speech',
|
|
||||||
},
|
|
||||||
// Also set preferred output list
|
|
||||||
preferredOutputList: useSpeaker ? ['speaker'] : ['earpiece'],
|
preferredOutputList: useSpeaker ? ['speaker'] : ['earpiece'],
|
||||||
forceHandleAudioRouting: true,
|
forceHandleAudioRouting: true,
|
||||||
|
audioTypeOptions: {
|
||||||
|
manageAudioFocus: true,
|
||||||
|
// Always use inCommunication for echo cancellation
|
||||||
|
audioMode: 'inCommunication',
|
||||||
|
audioFocusMode: 'gain',
|
||||||
|
audioStreamType: 'voiceCall',
|
||||||
|
audioAttributesUsageType: 'voiceCommunication',
|
||||||
|
audioAttributesContentType: 'speech',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Explicitly select output device
|
||||||
|
try {
|
||||||
|
await AudioSession.selectAudioOutput(useSpeaker ? 'speaker' : 'earpiece');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[AudioSession] selectAudioOutput failed:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[AudioSession] Audio output set to ${useSpeaker ? 'SPEAKER' : 'EARPIECE'}`);
|
console.log(`[AudioSession] Audio output set to ${useSpeaker ? 'SPEAKER' : 'EARPIECE'}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user