Commit Graph
13 Commits
Author SHA1 Message Date
EdisonJwa b8e9c8549e feat(ui,ios,android): audio output route picker + collapse voice controls into single modal
Two user-reported issues addressed:

  1. 'on supported devices such as iphone or android user should be
     able to select audio device such as speaker or airpods or phone'
  2. 'the bottom folder is duplicated with the app bar settings'

Issue 1 \u2014 audio output route picker:

Added the audio_router 1.1.1 plugin (MIT, supports iOS + Android)
which renders the platform-native picker:

  * iOS: Apple AVRoutePickerView system sheet \u2014 the same UI as
    Control Center's audio chooser. Lists Speaker / iPhone receiver /
    AirPods / connected Bluetooth devices / AirPlay / CarPlay.
    System manages the device list; we don't have to track route
    changes manually.

  * Android (post-rc.8 when we wire the platform): Material Design 3
    dialog backed by AudioManager.setCommunicationDevice() with
    SCO Bluetooth + USB headsets filtered for VoIP.

The picker prerequisite documented by the plugin (audio session
must be playAndRecord/voiceChat before the picker fires) is already
satisfied by our AppDelegate.swift configuration from commit 0466000.

The new _AudioOutputTile widget in voice_compact.dart subscribes to
AudioRouter.currentDeviceStream so the row label + icon auto-update
when the user plugs in headphones, connects AirPods, etc. \u2014 no
manual KVO observation needed.

Tile is mobile-only (Platform.isIOS || Platform.isAndroid). Desktop
hosts continue to use the system mixer; the tile is hidden.

Plugin caveat: the published enum AudioSourceType has no .usb
variant (despite README mentioning USB support). We map only the
seven real enum cases: builtinSpeaker / builtinReceiver / bluetooth /
wiredHeadset / carAudio / airplay / unknown.

Issue 2 \u2014 collapse voice controls into the single modal sheet:

The AppBar gear icon (Icons.tune) that opened VoiceSettingsDialog
was removed. It duplicated the configuration entry point that the
status chip \u2192 modal-sheet path already provides, and the user found
that duplication confusing on a phone-narrow screen where AppBar
real estate is precious.

The voice modal sheet (showVoiceDetailsSheet) is now the **single**
voice-controls surface on mobile, with layout (top to bottom):

  1. Audio output route picker tile (iOS / Android only) \u2014 new.
  2. Mode + bind / release-tail recap (display only).
  3. 'Adjust mode & release tail' OutlinedButton that closes the
     sheet and opens the same VoiceSettingsDialog the gear icon
     used to open. One config form, not two.
  4. Mic level meter.
  5. TX / RX frame counts + mic state.
  6. PTT capability badge (desktop only).

Sheet title renamed from 'Voice settings' (which collided with the
gear-icon tooltip) to 'Voice'. New l10n keys: voiceSheetTitle,
voiceAdjustSettings, audioOutputLabel, audioRoute{Speaker,Receiver,
Bluetooth,WiredHeadset,CarAudio,Airplay,Unknown}. en + zh translated.

Build: flutter build ios --release --no-codesign clean, 50.5 s,
Runner.app 30.2 MB (+200 KB from audio_router). flutter analyze
clean (6 pre-existing Radio.groupValue deprecation infos in
voice_settings.dart, unchanged).
2026-05-16 18:18:43 +08:00
EdisonJwa 4ee2b3850a fix(ios): rename allowBluetooth -> allowBluetoothHFP (iOS 26 SDK)
Xcode warning on iOS SDK 26+:
  'allowBluetooth' was deprecated in iOS 8.0: renamed to
  'AVAudioSession.CategoryOptions.allowBluetoothHFP'

The flag was renamed in iOS 8 (a decade ago) but the old name has
been kept as a soft-deprecated alias. iOS 26 SDK finally emits the
warning, and -Werror builds would fail on it. Same semantics:
permit HFP-profile Bluetooth headsets as input + output. Kept
.allowBluetoothA2DP alongside for higher-quality output-only A2DP
devices.

