Files
chanora/apps/chanora_flutter/lib/src/rust/api.dart
T
Edison Jwa 0a6ef55937 fix(ios): unblock iOS Debug builds + regenerate FRB for file transfer API (#41)
* fix(ios): preserve Silero VAD symbols in Flutter Debug builds

Flutter Debug builds split user code into a sibling Chanora.debug.dylib
alongside a minimal launcher executable; the six @_cdecl symbols
chanora_silero_vad_* land in the dylib, not the main binary. Two
problems were hiding behind that:

1. Debug.xcconfig only carried -exported_symbol flags, missing the
   load-bearing -u force-undefined flags Release.xcconfig already had.
   Without -u, the linker dropped the Swift @_cdecl symbols (no Swift
   caller exists) before -exported_symbol could re-export them, so the
   dylib shipped without the VAD entry points.

2. verify_silero_exports.sh inspected only ${EXECUTABLE_PATH}, which in
   Debug builds is the launcher stub. Even with the symbols correctly
   landing in the debug dylib, the script falsely reported them missing.

Mirror Release.xcconfig's -u + -exported_symbol pair in Debug.xcconfig
and teach the verify script to prefer Chanora.debug.dylib when it sits
next to the launcher. Release builds are unaffected (single fat binary
remains the inspected target).

* build(flutter): regenerate FRB bindings for file transfer API

Run flutter_rust_bridge_codegen against the post-PR-#40 chanora_bridge
to emit typed Dart bindings for initCache, downloadAvatar, downloadIcon,
clearFileCache, and fileCacheSize. Replaces the dynamic-cast +
NoSuchMethodError-swallowing shims that lib/src/rust/api.dart shipped
as a pre-regen safety net.

Functional change: downloadAvatar/downloadIcon now return Uint8List?
(was List<int>?) directly through the typed dispatch table. Existing
consumers stay source-compatible because Uint8List extends List<int>;
no call sites needed updates.

* fix(ios): set ITSAppUsesNonExemptEncryption to false

App uses only standard/exempt encryption (HTTPS, system-provided crypto);
declaring exempt status removes the App Store export-compliance prompt
at every TestFlight/release upload.
2026-06-10 15:20:59 +09:00

1636 lines
50 KiB
Dart

// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `dispatch_platform_audio_event`, `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `platform_audio_events`, `process`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `PlatformAudioEvent`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
/// Return the platform-conventional log-file path as a string, or
/// an empty string if the platform does not have one (mobile).
String logFilePathStr() => RustLib.instance.api.crateApiLogFilePathStr();
/// Connect to a TeamSpeak-compatible server and return the initial
/// state snapshot. Honours the DEC-006 single-connection invariant
/// via [`BridgeError::AlreadyConnected`].
///
/// `password` is optional — pass an empty string for servers that
/// don't require one.
Future<BridgeSnapshot> connect({
required String host,
required String nickname,
required String password,
}) => RustLib.instance.api.crateApiConnect(
host: host,
nickname: nickname,
password: password,
);
/// Warm server address resolution for the active host field. This is
/// intentionally fire-and-forget from the UI perspective: it schedules
/// Rust-side prefetch work and never opens a TS3 session.
Future<void> prefetchServer({required String host}) =>
RustLib.instance.api.crateApiPrefetchServer(host: host);
/// Re-fetch a fresh snapshot from the active connection.
Future<BridgeSnapshot> snapshot() => RustLib.instance.api.crateApiSnapshot();
/// Fetch richer profile and live connection details for one online client.
Future<BridgeClientProfile> clientProfile({required BigInt clientId}) =>
RustLib.instance.api.crateApiClientProfile(clientId: clientId);
/// Disconnect from the server. No-op if not connected.
Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
/// True if a connection is currently active.
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
/// Handle iOS AVAudioSession route changes (SDD-100).
void handleRouteChange({required BridgeAudioRoute route}) =>
RustLib.instance.api.crateApiHandleRouteChange(route: route);
/// Handle iOS AVAudioSession media-services reset with the current
/// route class. Called by AppDelegate after rebuilding the session.
///
/// `route_class` is the Swift-side route class string (e.g. "Speaker").
void handleMediaServicesResetWithRoute({required String routeClass}) => RustLib
.instance
.api
.crateApiHandleMediaServicesResetWithRoute(routeClass: routeClass);
/// Handle iOS AVAudioSession interruption begin (SDD-101).
void handleInterruptionBegan() =>
RustLib.instance.api.crateApiHandleInterruptionBegan();
/// Handle iOS AVAudioSession interruption end (SDD-101).
void handleInterruptionEnded({required bool shouldResume}) => RustLib
.instance
.api
.crateApiHandleInterruptionEnded(shouldResume: shouldResume);
/// Set the focused/on-screen push-to-talk hold state.
///
/// Binding capture chooses which physical key drives PTT, while this
/// command carries the actual press/release edge for fallback focused
/// keyboard handling and touch controls.
Future<void> setPtt({required bool active}) =>
RustLib.instance.api.crateApiSetPtt(active: active);
/// Join a voice channel (SDD-094). Moves the user to `channel_id`,
/// brings up the audio engine if needed, and emits
/// `BridgeEvent::VoiceState`. `password` may be empty.
Future<void> voiceJoin({required BigInt channelId, required String password}) =>
RustLib.instance.api.crateApiVoiceJoin(
channelId: channelId,
password: password,
);
/// Leave the current voice channel (SDD-094). Tears down the audio
/// engine and emits `BridgeEvent::VoiceState`.
Future<void> voiceLeave() => RustLib.instance.api.crateApiVoiceLeave();
/// Set the active transmit mode (SDD-095).
Future<void> setTransmitMode({required BridgeTransmitMode mode}) =>
RustLib.instance.api.crateApiSetTransmitMode(mode: mode);
/// Read the active transmit mode.
Future<BridgeTransmitMode> getTransmitMode() =>
RustLib.instance.api.crateApiGetTransmitMode();
/// Update the release-tail in milliseconds (SDD-096). Values are
/// clamped to `0..=500` on the Rust side; passing anything larger
/// silently saturates.
Future<void> setReleaseTailMs({required int ms}) =>
RustLib.instance.api.crateApiSetReleaseTailMs(ms: ms);
/// Read the current release-tail in milliseconds.
Future<int> getReleaseTailMs() =>
RustLib.instance.api.crateApiGetReleaseTailMs();
/// Engage or release the hard-mute clamp (SDD-094). When `true`
/// the audio engine transmits nothing regardless of mode.
Future<void> setHardMute({required bool muted}) =>
RustLib.instance.api.crateApiSetHardMute(muted: muted);
/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The
/// platform_key string is opaque to the bridge — it identifies the
/// bound key inside the platform backend and never appears in any
/// log record or diagnostic export (DEC-027 enforced at the
/// diagnostics-sanitizer layer).
Future<void> setPttBinding({
required BridgePttInputClass inputClass,
required String platformKey,
}) => RustLib.instance.api.crateApiSetPttBinding(
inputClass: inputClass,
platformKey: platformKey,
);
/// Read the current PTT capability descriptor. Matches the privacy-safe
/// `BridgeEvent::PttCapability` event shape; useful for the initial UI
/// render before the first event arrives.
Future<BridgePttDescriptor> pttDescriptor() =>
RustLib.instance.api.crateApiPttDescriptor();
/// Return the persisted PTT binding so the UI can hydrate its display
/// state at launch (e.g. show "PTT: Space" next to the badge before the
/// user re-opens the binding dialog). Empty strings mean no binding has
/// been persisted yet.
Future<BridgePttBinding> getPttBinding() =>
RustLib.instance.api.crateApiGetPttBinding();
/// Move our own client to `channel_id`. Optional channel password
/// for password-protected channels — pass an empty string when not
/// required.
Future<void> moveToChannel({
required BigInt channelId,
required String password,
}) => RustLib.instance.api.crateApiMoveToChannel(
channelId: channelId,
password: password,
);
/// Toggle self input-mute (microphone) on the server. Independent
/// of push-to-talk: a muted client never transmits regardless of
/// PTT state.
Future<void> setInputMuted({required bool muted}) =>
RustLib.instance.api.crateApiSetInputMuted(muted: muted);
/// Toggle self output-mute (speaker). Mutes locally *and* informs
/// the server. The server uses this for the channel icon next to
/// the client name; the local mute kicks in immediately even
/// before the server acknowledges.
Future<void> setOutputMuted({required bool muted}) =>
RustLib.instance.api.crateApiSetOutputMuted(muted: muted);
/// Set master output gain. `1.0` is unity, `0.0` is silent. Values
/// above `1.0` amplify and can clip downstream. Errors when audio
/// is not started.
Future<void> setOutputGain({required double gain}) =>
RustLib.instance.api.crateApiSetOutputGain(gain: gain);
/// Set per-client output volume (SRS-075). `1.0` is unity, `0.0`
/// mutes. No-op when client has no active voice queue. Volume is
/// applied directly to the tsclientlib AudioQueue and takes effect
/// immediately on the next render callback.
Future<void> setClientVolume({
required BigInt clientId,
required double volume,
}) => RustLib.instance.api.crateApiSetClientVolume(
clientId: clientId,
volume: volume,
);
/// Send a text message to the specified target.
Future<void> sendChatMessage({
required String message,
required BridgeMessageTarget target,
}) => RustLib.instance.api.crateApiSendChatMessage(
message: message,
target: target,
);
/// User-initiated diagnostic export. Returns a multi-line text
/// blob, redacted per the production policy, that the user can
/// share or copy. DEC-016 forbids automatic uploads — this is the
/// only path that surfaces logs.
///
/// SDD-112 item 10 / SDD-113 item 7 / SDD-116 item 3: when an
/// Android voice session is open this export embeds the
/// `[audio.android]` verification-matrix fragment (requested /
/// achieved stream config, per-effect engagement, latency tier).
/// On non-Android targets or before a voice session opens the
/// section is omitted. Per SDD-090 every field in that section is
/// a device-side technical scalar — no PII admitted.
String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
/// Wire the identity persistence store to a platform-private
/// directory. Should be called once on app start after Flutter has
/// resolved `getApplicationSupportDirectory()` (or equivalent).
///
/// Subsequent [`connect`] calls will reuse the persisted identity,
/// or generate-and-persist a fresh one on first use. This keeps the
/// server-visible UID stable across app restarts.
///
/// Beta caveat: the identity is stored as a plain file (mode 0600
/// on Unix). It is *not* encrypted at rest. RISK-PoC-002 documents
/// this gap; the v0.4 storage rework lands the proper Secret
/// Service + Android Keystore + iOS Keychain backends.
Future<void> initStorage({required String dir}) =>
RustLib.instance.api.crateApiInitStorage(dir: dir);
/// Configure the bridge blob cache root.
Future<void> initCache({required String dir}) =>
RustLib.instance.api.crateApiInitCache(dir: dir);
/// Resolve avatar bytes through the bridge.
Future<Uint8List?> downloadAvatar({
required String avatarHash,
required String clientUid,
}) => RustLib.instance.api.crateApiDownloadAvatar(
avatarHash: avatarHash,
clientUid: clientUid,
);
/// Resolve icon bytes through the bridge.
Future<Uint8List?> downloadIcon({required BigInt iconId}) =>
RustLib.instance.api.crateApiDownloadIcon(iconId: iconId);
/// Purge cached protocol-owned assets.
Future<void> clearFileCache() => RustLib.instance.api.crateApiClearFileCache();
/// Report the configured file-cache size.
Future<BigInt> fileCacheSize() => RustLib.instance.api.crateApiFileCacheSize();
/// List persisted bookmarks.
Future<List<BridgeBookmark>> listBookmarks() =>
RustLib.instance.api.crateApiListBookmarks();
/// Insert a bookmark and return its assigned id. The `id` field on
/// the input is ignored.
Future<PlatformInt64> addBookmark({required BridgeBookmark b}) =>
RustLib.instance.api.crateApiAddBookmark(b: b);
/// Update an existing bookmark.
Future<void> updateBookmark({required BridgeBookmark b}) =>
RustLib.instance.api.crateApiUpdateBookmark(b: b);
/// Delete a bookmark by id.
Future<void> deleteBookmark({required PlatformInt64 id}) =>
RustLib.instance.api.crateApiDeleteBookmark(id: id);
/// Notify the core of the latest OS-reported connectivity state.
/// Called by the Flutter side from `connectivity_plus` callbacks.
/// The core's supervisor uses this to (a) pre-charge the watchdog
/// on Offline and (b) short-circuit reconnect backoff on Online.
void setNetworkState({required BridgeNetworkState state}) =>
RustLib.instance.api.crateApiSetNetworkState(state: state);
/// Subscribe to lifecycle events. Each call yields a fresh
/// subscription; multiple subscribers are supported. On slow
/// consumers, events are dropped rather than blocking the supervisor
/// (consistent with `tokio::sync::broadcast::Receiver` semantics).
Stream<BridgeEvent> eventsStream() =>
RustLib.instance.api.crateApiEventsStream();
/// Read audio statistics. Errors if no connection or audio not started.
Future<BridgeAudioStats> audioStats() =>
RustLib.instance.api.crateApiAudioStats();
/// Subscribe to real-time microphone input level at ~30 Hz.
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
/// when the Dart subscriber cancels, the session is dropped, or
/// the session becomes persistently unavailable.
Stream<double> inputLevelStream() =>
RustLib.instance.api.crateApiInputLevelStream();
/// Apply the P1 audio-processing config.
Future<void> setAudioProcessingConfig({
required BridgeAudioProcessingConfig config,
}) => RustLib.instance.api.crateApiSetAudioProcessingConfig(config: config);
/// Read the current audio-processing config.
///
/// Returns the live config as last applied to the audio engine.
/// Returns a default config when no session is active.
Future<BridgeAudioProcessingConfig> getAudioProcessingConfig() =>
RustLib.instance.api.crateApiGetAudioProcessingConfig();
/// Read P1 audio-processing diagnostics.
Future<BridgeAudioProcessingStats> audioProcessingStats() =>
RustLib.instance.api.crateApiAudioProcessingStats();
/// List available audio input and output devices from the platform.
Future<BridgeAudioDeviceList> listAudioDevices() =>
RustLib.instance.api.crateApiListAudioDevices();
/// Set the preferred input device by id. Takes effect on next
/// `start_audio`.
Future<void> setInputDevice({String? id}) =>
RustLib.instance.api.crateApiSetInputDevice(id: id);
/// Set the preferred output device by id.
Future<void> setOutputDevice({String? id}) =>
RustLib.instance.api.crateApiSetOutputDevice(id: id);
/// Configure the VAD model path.
Future<void> setVadModelPath({required String path}) =>
RustLib.instance.api.crateApiSetVadModelPath(path: path);
/// Enable or disable audio debug WAV dumping.
Future<void> enableAudioDebugWavDump({required bool enabled}) =>
RustLib.instance.api.crateApiEnableAudioDebugWavDump(enabled: enabled);
/// Select the iOS voice-processing mode.
Future<void> setIosVoiceProcessingMode({
required BridgeIosVoiceProcessingMode mode,
}) => RustLib.instance.api.crateApiSetIosVoiceProcessingMode(mode: mode);
/// Set the preferred audio output route (Android/iOS).
void setAudioOutputRoute({required BridgeAudioRoute route}) =>
RustLib.instance.api.crateApiSetAudioOutputRoute(route: route);
/// Called from Flutter when the app enters background/foreground.
void recordLifecycleEvent({required String state}) =>
RustLib.instance.api.crateApiRecordLifecycleEvent(state: state);
/// Bridge processing backend.
enum BridgeAudioBackend {
/// Platform voice processing.
platformVoiceProcessing,
/// Sonora backend.
sonora,
/// WebRTC APM backend.
webrtcApm,
/// No-op backend.
noop,
}
/// Audio device info from the platform.
class BridgeAudioDevice {
/// Stable platform-reported device identifier.
final String id;
/// Human-readable device name.
final String name;
/// Additional device details useful for disambiguation.
final String details;
/// True if the OS reports this as the default device.
final bool isDefault;
/// True if Chanora currently has this device pinned.
final bool isSelected;
const BridgeAudioDevice({
required this.id,
required this.name,
required this.details,
required this.isDefault,
required this.isSelected,
});
@override
int get hashCode =>
id.hashCode ^
name.hashCode ^
details.hashCode ^
isDefault.hashCode ^
isSelected.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioDevice &&
runtimeType == other.runtimeType &&
id == other.id &&
name == other.name &&
details == other.details &&
isDefault == other.isDefault &&
isSelected == other.isSelected;
}
/// List of available audio devices.
class BridgeAudioDeviceList {
/// Available input devices.
final List<BridgeAudioDevice> inputDevices;
/// Available output devices.
final List<BridgeAudioDevice> outputDevices;
const BridgeAudioDeviceList({
required this.inputDevices,
required this.outputDevices,
});
@override
int get hashCode => inputDevices.hashCode ^ outputDevices.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioDeviceList &&
runtimeType == other.runtimeType &&
inputDevices == other.inputDevices &&
outputDevices == other.outputDevices;
}
/// P1 audio-processing configuration DTO.
class BridgeAudioProcessingConfig {
/// Route class.
final BridgeAudioRoute route;
/// iOS voice-processing mode.
final BridgeIosVoiceProcessingMode iosMode;
/// Processing backend.
final BridgeAudioBackend processingBackend;
/// VAD backend.
final BridgeVadBackend vadBackend;
/// AEC owner.
final BridgeEffectOwner aec;
/// Noise suppression owner.
final BridgeEffectOwner ns;
/// AGC owner.
final BridgeEffectOwner agc;
/// High-pass filter enabled.
final bool hpfEnabled;
/// Limiter enabled.
final bool limiterEnabled;
/// VAD hangover in ms.
final int vadHangoverMs;
/// VAD pre-roll in ms.
final int vadPreRollMs;
/// Minimum transmit duration in ms.
final int vadMinTxMs;
/// Debug WAV dump enabled.
final bool debugWavDumpEnabled;
const BridgeAudioProcessingConfig({
required this.route,
required this.iosMode,
required this.processingBackend,
required this.vadBackend,
required this.aec,
required this.ns,
required this.agc,
required this.hpfEnabled,
required this.limiterEnabled,
required this.vadHangoverMs,
required this.vadPreRollMs,
required this.vadMinTxMs,
required this.debugWavDumpEnabled,
});
@override
int get hashCode =>
route.hashCode ^
iosMode.hashCode ^
processingBackend.hashCode ^
vadBackend.hashCode ^
aec.hashCode ^
ns.hashCode ^
agc.hashCode ^
hpfEnabled.hashCode ^
limiterEnabled.hashCode ^
vadHangoverMs.hashCode ^
vadPreRollMs.hashCode ^
vadMinTxMs.hashCode ^
debugWavDumpEnabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioProcessingConfig &&
runtimeType == other.runtimeType &&
route == other.route &&
iosMode == other.iosMode &&
processingBackend == other.processingBackend &&
vadBackend == other.vadBackend &&
aec == other.aec &&
ns == other.ns &&
agc == other.agc &&
hpfEnabled == other.hpfEnabled &&
limiterEnabled == other.limiterEnabled &&
vadHangoverMs == other.vadHangoverMs &&
vadPreRollMs == other.vadPreRollMs &&
vadMinTxMs == other.vadMinTxMs &&
debugWavDumpEnabled == other.debugWavDumpEnabled;
}
/// P1 audio-processing stats DTO.
class BridgeAudioProcessingStats {
/// Input dBFS.
final double inputDbfs;
/// Render dBFS.
final double renderDbfs;
/// Processed capture dBFS.
final double processedDbfs;
/// Latest VAD probability.
final double vadProbability;
/// VAD active.
final bool vadActive;
/// Currently transmitting.
final bool transmitting;
/// VAD backend.
final BridgeVadBackend vadBackend;
/// Fallback VAD active.
final bool vadFallbackActive;
/// Processing backend.
final BridgeAudioBackend processingBackend;
/// iOS voice-processing mode.
final BridgeIosVoiceProcessingMode iosVoiceProcessingMode;
/// Audio route.
final BridgeAudioRoute audioRoute;
/// Actual sample rate.
final int actualSampleRateHz;
/// Actual IO buffer frames.
final int actualIoBufferFrames;
/// Input overruns.
final BigInt inputOverruns;
/// Output underruns.
final BigInt outputUnderruns;
/// Callback xruns.
final BigInt callbackXruns;
/// Clipped samples.
final BigInt clippedSamples;
/// Number of effectively silent processed capture frames.
final BigInt zeroFrames;
/// Number of processed capture frames.
final BigInt captureFrames;
/// Input callbacks carrying 10 ms of audio.
final BigInt callbacks10Ms;
/// Input callbacks carrying 20 ms of audio.
final BigInt callbacks20Ms;
/// Input callbacks carrying other sizes.
final BigInt callbacksOther;
/// Sonora enabled.
final bool sonoraEnabled;
/// Platform voice processing enabled.
final bool platformVoiceProcessingEnabled;
const BridgeAudioProcessingStats({
required this.inputDbfs,
required this.renderDbfs,
required this.processedDbfs,
required this.vadProbability,
required this.vadActive,
required this.transmitting,
required this.vadBackend,
required this.vadFallbackActive,
required this.processingBackend,
required this.iosVoiceProcessingMode,
required this.audioRoute,
required this.actualSampleRateHz,
required this.actualIoBufferFrames,
required this.inputOverruns,
required this.outputUnderruns,
required this.callbackXruns,
required this.clippedSamples,
required this.zeroFrames,
required this.captureFrames,
required this.callbacks10Ms,
required this.callbacks20Ms,
required this.callbacksOther,
required this.sonoraEnabled,
required this.platformVoiceProcessingEnabled,
});
@override
int get hashCode =>
inputDbfs.hashCode ^
renderDbfs.hashCode ^
processedDbfs.hashCode ^
vadProbability.hashCode ^
vadActive.hashCode ^
transmitting.hashCode ^
vadBackend.hashCode ^
vadFallbackActive.hashCode ^
processingBackend.hashCode ^
iosVoiceProcessingMode.hashCode ^
audioRoute.hashCode ^
actualSampleRateHz.hashCode ^
actualIoBufferFrames.hashCode ^
inputOverruns.hashCode ^
outputUnderruns.hashCode ^
callbackXruns.hashCode ^
clippedSamples.hashCode ^
zeroFrames.hashCode ^
captureFrames.hashCode ^
callbacks10Ms.hashCode ^
callbacks20Ms.hashCode ^
callbacksOther.hashCode ^
sonoraEnabled.hashCode ^
platformVoiceProcessingEnabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioProcessingStats &&
runtimeType == other.runtimeType &&
inputDbfs == other.inputDbfs &&
renderDbfs == other.renderDbfs &&
processedDbfs == other.processedDbfs &&
vadProbability == other.vadProbability &&
vadActive == other.vadActive &&
transmitting == other.transmitting &&
vadBackend == other.vadBackend &&
vadFallbackActive == other.vadFallbackActive &&
processingBackend == other.processingBackend &&
iosVoiceProcessingMode == other.iosVoiceProcessingMode &&
audioRoute == other.audioRoute &&
actualSampleRateHz == other.actualSampleRateHz &&
actualIoBufferFrames == other.actualIoBufferFrames &&
inputOverruns == other.inputOverruns &&
outputUnderruns == other.outputUnderruns &&
callbackXruns == other.callbackXruns &&
clippedSamples == other.clippedSamples &&
zeroFrames == other.zeroFrames &&
captureFrames == other.captureFrames &&
callbacks10Ms == other.callbacks10Ms &&
callbacks20Ms == other.callbacks20Ms &&
callbacksOther == other.callbacksOther &&
sonoraEnabled == other.sonoraEnabled &&
platformVoiceProcessingEnabled ==
other.platformVoiceProcessingEnabled;
}
/// Bridge route class for P1 audio-processing policy.
enum BridgeAudioRoute {
/// Built-in speakerphone.
speaker,
/// Built-in receiver/earpiece.
earpiece,
/// Wired or USB headset.
wiredHeadset,
/// Bluetooth HFP duplex route.
bluetoothHfp,
/// Bluetooth A2DP output-only route.
bluetoothA2Dp,
/// Unknown route.
unknown,
}
/// Statistics from the audio engine.
class BridgeAudioStats {
/// Number of Opus frames sent since audio started.
final int framesSent;
/// Number of inbound voice packets decoded.
final int framesReceived;
/// Current push-to-talk state.
final bool pttActive;
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
final double inputLevel;
const BridgeAudioStats({
required this.framesSent,
required this.framesReceived,
required this.pttActive,
required this.inputLevel,
});
@override
int get hashCode =>
framesSent.hashCode ^
framesReceived.hashCode ^
pttActive.hashCode ^
inputLevel.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioStats &&
runtimeType == other.runtimeType &&
framesSent == other.framesSent &&
framesReceived == other.framesReceived &&
pttActive == other.pttActive &&
inputLevel == other.inputLevel;
}
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
class BridgeBookmark {
/// Row id assigned by SQLite. Use `0` when adding new rows;
/// the returned id is then meaningful.
final PlatformInt64 id;
/// User-facing label.
final String displayName;
/// `hostname[:port]` or TSDNS name.
final String host;
/// Nickname to use for this bookmark.
final String nickname;
/// Optional remembered password. Empty string = none.
final String password;
const BridgeBookmark({
required this.id,
required this.displayName,
required this.host,
required this.nickname,
required this.password,
});
@override
int get hashCode =>
id.hashCode ^
displayName.hashCode ^
host.hashCode ^
nickname.hashCode ^
password.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeBookmark &&
runtimeType == other.runtimeType &&
id == other.id &&
displayName == other.displayName &&
host == other.host &&
nickname == other.nickname &&
password == other.password;
}
/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
/// without any wrapper-type ceremony.
class BridgeChannel {
/// Stable channel id.
final BigInt id;
/// Parent channel id; 0 means top-level.
final BigInt parent;
/// Display name.
final String name;
/// Server-side ordering hint.
final PlatformInt64 order;
/// True when the server marks the channel as password-protected.
final bool hasPassword;
/// Talk power threshold required to speak in this channel.
/// None means no talk-power restriction.
final int? neededTalkPower;
const BridgeChannel({
required this.id,
required this.parent,
required this.name,
required this.order,
required this.hasPassword,
this.neededTalkPower,
});
@override
int get hashCode =>
id.hashCode ^
parent.hashCode ^
name.hashCode ^
order.hashCode ^
hasPassword.hashCode ^
neededTalkPower.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeChannel &&
runtimeType == other.runtimeType &&
id == other.id &&
parent == other.parent &&
name == other.name &&
order == other.order &&
hasPassword == other.hasPassword &&
neededTalkPower == other.neededTalkPower;
}
/// Client as seen by Dart.
class BridgeClient {
/// Stable client id.
final BigInt id;
/// Channel id the client is currently in.
final BigInt channel;
/// Nickname.
final String name;
/// True when this client has muted microphone/input capture.
final bool inputMuted;
/// True when this client has muted speaker/output audio.
final bool outputMuted;
/// True when recent inbound voice activity was observed for this client.
final bool isSpeaking;
/// True for TeamSpeak ServerQuery clients.
final bool isServerQuery;
/// Current talk power value assigned by the server.
final int talkPower;
/// True when the server has granted talk power regardless of numeric value.
final bool talkPowerGranted;
const BridgeClient({
required this.id,
required this.channel,
required this.name,
required this.inputMuted,
required this.outputMuted,
required this.isSpeaking,
required this.isServerQuery,
required this.talkPower,
required this.talkPowerGranted,
});
@override
int get hashCode =>
id.hashCode ^
channel.hashCode ^
name.hashCode ^
inputMuted.hashCode ^
outputMuted.hashCode ^
isSpeaking.hashCode ^
isServerQuery.hashCode ^
talkPower.hashCode ^
talkPowerGranted.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeClient &&
runtimeType == other.runtimeType &&
id == other.id &&
channel == other.channel &&
name == other.name &&
inputMuted == other.inputMuted &&
outputMuted == other.outputMuted &&
isSpeaking == other.isSpeaking &&
isServerQuery == other.isServerQuery &&
talkPower == other.talkPower &&
talkPowerGranted == other.talkPowerGranted;
}
/// Rich profile and live connection details for one online client.
class BridgeClientProfile {
/// Stable online client id.
final BigInt id;
/// Channel the client is currently in.
final BigInt channel;
/// Nickname.
final String name;
/// TeamSpeak unique id.
final String uniqueId;
/// Stable TeamSpeak database id, when visible.
final BigInt? databaseId;
/// ISO country code, when visible.
final String countryCode;
/// User description, when visible.
final String description;
/// Client version string.
final String version;
/// Client platform string.
final String platform;
/// Account creation time as Unix seconds.
final PlatformInt64? createdUnixSeconds;
/// Last connection time as Unix seconds.
final PlatformInt64? lastConnectedUnixSeconds;
/// Total historical connections.
final BigInt? connectionsTotal;
/// Current online duration in seconds.
final PlatformInt64? onlineSeconds;
/// Current idle time in milliseconds.
final PlatformInt64? idleMilliseconds;
/// Current ping in milliseconds.
final PlatformInt64? pingMilliseconds;
/// Current ping deviation in milliseconds.
final PlatformInt64? pingDeviationMilliseconds;
/// Client address. Empty when permission-gated.
final String clientAddress;
/// Resolved server group names.
final List<String> serverGroups;
/// Resolved channel group name.
final String channelGroup;
/// TeamSpeak avatar file path suffix.
final String avatarPath;
/// Downloaded bytes this month.
final BigInt? bytesDownloadedMonth;
/// Uploaded bytes this month.
final BigInt? bytesUploadedMonth;
/// Downloaded bytes across all time.
final BigInt? bytesDownloadedTotal;
/// Uploaded bytes across all time.
final BigInt? bytesUploadedTotal;
/// Client-to-server total packet loss ratio.
final double? packetLossClientToServerTotal;
/// Server-to-client total packet loss ratio.
final double? packetLossServerToClientTotal;
const BridgeClientProfile({
required this.id,
required this.channel,
required this.name,
required this.uniqueId,
this.databaseId,
required this.countryCode,
required this.description,
required this.version,
required this.platform,
this.createdUnixSeconds,
this.lastConnectedUnixSeconds,
this.connectionsTotal,
this.onlineSeconds,
this.idleMilliseconds,
this.pingMilliseconds,
this.pingDeviationMilliseconds,
required this.clientAddress,
required this.serverGroups,
required this.channelGroup,
required this.avatarPath,
this.bytesDownloadedMonth,
this.bytesUploadedMonth,
this.bytesDownloadedTotal,
this.bytesUploadedTotal,
this.packetLossClientToServerTotal,
this.packetLossServerToClientTotal,
});
@override
int get hashCode =>
id.hashCode ^
channel.hashCode ^
name.hashCode ^
uniqueId.hashCode ^
databaseId.hashCode ^
countryCode.hashCode ^
description.hashCode ^
version.hashCode ^
platform.hashCode ^
createdUnixSeconds.hashCode ^
lastConnectedUnixSeconds.hashCode ^
connectionsTotal.hashCode ^
onlineSeconds.hashCode ^
idleMilliseconds.hashCode ^
pingMilliseconds.hashCode ^
pingDeviationMilliseconds.hashCode ^
clientAddress.hashCode ^
serverGroups.hashCode ^
channelGroup.hashCode ^
avatarPath.hashCode ^
bytesDownloadedMonth.hashCode ^
bytesUploadedMonth.hashCode ^
bytesDownloadedTotal.hashCode ^
bytesUploadedTotal.hashCode ^
packetLossClientToServerTotal.hashCode ^
packetLossServerToClientTotal.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeClientProfile &&
runtimeType == other.runtimeType &&
id == other.id &&
channel == other.channel &&
name == other.name &&
uniqueId == other.uniqueId &&
databaseId == other.databaseId &&
countryCode == other.countryCode &&
description == other.description &&
version == other.version &&
platform == other.platform &&
createdUnixSeconds == other.createdUnixSeconds &&
lastConnectedUnixSeconds == other.lastConnectedUnixSeconds &&
connectionsTotal == other.connectionsTotal &&
onlineSeconds == other.onlineSeconds &&
idleMilliseconds == other.idleMilliseconds &&
pingMilliseconds == other.pingMilliseconds &&
pingDeviationMilliseconds == other.pingDeviationMilliseconds &&
clientAddress == other.clientAddress &&
serverGroups == other.serverGroups &&
channelGroup == other.channelGroup &&
avatarPath == other.avatarPath &&
bytesDownloadedMonth == other.bytesDownloadedMonth &&
bytesUploadedMonth == other.bytesUploadedMonth &&
bytesDownloadedTotal == other.bytesDownloadedTotal &&
bytesUploadedTotal == other.bytesUploadedTotal &&
packetLossClientToServerTotal ==
other.packetLossClientToServerTotal &&
packetLossServerToClientTotal == other.packetLossServerToClientTotal;
}
/// Bridge effect owner for AEC/NS/AGC.
enum BridgeEffectOwner {
/// Platform-owned effect.
platform,
/// Sonora-owned effect.
sonora,
/// WebRTC APM-owned effect.
webrtcApm,
/// Conservative route-managed setting.
conservative,
/// Disabled.
off,
}
@freezed
sealed class BridgeEvent with _$BridgeEvent {
const BridgeEvent._();
/// Initial connect succeeded, or a reconnect attempt succeeded.
const factory BridgeEvent.connected({
/// Server name reported by the server snapshot.
required String serverName,
}) = BridgeEvent_Connected;
/// Connection lost; supervisor will retry.
const factory BridgeEvent.lost({
/// Reason classification from the protocol layer.
required String reason,
}) = BridgeEvent_Lost;
/// Supervisor is sleeping before its next reconnect attempt.
const factory BridgeEvent.reconnecting({
/// 1-based attempt counter for the current outage.
required int attempt,
/// Seconds the supervisor will sleep before this attempt.
required int delaySecs,
}) = BridgeEvent_Reconnecting;
/// Session ended (user-requested disconnect or unrecoverable).
const factory BridgeEvent.disconnected({
/// Reason classification.
required String reason,
}) = BridgeEvent_Disconnected;
/// Audio engine started.
const factory BridgeEvent.audioStarted() = BridgeEvent_AudioStarted;
/// Audio engine stopped.
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
/// Detected desktop Push-to-Talk capability (gen2 v0.9.3,
/// DEC-023..028). The fields carry only privacy-safe values per
/// DEC-027: the capability level, a stable backend identifier,
/// and the bound input class. No key codes, scan codes, or
/// virtual-key values cross this boundary.
const factory BridgeEvent.pttCapability({
/// Stable capability identifier (`"L0Focused"`,
/// `"L1GlobalShortcut"`, `"L2GlobalHoldToTalk"`,
/// `"L3GlobalWithMouseButtons"`, or `"L4DeviceAware"`).
required String level,
/// Stable backend identifier (e.g. `"focused"`).
required String backendId,
/// Coarse bound input class (e.g. `"keyboard"`,
/// `"mouse-side-button"`); empty when no binding is active.
required String boundInputClass,
}) = BridgeEvent_PttCapability;
/// Voice subsystem state snapshot (SDD-094). The Flutter
/// VoiceBar listens to this stream.
const factory BridgeEvent.voiceState({
/// True when the session is currently joined to a voice
/// channel and the audio engine is running.
required bool inChannel,
/// Active transmit mode.
required BridgeTransmitMode transmitMode,
/// True when the hard-mute clamp is engaged.
required bool mute,
/// Current release-tail in milliseconds (0..=500).
required int releaseTailMs,
/// Last confirmed authoritative channel id.
BigInt? currentChannelId,
/// Non-authoritative pending join target channel id.
BigInt? pendingTargetChannelId,
/// Whether a new join intent is currently allowed.
required bool canJoin,
/// Whether leave intent is currently allowed.
required bool canLeave,
/// Join projection sync state.
required BridgeVoiceJoinSyncState joinSyncState,
/// Last stable join error code, if any.
BridgeVoiceJoinErrorCode? joinErrorCode,
}) = BridgeEvent_VoiceState;
/// iOS audio interruption state (SDD-101).
const factory BridgeEvent.interruptionState({
/// True when interruption began, false when it ended.
required bool began,
/// Resume recommendation from the platform. False on begin.
required bool shouldResume,
}) = BridgeEvent_InterruptionState;
/// SDD-106 §5: resolved platform-level permission state. On
/// Android this is published by the JNI hook in
/// [`crate::permission_jni`] whenever
/// `AndroidPermissionRequester` reports a state transition
/// (initial grant, denial, permanent denial, or mid-session
/// revocation). The audio engine's `TransmitModeSelector`
/// observes the `RECORD_AUDIO` variant of this event as an
/// authoritative clamp on the transmit gate per SDD-106 §6
/// (and SRS-209's listen-only fail-safe).
///
/// Carries the canonical Android permission string in
/// `permission` (e.g. `"android.permission.RECORD_AUDIO"`)
/// and the resolved state in `state`. No raw user input,
/// timestamps, or other PII cross this boundary.
const factory BridgeEvent.permissionState({
/// Canonical Android permission identifier.
required String permission,
/// Resolved permission state.
required PermissionStateKind state,
}) = BridgeEvent_PermissionState;
/// A text message received from the server.
const factory BridgeEvent.chatMessage({
/// Client id of the sender.
required BigInt senderId,
/// Nickname of the sender.
required String senderName,
/// Message content.
required String message,
/// Target scope (server/channel/private/poke).
required BridgeMessageTarget target,
/// Poke notification strength, present only for poke messages.
BridgePokeStrength? pokeStrength,
}) = BridgeEvent_ChatMessage;
/// Human-readable server activity surfaced from protocol bookkeeping events.
const factory BridgeEvent.serverActivity({
/// TeamSpeak-style activity line.
required String message,
}) = BridgeEvent_ServerActivity;
/// Audio route changed (speaker/earpiece/BT/wired).
const factory BridgeEvent.audioRouteChanged({
/// New audio output route.
required BridgeAudioRoute route,
}) = BridgeEvent_AudioRouteChanged;
/// A client moved to a different channel.
const factory BridgeEvent.clientMoved({
/// Unique client identifier.
required BigInt clientId,
/// Destination channel.
required BigInt newChannelId,
}) = BridgeEvent_ClientMoved;
/// A new client connected.
const factory BridgeEvent.clientJoined({
/// Unique client identifier.
required BigInt clientId,
/// Channel the client joined.
required BigInt channelId,
/// Display nickname.
required String name,
/// Microphone muted state.
required bool inputMuted,
/// Speaker muted state.
required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery,
/// Client's talk power value.
required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted,
}) = BridgeEvent_ClientJoined;
/// A client disconnected.
const factory BridgeEvent.clientLeft({
/// Unique client identifier.
required BigInt clientId,
/// Display nickname at time of disconnect.
required String name,
}) = BridgeEvent_ClientLeft;
/// Client properties changed.
const factory BridgeEvent.clientUpdated({
/// Unique client identifier.
required BigInt clientId,
/// Microphone muted state.
required bool inputMuted,
/// Speaker muted state.
required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery,
/// Client's talk power value.
required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted,
}) = BridgeEvent_ClientUpdated;
/// A new channel appeared.
const factory BridgeEvent.channelAdded({
/// Unique channel identifier.
required BigInt id,
/// Parent channel ID.
required BigInt parent,
/// Channel name.
required String name,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
required PlatformInt64 order,
/// Whether the channel requires a password.
required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower,
}) = BridgeEvent_ChannelAdded;
/// A channel was deleted.
const factory BridgeEvent.channelRemoved({
/// Channel identifier.
required BigInt id,
}) = BridgeEvent_ChannelRemoved;
/// Channel properties changed.
const factory BridgeEvent.channelUpdated({
/// Unique channel identifier.
required BigInt id,
/// Channel name.
required String name,
/// Whether the channel requires a password.
required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower,
}) = BridgeEvent_ChannelUpdated;
}
/// Bridge iOS voice-processing mode.
enum BridgeIosVoiceProcessingMode {
/// Apple VoiceProcessingIO path.
platformVoiceProcessing,
}
@freezed
sealed class BridgeMessageTarget with _$BridgeMessageTarget {
const BridgeMessageTarget._();
/// Broadcast to entire server.
const factory BridgeMessageTarget.server() = BridgeMessageTarget_Server;
/// Broadcast to current channel.
const factory BridgeMessageTarget.channel() = BridgeMessageTarget_Channel;
/// Private message to a specific client.
const factory BridgeMessageTarget.client(BigInt field0) =
BridgeMessageTarget_Client;
/// Poke a specific client.
const factory BridgeMessageTarget.poke(BigInt field0) =
BridgeMessageTarget_Poke;
}
/// Coarse OS-reported network state. Mirrors
/// [`chanora_core::NetworkState`] across the bridge.
enum BridgeNetworkState {
/// No signal seen yet.
unknown,
/// OS reports a usable network.
online,
/// OS reports no network.
offline,
}
/// Bridge poke notification strength.
enum BridgePokeStrength {
/// Poke should be surfaced at full strength.
strong,
/// Poke is rate-limited but below overflow severity.
suppressed,
/// Poke remains suppressed after repeated suppressed pokes.
suppressedOverflow,
}
/// Persisted PTT binding display state for the UI.
class BridgePttBinding {
/// Stable input category string (`""`, `"keyboard"`, or
/// `"mouse-side-button"`).
final String inputClass;
/// Display-only key label; empty when no binding is active.
final String keyLabel;
const BridgePttBinding({required this.inputClass, required this.keyLabel});
@override
int get hashCode => inputClass.hashCode ^ keyLabel.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgePttBinding &&
runtimeType == other.runtimeType &&
inputClass == other.inputClass &&
keyLabel == other.keyLabel;
}
/// Privacy-safe PTT capability descriptor for the UI.
class BridgePttDescriptor {
/// Stable capability level name.
final String level;
/// Stable backend identifier.
final String backendId;
/// Coarse bound input class; empty when no binding is active.
final String boundInputClass;
const BridgePttDescriptor({
required this.level,
required this.backendId,
required this.boundInputClass,
});
@override
int get hashCode =>
level.hashCode ^ backendId.hashCode ^ boundInputClass.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgePttDescriptor &&
runtimeType == other.runtimeType &&
level == other.level &&
backendId == other.backendId &&
boundInputClass == other.boundInputClass;
}
/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings;
/// the bridge never carries raw key codes.
enum BridgePttInputClass {
/// No binding is active.
none,
/// A keyboard key.
keyboard,
/// A mouse side button (Mouse4 / Mouse5).
mouseSideButton,
}
/// Server snapshot as seen by Dart.
class BridgeSnapshot {
/// Server name.
final String serverName;
/// Welcome banner text.
final String welcomeMessage;
/// Server platform (e.g. "Linux").
final String platform;
/// Server version string.
final String version;
/// Channels currently known.
final List<BridgeChannel> channels;
/// Clients currently known.
final List<BridgeClient> clients;
/// Our own client id. Useful for the UI to highlight our row
/// in the client list and to know which channel we are in
/// without trusting the optimistic local state.
final BigInt ownClientId;
const BridgeSnapshot({
required this.serverName,
required this.welcomeMessage,
required this.platform,
required this.version,
required this.channels,
required this.clients,
required this.ownClientId,
});
@override
int get hashCode =>
serverName.hashCode ^
welcomeMessage.hashCode ^
platform.hashCode ^
version.hashCode ^
channels.hashCode ^
clients.hashCode ^
ownClientId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeSnapshot &&
runtimeType == other.runtimeType &&
serverName == other.serverName &&
welcomeMessage == other.welcomeMessage &&
platform == other.platform &&
version == other.version &&
channels == other.channels &&
clients == other.clients &&
ownClientId == other.ownClientId;
}
/// Voice transmit mode mirror (SDD-095). Schema-controlled enum;
/// the wire encoding matches [`chanora_core::TransmitMode::as_u8`].
enum BridgeTransmitMode {
/// Push-to-talk (default).
ptt,
/// Continuous transmission while in channel and not muted.
continuous,
/// Voice-activity detection — reserved per DEC-030; v1 behaves
/// as `Continuous`.
voiceActivity,
}
/// Bridge VAD backend.
enum BridgeVadBackend {
/// Silero ONNX VAD.
sileroOnnx,
/// WebRTC fallback VAD.
webrtcVad,
/// Debug energy VAD.
energyDebug,
/// VAD disabled.
disabled,
}
/// Bridge mirror of stable join error/status codes.
enum BridgeVoiceJoinErrorCode {
/// Duplicate same-target join intent was coalesced.
duplicateSameTargetCoalesced,
/// A different target was requested while one is already pending.
joinAlreadyPendingDifferentTarget,
/// Join denied by server policy/permission.
joinDenied,
/// Join failed due to protocol-level error.
joinProtocolFailure,
/// Join failed due to transport/network error.
joinNetworkFailure,
/// Join timed out awaiting confirmation.
joinTimeout,
/// Pending join was superseded by user leave.
joinSupersededByLeave,
/// Stale join outcome was ignored.
joinStaleOutcomeIgnored,
/// Authoritative membership reconciled to different channel.
joinReconciledDifferentChannel,
/// Join command was rejected before send acceptance.
joinCommandRejectedBeforeSend,
/// Join intent rejected while reducer synchronizing.
joinCannotStartWhileSynchronizing,
}
/// Bridge mirror of core join projection sync state.
enum BridgeVoiceJoinSyncState {
/// Reducer is ready to accept channel actions.
ready,
/// Reducer is waiting on initial snapshot reconciliation.
synchronizingInitialSnapshot,
/// Reducer is waiting on reconnect snapshot reconciliation.
synchronizingReconnect,
}
/// Schema-controlled mirror of the Kotlin
/// `AndroidPermissionRequester.PermissionState` sealed class
/// (SDD-106 §5). Crosses the bridge as an enum so the Dart side can
/// `switch` on it exhaustively without parsing strings.
enum PermissionStateKind {
/// Permission granted by the user; capture may proceed.
granted,
/// Permission denied (re-promptable).
denied,
/// Permission permanently denied — the UI is expected to
/// deep-link to system settings (SDD-106 §3).
permanentlyDenied,
/// Any state string that did not match the contract above.
/// Treated identically to `Denied` by the transmit clamp
/// (fail-safe per SRS-209).
unknown,
}