diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 7dcce9f..2583e1b 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -8,6 +8,8 @@ import { Alert, TextInput, Modal, + KeyboardAvoidingView, + Platform, } from 'react-native'; import { router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; @@ -223,7 +225,10 @@ export default function ProfileScreen() { animationType="fade" onRequestClose={() => setShowDeploymentModal(false)} > - + Deployment ID @@ -264,7 +269,7 @@ export default function ProfileScreen() { - + ); diff --git a/utils/audioSession.ts b/utils/audioSession.ts index efec804..e566f7f 100644 --- a/utils/audioSession.ts +++ b/utils/audioSession.ts @@ -50,102 +50,93 @@ export async function configureAudioForVoiceCall(): Promise { } if (Platform.OS === 'ios') { - // iOS-specific configuration with fallback strategies - // Try multiple configurations in order of preference + // iOS-specific configuration - FORCE SPEAKER OUTPUT + // Using videoChat mode + defaultSpeakerOutput option for guaranteed speaker + console.log('[AudioSession] Configuring iOS for SPEAKER output...'); - const configs = [ - // Strategy 1: videoChat mode (speaker by default, no problematic options) - { - name: 'videoChat', - config: { - audioCategory: 'playAndRecord', - audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], - audioMode: 'videoChat', - }, - }, - // Strategy 2: voiceChat mode (more compatible, but earpiece by default) - { - name: 'voiceChat', - config: { - audioCategory: 'playAndRecord', - audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], - audioMode: 'voiceChat', - }, - }, - // 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 - } + try { + // Primary config: videoChat mode with defaultSpeakerOutput + await AudioSession.setAppleAudioConfiguration({ + audioCategory: 'playAndRecord', + audioCategoryOptions: [ + 'allowBluetooth', + 'mixWithOthers', + 'defaultToSpeaker', // KEY: Forces speaker as default output + ], + audioMode: 'videoChat', // videoChat mode uses speaker by default + }); + console.log('[AudioSession] iOS videoChat + defaultToSpeaker configured!'); + } catch (err) { + console.warn('[AudioSession] Primary iOS config failed, trying fallback:', err); + // Fallback: just videoChat without defaultToSpeaker option + await AudioSession.setAppleAudioConfiguration({ + audioCategory: 'playAndRecord', + audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], + audioMode: 'videoChat', + }); } - if (!configSuccess) { - console.error('[AudioSession] All iOS configurations failed!'); - throw lastError || new Error('All audio configurations failed'); - } - - console.log('[AudioSession] Starting audio session...'); + console.log('[AudioSession] Starting iOS audio session...'); 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 { - console.log('[AudioSession] Setting default output to speaker...'); + console.log('[AudioSession] Setting iOS default output to speaker...'); await AudioSession.configureAudio({ ios: { defaultOutput: 'speaker', }, }); + console.log('[AudioSession] iOS speaker output set!'); } catch (outputErr) { console.warn('[AudioSession] Could not set speaker output:', outputErr); - // Continue anyway - audio will work, just maybe on earpiece } } else if (Platform.OS === 'android') { - // Android-specific configuration - // IMPORTANT: Using 'music' stream type to force output to speaker - // 'voiceCall' stream type defaults to earpiece on many Android devices - console.log('[AudioSession] Configuring Android audio for SPEAKER...'); + // Android-specific configuration - FORCE SPEAKER OUTPUT + // SOLUTION: Use 'inCommunication' for echo cancellation + forceHandleAudioRouting + explicit speaker selection + console.log('[AudioSession] Configuring Android audio for SPEAKER with echo cancellation...'); + await AudioSession.configureAudio({ 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: { manageAudioFocus: true, - audioMode: 'normal', + // Use 'inCommunication' for echo cancellation (important for voice calls!) + audioMode: 'inCommunication', audioFocusMode: 'gain', - // Use 'music' stream - goes to speaker by default - audioStreamType: 'music', - audioAttributesUsageType: 'media', - audioAttributesContentType: 'music', + // Voice call stream type for proper routing + audioStreamType: 'voiceCall', + audioAttributesUsageType: 'voiceCommunication', + audioAttributesContentType: 'speech', }, - // Force speaker as output - preferredOutputList: ['speaker'], - // Allow us to control audio routing - forceHandleAudioRouting: true, }, }); console.log('[AudioSession] Starting Android audio session...'); 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!'); @@ -191,7 +182,7 @@ export async function reconfigureAudioForPlayback(): Promise { return; } - console.log(`[AudioSession] Reconfiguring for playback on ${Platform.OS}...`); + console.log(`[AudioSession] Reconfiguring for playback (SPEAKER) on ${Platform.OS}...`); try { const AudioSession = await getAudioSession(); @@ -200,33 +191,50 @@ export async function reconfigureAudioForPlayback(): Promise { } if (Platform.OS === 'ios') { - // Reconfigure with same safe settings - this "refreshes" the audio routing + // Reconfigure iOS - force speaker output await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', audioCategoryOptions: [ 'allowBluetooth', 'mixWithOthers', + 'defaultToSpeaker', // Force speaker ], - // Use 'videoChat' - defaults to speaker - audioMode: 'videoChat', + audioMode: 'videoChat', // videoChat = speaker by default }); - } else if (Platform.OS === 'android') { - // Reconfigure Android audio to ensure speaker output - // Using 'music' stream type to force speaker + + // Also set default output to speaker await AudioSession.configureAudio({ - android: { - audioTypeOptions: { - manageAudioFocus: true, - audioMode: 'normal', - audioFocusMode: 'gain', - audioStreamType: 'music', - audioAttributesUsageType: 'media', - audioAttributesContentType: 'music', - }, - preferredOutputList: ['speaker'], - forceHandleAudioRouting: true, + ios: { + defaultOutput: 'speaker', }, }); + 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'); @@ -252,11 +260,12 @@ export async function setAudioOutput(useSpeaker: boolean): Promise { } if (Platform.OS === 'ios') { - // iOS: Update configuration based on desired output - // Use 'videoChat' mode for speaker, 'voiceChat' for earpiece + // iOS: Use videoChat mode + defaultToSpeaker for speaker, voiceChat for earpiece await AudioSession.setAppleAudioConfiguration({ audioCategory: 'playAndRecord', - audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], + audioCategoryOptions: useSpeaker + ? ['allowBluetooth', 'mixWithOthers', 'defaultToSpeaker'] + : ['allowBluetooth', 'mixWithOthers'], audioMode: useSpeaker ? 'videoChat' : 'voiceChat', }); @@ -267,25 +276,29 @@ export async function setAudioOutput(useSpeaker: boolean): Promise { }, }); } else if (Platform.OS === 'android') { - // Android: Switch stream type to control speaker/earpiece - // - 'music' stream goes to speaker by default - // - 'voiceCall' stream goes to earpiece by default + // Android: Keep inCommunication mode for echo cancellation, use explicit output selection await AudioSession.configureAudio({ 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'], 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'}`);