Files
chanora/apps/chanora_flutter/lib/src/rust/api.dart
T
EdisonJwa 7966a7c8c6 feat(bridge,android): BridgeEvent::PermissionState + JNI publish hook + c++_shared link
Per SDD-106 §5 add BridgeEvent::PermissionState{permission, state}
with the PermissionStateKind enum (Granted, Denied, PermanentlyDenied,
Unknown). The Kotlin side publishes mid-session permission changes
through a new JNI entry point Java_app_chanora_chanora_1flutter
_MainActivity_publishPermissionState routed by the new
permission_jni.rs module; the Rust audio engine subscribes and
authoritatively clamps the transmit gate (see SDD-106 §6).

Adds crates/chanora_bridge/build.rs to emit
cargo:rustc-link-lib=dylib=c++_shared on Android so libchanora_bridge
.so carries DT_NEEDED libc++_shared.so; this is required by Android
API 24+ per-library linker namespaces to resolve __cxa_pure_virtual
and friends at System.loadLibrary time.

Includes the FRB-regenerated Dart counterparts so each commit is
independently buildable.

Trace: SDD-105, SDD-106 §5, SDD-118 item 6 (extended).
2026-05-18 12:32:01 +08:00

610 lines
21 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`: `log_file_path`, `log_sink`, `open_log_file`, `permission_events`, `publish_permission_state`, `runtime`, `session`, `transmit_mode_from_u8`
// 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`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `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,
);
/// Re-fetch a fresh snapshot from the active connection.
Future<BridgeSnapshot> snapshot() => RustLib.instance.api.crateApiSnapshot();
/// 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() => RustLib.instance.api.crateApiHandleRouteChange();
/// 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 push-to-talk state.
///
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
/// dialog. Retained so legacy callers and integration tests keep
/// working; the new VoiceBar UI no longer invokes this.
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. Returns a
/// `(level, backend_id, bound_input_class)` triple matching the
/// privacy-safe `BridgeEvent::PttCapability` event shape; useful
/// for the initial UI render before the first event arrives.
Future<(String, String, String)> pttDescriptor() =>
RustLib.instance.api.crateApiPttDescriptor();
/// Return the persisted PTT binding as a
/// `(input_class, platform_key)` pair 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<(String, String)> 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);
/// 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);
/// 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();
/// 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;
const BridgeAudioStats({
required this.framesSent,
required this.framesReceived,
required this.pttActive,
});
@override
int get hashCode =>
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioStats &&
runtimeType == other.runtimeType &&
framesSent == other.framesSent &&
framesReceived == other.framesReceived &&
pttActive == other.pttActive;
}
/// 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;
const BridgeChannel({
required this.id,
required this.parent,
required this.name,
required this.order,
});
@override
int get hashCode =>
id.hashCode ^ parent.hashCode ^ name.hashCode ^ order.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;
}
/// 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 for TeamSpeak ServerQuery clients.
final bool isServerQuery;
const BridgeClient({
required this.id,
required this.channel,
required this.name,
required this.isServerQuery,
});
@override
int get hashCode =>
id.hashCode ^ channel.hashCode ^ name.hashCode ^ isServerQuery.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeClient &&
runtimeType == other.runtimeType &&
id == other.id &&
channel == other.channel &&
name == other.name &&
isServerQuery == other.isServerQuery;
}
@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;
/// Snapshot probe observed a change in channel/client counts.
/// UI uses this to drive an auto-refresh without active
/// polling.
const factory BridgeEvent.snapshotChanged({
/// Latest channel count.
required int channels,
/// Latest client count.
required int clients,
}) = BridgeEvent_SnapshotChanged;
/// 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,
}) = 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;
}
/// 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,
}
/// 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,
}
/// 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,
}