flutter build ios --release --no-codesign: 10.7 s, Runner.app
30.0 MB.
2026-05-16 17:56:25 +08:00
EdisonJwa 0466000733 fix(ios): defer AVAudioSession.setActive(true) to didBecomeActive
From iPhone log:
  chanora_flutter: AVAudioSession setup failed:
    Error Domain=NSOSStatusErrorDomain Code=561017449
    'Session activation failed'

Error code 561017449 = AVAudioSessionErrorCodeCannotStartPlaying
(ASCII '!cat' big-endian). iOS 17+ refuses setActive(true) calls
made before the app's scene is foregrounded: the audio policy
server denies the activation because the app is not yet considered
the foreground priority owner. didFinishLaunchingWithOptions runs
BEFORE the scene becomes .active, so synchronous activation there
hits this race on cold launch.

Symptom flow:
  1. App cold-launch -> AppDelegate.didFinishLaunching fires
  2. setActive(true) -> Error 561017449
  3. Audio session is left inactive
  4. cpal's later attempts to open RemoteIO see an inactive
     session and reject with StreamConfigNotSupported
  5. voice_join fails at ensure_audio_running
  6. user sees the audio failure manifested as missing mute /
     continuous / PTT buttons (now fixed in f1f81a3 to be
     lenient; this commit also unblocks the underlying audio).

Fix: split the AVAudioSession configuration into two phases:
  * setCategory at didFinishLaunching (always safe).
  * setActive(true) deferred to UIApplication.didBecomeActive    Notification, which fires after the cold-launch settle and
    on every resume-from-background. Repeated setActive while
    already-active is a no-op per docs.

This is the canonical iOS voice-app pattern (Discord, Zoom,
FaceTime, Flutter's  package all follow it). Documented
in commit body comments.

flutter build ios --release --no-codesign: 13.2 s, Runner.app
30.0 MB.
2026-05-16 17:53:49 +08:00
EdisonJwa a93109ac37 fix(ios): rebuild chanora_bridge.framework on every Xcode build, not just pod install
The podspec's prepare_command only fires on `pod install`. Once a
framework was generated, Rust source changes were silently ignored
because Xcode kept re-bundling the stale framework into Runner.app.
Manifested today as: ran `cargo build --release --target
aarch64-apple-ios` to pick up the lenient voice_join fix, ran
`flutter build ios`, but Runner.app/Frameworks/chanora_bridge.
framework/chanora_bridge was still the framework from the previous
pod install (16:10) not the just-built 18:30 dylib.

Add an explicit `script_phase` to the podspec that re-runs:
  1. cargo build --release --target aarch64-apple-ios -p chanora_bridge
  2. cp dylib into Frameworks/chanora_bridge.framework/chanora_bridge
  3. install_name_tool -id @rpath/...

on every Xcode 'Build', not just on pod install. The script short-
circuits when the framework's binary mtime is newer than the cargo
output (fast no-op on incremental builds where Rust didn't change).

Side effect: every Xcode build now invokes cargo, which can take
~5 s on a warm cache and ~1 min cold. This is the right trade-off
because the previous behavior silently shipped stale Rust code.
2026-05-16 17:31:43 +08:00
EdisonJwa f1f81a3d7e fix(core,bridge,ios): voice_join survives audio-engine failure; iOS log file
User report from iPhone: 'mute / continuous / PTT buttons missing'
with NO error popup. Root cause: voice_join's audio-engine startup
was failing silently, and the failure propagated out as a hard
error \u2014 which means SessionEvent::VoiceState(true) was never sent
to Dart even though the server-side channel move had already
succeeded. Dart's _inChannel stayed false; every control gated on
_inChannel disappeared while the channel tree continued to show
the user as joined.

This commit makes voice_join lenient on audio-engine failures so
the UI state matches the server-side reality, and also gives iOS
a writable log file so diagnostics from device builds are
recoverable for the first time.

core/chanora_core/src/lib.rs::voice_join
  * ensure_audio_running's error is now logged + emitted as
    SessionEvent::AudioStopped, but does NOT abort voice_join.
    The server move at step 1 already succeeded; failing the
    Dart-visible promise here would leave the UI in a phantom
    'in-channel visually but no controls' state. After this
    commit:
      - mic / headset / settings appear in the AppBar
      - PTT button appears at the bottom
      - status chip shows live audio state ('Mic on/off')
      - if audio actually failed (mic permission denied,
        no input device, CoreAudio rejecting stream config)
        the user can retry by switching modes / channels;
        BridgeEvent::AudioStopped wires _audioStarted=false
        in Dart so audio-stats poll is honest about the
        engine state.

crates/chanora_bridge/src/api.rs::log_file_path
  * iOS now writes the log to /home/milkice/Documents/chanora.log
    (Documents is the standard user-visible iOS sandbox dir).
  * Android remains None pending the bridge JNI init wiring
    a writable path (P1 follow-up).

apps/chanora_flutter/ios/Runner/Info.plist
  * Adds UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace
    so the Documents directory shows up under 'On My iPhone \u2192
    Chanora' in the Files.app. The user can now copy chanora.log
    out for support without needing Xcode \u2192 Devices and
    Simulators \u2192 Download Container.

Workspace tests: 78/0/1 unchanged.
flutter build ios --release --no-codesign: 22.7 s clean
(Runner.app 29.9 MB).
2026-05-16 17:16:17 +08:00
EdisonJwa 7c0e5caa92 fix(ui,ios): first-tap on TextField now opens the keyboard
User report: 'every first time to tap to input box nothing
happened'. Symptom is iPhone-specific: the very first tap on any
of the Host / Nickname / Password text boxes in the connect form
fails to focus + open the keyboard. The second tap on the same
field works.

Two distinct causes, both addressed:

  apps/chanora_flutter/lib/main.dart
    The connect form lives inside a SingleChildScrollView. Flutter
    on iOS has a long-standing issue (flutter#19027) where the
    enclosing Scrollable's gesture-arena participant absorbs the
    first tap as a possible scroll-intent, leaving the TextField
    unfocused; the second tap reaches the field because the
    scrollable has already declined to handle a drag.

    Fix: _ConnectForm converted from StatelessWidget to
    StatefulWidget so it can own FocusNodes for the three text
    fields. Each TextField gains:
      * focusNode: <its own FocusNode>
      * onTap: () => focusNode.requestFocus()
        — forces focus on tap-down regardless of arena outcome
      * textInputAction: TextInputAction.next (host, nick) /
        TextInputAction.done (password) for return-key flow
      * autocorrect: false, enableSuggestions: false
        — these are server-host / nickname / password fields, the
        iOS auto-correct + suggestion bar is wrong for all three.

    Also set keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior
    .onDrag on the SingleChildScrollView so the keyboard hides
    when the user starts scrolling the bookmark list below.

  apps/chanora_flutter/ios/Runner/AppDelegate.swift
    AVAudioSession.sharedInstance().requestRecordPermission was
    fired synchronously from didFinishLaunchingWithOptions. The
    permission alert can race with iOS's text-input subsystem
    initialisation: if the alert appears before the keyboard
    layer finishes wiring up, subsequent text-field focus
    requests are dropped silently.

    Fix: DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) to
    defer the permission request until ~1 s after the app shell
    is on screen. Long enough for iOS's text-input layer to fully
    initialise; short enough that the user reads the prompt
    before tapping a field.

flutter analyze: clean (6 pre-existing Radio.groupValue infos).
flutter build ios --release --no-codesign: 26.8 s clean
(Runner.app 29.8 MB).
2026-05-16 15:50:27 +08:00
EdisonJwa d4c04b6a72 fix(ui,audio,ios,macos): eight P0 mobile fixes
User report from sideloaded iPhone build, in order of priority:

  #4 'Could not join channel: audio: audio backend:
     build_output_stream: The requested stream configuration is
     not supported by the device.'

     Cause: we forced cpal::BufferSize::Fixed(2048) on the output
     and input streams unconditionally on non-Linux. iOS CoreAudio
     RemoteIO units reject arbitrary buffer-size requests with that
     exact error. Windows WASAPI needs the pinning for shared-mode
     jitter, but macOS / iOS do not.
     Fix: cfg-gate Fixed(2048) to target_os = 'windows'; everywhere
     else use BufferSize::Default and let the platform HAL pick.
     crates/chanora_audio/src/engine.rs.

  #5 'Could not join channel: invariant violated:
     voice_in already taken'

     Cause: start_audio tore down the old engine BEFORE attempting
     to construct the new one, and consumed voice_in (an mpsc
     Receiver that can only be taken once) early. When the new
     engine failed mid-construction (e.g. because of #4 above) the
     session was left with: no audio engine, voice_in consumed,
     no way to retry without reconnect. The second voice_join
     attempt surfaced the invariant message.
     Fix: build the new engine BEFORE tearing down the old. Only
     swap state.audio if construction succeeded. crates/chanora_
     core/src/lib.rs::ChanoraSession::start_audio. Additionally
     added a put_voice_in helper to the protocol adapter (
     crates/chanora_protocol/src/adapter.rs) for a future
     broadcast-channel migration; the helper is unused on the
     immediate fix path but documents the intent.

  #3 'permission request would better on first open'

     Cause: AVAudioSession only triggers the mic-permission
     prompt the first time it tries to record. We never recorded
     until voice_join, so the prompt fired then.
     Fix iOS: AVAudioSession.sharedInstance().requestRecordPermission
     in AppDelegate.swift::application(_:didFinishLaunchingWithOptions:).
     Fix macOS: AVCaptureDevice.requestAccess(for: .audio) in
     macos/Runner/AppDelegate.swift::applicationDidFinishLaunching.
     Both run non-blocking; user can deny without crashing app
     launch, and voice_join then surfaces a clearer downstream
     error when the engine fails to open the input device.

  #1 + #2 'one-column upper takes too much space; Push to Talk
           button at bottom would be better'

     Layout rework for narrow-mode (single column, mobile shape):
     - Flipped the stacking order in main.dart so Voice Bar moves
       to the BOTTOM of the body and the channel tree (Expanded)
       fills above. Wide-mode (Row, >= 840 dp) layout unchanged.
     - Inside the Voice Bar on touch-only hosts, moved the
       on-screen Push to Talk button to be the LAST element of
       the Voice Bar (was Row 3). Order now: pill + mutes, mode
       badge + settings, level meter, stats line, release-tail
       caption, PTT button. The button is closest to the user's
       thumb when the Voice Bar is pinned to the bottom of a
       narrow-layout screen.

  #6 'remove right top debug badge'

     debugShowCheckedModeBanner: false on the MaterialApp.
     Release builds never showed it anyway; this only affects
     local dev / debug builds.

  #7 'what does the refresh button use for? nothing happened'

     Removed. The snapshot updates via BridgeEvent::SnapshotChanged
     are pushed from the bridge — a manual rust.snapshot() call
     was redundant. Now only the Diagnostics + Disconnect actions
     remain in the AppBar trailing row when connected.

  #8 'Bind Key related function should not be added to a mobile
     platform'

     widgets/voice_settings.dart: bind-key OutlinedButton is now
     #cfg'd out when Platform.isIOS || Platform.isAndroid. The
     release-tail slider stays because it still applies to the
     on-screen PTT button. Capability badge in voice_bar.dart
     also hidden on mobile (it would always show L0Focused which
     is redundant with the visible on-screen button).

Tests + analyze: chanora_audio 34/0/0 on macOS, workspace 78/0/1
on Linux; flutter analyze clean (6 pre-existing Radio.groupValue
infos). flutter build ios --release --no-codesign: 28.8 s clean
(Runner.app 29.9 MB).
2026-05-16 15:41:08 +08:00
EdisonJwa f5d3810f5f feat(ios,macos): Apple privacy manifest (PrivacyInfo.xcprivacy)
Apple has enforced a `PrivacyInfo.xcprivacy` privacy manifest at App
Store submission since May 2024 for iOS / iPadOS / visionOS /
watchOS, and rolled the requirement out to macOS in late 2024.
Without the file, App Store Connect rejects archive uploads with
"missing required privacy manifest". This commit adds the manifest
for both iOS and macOS Runner targets.

apps/chanora_flutter/ios/Runner/PrivacyInfo.xcprivacy
apps/chanora_flutter/macos/Runner/PrivacyInfo.xcprivacy
  Identical content. Declarations:

  NSPrivacyCollectedDataTypes:
    NSPrivacyCollectedDataTypeAudioData
      Microphone audio transmitted to the user's chosen voice
      server while connected and unmuted. Not linked to user
      identity (no Apple ID / IDFA tied), not used for tracking.
      Purpose: AppFunctionality (communications).

  NSPrivacyTracking: false
  NSPrivacyTrackingDomains: []
    Chanora performs no cross-app / cross-website tracking.

  NSPrivacyAccessedAPITypes:
    FileTimestamp (C617.1)
      tokio + rusqlite file I/O for identity.tskey, chanora.db,
      audio_meta.json, chanora.log inside the app container.
    UserDefaults (CA92.1)
      Indirect via path_provider Flutter plugin querying for
      Application Support / Documents directories.
    SystemBootTime (35F9.1)
      tracing-subscriber timestamps log records relative to boot.
    DiskSpace (85F4.1)
      rusqlite checks before sqlite page writes.

  All four "required reason" API categories use Apple's published
  allow-list reason codes; no fingerprinting / analytics usage.

apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj
  Added PrivacyInfo.xcprivacy to the Runner group and to the
  Runner target's "Copy Bundle Resources" build phase via the
  xcodeproj Ruby gem (via a one-shot script). With this, the file
  is placed at Runner.app/PrivacyInfo.xcprivacy where Apple's
  validator looks for it — `find Runner.app -name
  PrivacyInfo.xcprivacy` shows our manifest at the bundle root
  alongside Flutter's and connectivity_plus's.

Verified on the M1 Mac (coder@100.118.130.73):
  flutter build ios --release --no-codesign                4.0 s
    -> Runner.app/PrivacyInfo.xcprivacy present
  flutter build ipa --release --no-codesign                28.4 s
    -> Runner.xcarchive built (171.4 MB)
    -> archive's Runner.app/PrivacyInfo.xcprivacy present
    -> archive's Runner.app/Frameworks/chanora_bridge.framework
       built fresh via the chanora_bridge.podspec prepare_command
       under xcodebuild's sandbox (no PATH / env weirdness).

P1 follow-ups noted by xcodebuild's validator (not blockers for
this commit but for App Store submission):
  * Real app icon (currently default placeholder)
  * Real launch image (currently default placeholder)
  * Paid Apple Developer Program account, registered App ID, and
    Distribution provisioning profile (Personal Team sideloads
    still work as today).
2026-05-16 16:03:39 +09:00
EdisonJwa 1f729284b3 feat(flutter,ios): vendor chanora_bridge as a CocoaPods framework
iOS rejects loose .dylib loads (`dlopen` of any path outside the
app bundle is sandboxed), so a Flutter-Rust bridge has to ship
inside the .app as an Embed-and-Sign framework that
flutter_rust_bridge's runtime loader can dlopen via its default
`chanora_bridge.framework/chanora_bridge` lookup path.

This commit wires the bridge into the iOS build via a CocoaPods
podspec. Same role Cargokit plays for other Flutter+Rust setups,
done by hand against this repo's layout to avoid the Cargokit
vendoring footprint that was previously dropped.

  apps/chanora_flutter/ios/chanora_bridge.podspec
    New file. `prepare_command` invokes
    `cargo build --release --target aarch64-apple-ios -p chanora_bridge`
    with IPHONEOS_DEPLOYMENT_TARGET=13.0 +
    CMAKE_POLICY_VERSION_MINIMUM=3.5 (satisfies audiopus_sys's
    cmake invocation on modern CMake 4.x), then wraps the
    produced libchanora_bridge.dylib into
    chanora_bridge.framework with an Info.plist that declares
    iPhoneOS / MinimumOSVersion=13.0, and rewrites LC_ID_DYLIB
    to @rpath/chanora_bridge.framework/chanora_bridge.
    `vendored_frameworks` exposes the result to CocoaPods, which
    integrates it into Runner.xcodeproj with Embed & Sign
    automatically. No Xcode UI edits required.

  apps/chanora_flutter/ios/Podfile
    Add `pod 'chanora_bridge', :path => '.'` to the Runner
    target.

  apps/chanora_flutter/ios/Podfile.lock
    Generated by `pod install` after the pod was added. Pins
    chanora_bridge 1.0.0 + checksum so iOS builds on other
    developer machines pull the same framework version.

  .gitignore
    Add `/apps/chanora_flutter/ios/Frameworks/` so the
    ~13 MB built framework (regenerated on every pod install) is
    not committed.

Verified end-to-end on the M1 Mac (coder@100.118.130.73):
  pod install                                              ok
    chanora_bridge.framework generated at
    apps/chanora_flutter/ios/Frameworks/chanora_bridge.framework
    Install name: @rpath/chanora_bridge.framework/chanora_bridge
  flutter build ios --release --no-codesign                12.7 s
    -> Runner.app 29.9 MB (was 16.9 MB without the bridge)
    -> Runner.app/Frameworks/chanora_bridge.framework present
       alongside Flutter.framework, App.framework,
       connectivity_plus.framework, objective_c.framework.

DEC-025: iOS / iPad officially in scope for P0.

Notes for owners installing on physical iOS devices:
  The Personal Apple Team in Xcode requires a unique
  PRODUCT_BUNDLE_IDENTIFIER (the default 'app.chanora.chanoraFlutter'
  may already be claimed in the App Store registry). Set this
  locally in Xcode -> Runner target -> Signing & Capabilities ->
  Bundle Identifier (e.g. yourname.chanora.chanoraFlutter); do
  NOT commit that change back since it is user-specific.
2026-05-16 15:57:36 +09:00
EdisonJwa f0ddb160a0 fix(protocol,audio,ios): native-tls instead of rustls+aws-lc-rs
Building the bridge for `aarch64-apple-ios` failed in two ways with
the previous TLS stack:

  1. `aws-lc-sys` (transitive: rustls -> aws-lc-rs -> aws-lc-sys)
     does not cross-compile cleanly to iOS — the build produced
     undefined symbols for architecture arm64 (mldsa44, ec_GFp_mont,
     etc).
  2. `audiopus_sys` linked against the wrong iOS runtime version,
     missing `___chkstk_darwin`.

Following the rustls-platform-verifier docs and the standard Rust+
iOS+TLS pattern used by 1Password / Signal / rustup / Bitwarden,
this commit swaps the TLS provider to **native-tls** so each
platform picks its own:

  * macOS + iOS  -> Security.framework (no external C deps)
  * Windows      -> SChannel
  * Linux/BSD    -> system OpenSSL

Changes:

crates/chanora_protocol/Cargo.toml
crates/chanora_audio/Cargo.toml
  * Drop `default-tls` from tsclientlib's features. The remaining
    `audio` feature is what we actually use; default-tls was a
    reqwest convenience that picked rustls+aws-lc-rs.
  * Add a direct `reqwest` dep with `default-features = false,
    features = ["charset", "http2", "native-tls"]`. Cargo's
    workspace feature unification carries this through the
    transitive `tsclientlib -> reqwest` chain.

apps/chanora_flutter/ios/Podfile
  * Uncomment `platform :ios, '13.0'` so CocoaPods stops emitting
    the implicit-platform warning and Xcode's iOS deployment-
    target check is honored.

apps/chanora_flutter/ios/Podfile.lock
  * Generated by `pod install` after the platform pin. Committed so
    iOS builds on other developer machines pull the exact same Pod
    versions.

apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj
apps/chanora_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
  * CocoaPods auto-integration: adds Pods_Runner.framework +
    Pods_RunnerTests.framework references and the Pods xcconfig
    file references. Standard `pod install` output; reviewing the
    diff shows only Pod-bookkeeping additions, no signing or
    target-config drift.

Verified end-to-end on the M1 Mac (coder@100.118.130.73):
  cargo build --release -p chanora_bridge                29.09 s
  cargo build --release --target aarch64-apple-ios       24.32 s
    (with IPHONEOS_DEPLOYMENT_TARGET=13.0 and
     CMAKE_POLICY_VERSION_MINIMUM=3.5 in the env to satisfy the
     audiopus_sys cmake invocation; documented as a P1 build-glue
     follow-up.)
  flutter build ios --release --no-codesign              ok
    Built build/ios/iphoneos/Runner.app (16.9 MB)
  Xcode GUI build of Runner.xcworkspace                  ok
    (after the user opened Runner.xcworkspace, NOT
     Runner.xcodeproj, and Clean Build Folder.)

Tests on macOS unchanged: chanora_audio 34 / 0 / 0.

DEC-025: iOS + macOS officially in scope for P0.
2026-05-16 15:32:08 +09:00
EdisonJwa f3320715ea feat(audio,ios): AVAudioSession PlayAndRecord+voiceChat in AppDelegate
iOS AVAudioSession must be configured BEFORE Flutter starts its
audio pipeline; the canonical place is application(_:didFinishLaunching\
WithOptions:) in AppDelegate.swift. This commit:

apps/chanora_flutter/ios/Runner/AppDelegate.swift:
  * import AVFoundation
  * In application(_:didFinishLaunchingWithOptions:), call
    AVAudioSession.sharedInstance().setCategory(.playAndRecord,
      mode: .voiceChat,
      options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
    followed by setActive(true). Failures are NSLogged but do not
    block app launch — cpal's CoreAudio backend will still come up
    against the default iOS routing.

  This shape:
   * routes the receiver/speaker like a phone call (.playAndRecord +
     .voiceChat),
   * engages on-device AEC / NS where supported,
   * defaults to speaker so users don't have to hold the phone to
     their ear,
   * permits Bluetooth headsets (AirPods et al. just work).

crates/chanora_audio/src/engine.rs:
  * Replace the iOS engine-start placeholder log line ('binding
    pending — Chanora iOS audio is documented-only for Beta') with
    an honest acknowledgment that the AVAudioSession configuration
    lives Swift-side. The Rust engine acknowledges the request, then
    cpal opens its CoreAudio streams against the session.

iOS-only Rust code is #[cfg(target_os = "ios")]-gated so this commit
is no-op on every other platform.

SRS-197: iOS/macOS audio routing contract. DEC-025: iOS officially
in scope for P0 (Focused PTT only — Apple's sandbox model has no
global PTT analogue).
2026-05-16 13:50:43 +08:00
EdisonJwa e3d7017dd9 feat(macos,ios): entitlements + permission strings + bundle-glue script
macOS:
  * Runner/DebugProfile.entitlements + Release.entitlements: add
    com.apple.security.network.client (outbound TS3 server connect)
    and com.apple.security.device.audio-input (microphone capture).
    Debug keeps com.apple.security.network.server + cs.allow-jit
    (Flutter hot-reload needs both); Release drops them.
  * Runner/Info.plist: add NSMicrophoneUsageDescription and
    NSInputMonitoringUsageDescription so the macOS system prompts
    show a sensible explanation when Chanora first needs mic or
    Input Monitoring access. Input Monitoring is required by
    CGEventTapCreate (SDD-085).
  * Runner.xcodeproj/project.pbxproj: switch Debug/Release/Profile
    code-signing from Automatic + Apple Development to Manual +
    "Sign to Run Locally" (CODE_SIGN_IDENTITY = -). This lets
    `flutter build macos --release` work over SSH where the login
    keychain is locked. The owner re-enables the personal team
    locally in Xcode for physical-device iOS testing later.

iOS:
  * Runner/Info.plist: add NSMicrophoneUsageDescription and the
    UIBackgroundModes = ['audio'] entry so voice traffic continues
    when the app is backgrounded (TS3 servers drop clients on idle
    audio streams).

tools/macos-postbuild.sh: new script. flutter build macos --release
emits build/macos/Build/Products/Release/chanora_flutter.app but
does NOT bundle libchanora_bridge.dylib. FRB on macOS dlopen()s the
bridge as chanora_bridge.framework/chanora_bridge, not a plain
dylib. This script:

  1. Wraps target/release/libchanora_bridge.dylib in a proper
     chanora_bridge.framework (Versions/A layout, Info.plist,
     Resources, symlinks).
  2. Rewrites LC_ID_DYLIB to
     @rpath/chanora_bridge.framework/chanora_bridge.
  3. Ad-hoc codesigns the framework and the .app bundle.
  4. Verifies with codesign --verify --deep --strict.

macOS analogue of buildit.cmd on Windows. Auto-integration into
Xcode build phases via cargokit / corrosion is a P1 carryover.

Verified end-to-end on the M1 Mac:
  cargo build --release -p chanora_bridge          11.76 s
  flutter build macos --release                    ok (59.2 MB)
  tools/macos-postbuild.sh Release                 ok
  chanora_flutter.app launch via SSH               bridge initialised,
                                                   identity + bookmark
                                                   store initialised
                                                   (~5 s smoke).
  ~/Library/Logs/app.chanora.chanora_flutter/chanora.log captures
  the boot sequence cleanly.

DEC-025 reference: macOS desktop is officially in scope.
2026-05-16 14:26:13 +09:00
EdisonJwa 4d57d189c9 feat(flutter,macos,ios): scaffold platform Xcode projects via flutter create
Ran `flutter create --platforms=macos,ios --project-name=chanora_flutter
--org=app.chanora .` on the M1 Mac to generate the standard Flutter
platform-specific scaffolding (Runner.xcodeproj, Podfile, AppDelegate,
entitlements, etc.) for both macOS and iOS.

The cross-platform Dart source (lib/) and Rust workspace (crates/,
core/) carry the actual application logic; these scaffolds are
required only so flutter build macos / ios can resolve their Xcode
projects. No application code added.

macOS smoke-launch from the M1 Mac verified the bridge dylib load
path: after manually wrapping libchanora_bridge.dylib into a proper
chanora_bridge.framework bundle (FRB on macOS expects a framework,
not a plain dylib) and codesigning ad-hoc, the runner starts cleanly
through 'bridge initialised', 'identity store initialised', 'bookmark
store initialised' just like the Linux runner.

Following commits will:
  * Wire the framework-bundling step into build glue (currently manual
    install_name_tool + codesign).
  * Replace the macOS PTT backend stub (crates/chanora_audio/src/
    ptt_backends/macos.rs) with live IOHIDCheckAccess +
    CGEventTapCreate so the descriptor advertises real L2/L3 capability
    on a permission-granted box (SDD-085).
  * Add the iOS AVAudioSession PlayAndRecord+voiceChat wiring.
  * Add docs/verification/macos-p0-acceptance.md and ios-p0-acceptance.md.
2026-05-16 14:07:33 +09:00