import UIKit import Flutter import AVFoundation @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Configure the iOS AVAudioSession for voice chat BEFORE Flutter // starts the audio engine. The category + mode combination tells // iOS to: // * route via the receiver/speaker like a phone call // (`.playAndRecord` + `.voiceChat`) // * engage hardware AEC / NS where the device supports it // * default the speaker output (so the user doesn't have to // hold the phone to their ear) // * permit Bluetooth headsets (so AirPods et al. just work) // // SRS-197 covers the iOS audio routing contract; this is the // matching iOS-side implementation. Failures are logged but do // not block app launch — the audio engine will still come up, // just at the iOS default playback route. do { let session = AVAudioSession.sharedInstance() try session.setCategory( .playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP] ) try session.setActive(true, options: []) NSLog("chanora_flutter: AVAudioSession configured (playAndRecord/voiceChat)") } catch { NSLog("chanora_flutter: AVAudioSession setup failed: \(error)") } // Request microphone access on first launch rather than waiting // for the user's first voice-channel join. The latter is // surprising: the user has only tapped "connect to server" and // suddenly iOS pops the permission prompt because joining a // text channel happens to trigger audio engine startup. Asking // up-front matches user expectations for a voice-chat client. // // The request is asynchronous and non-blocking. If the user // denies, voice_join will surface a clearer error later when // the audio engine fails to open the input device. The // permission state is cached by iOS so subsequent launches // skip the prompt. // // Deferred ~1 s so iOS finishes initialising the keyboard / // text-input subsystem before the permission alert appears. // Firing the alert too early steals focus from the not-yet- // ready text-input layer, with the symptom that the first tap // on a TextField does nothing (the second tap works because // by then iOS has caught up). DispatchQueue.main.asyncAfter // keeps everything on the main thread; the permission API // itself must be called there too. DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { AVAudioSession.sharedInstance().requestRecordPermission { granted in NSLog("chanora_flutter: microphone permission granted=\(granted)") } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } }