feat(voice): harden Android audio and channel joins

This commit is contained in:
Edison Jwa
2026-05-19 01:58:07 +09:00
parent 29a553d4e1
commit 8c253f1d4d
23 changed files with 2948 additions and 363 deletions
+68 -48
View File
@@ -252,6 +252,9 @@ class _BetaHomeState extends State<_BetaHome> {
bool _hardMute = false;
int _releaseTailMs = 200;
BigInt? _currentVoiceChannelId;
BigInt? _pendingVoiceChannelId;
bool _canJoinVoiceChannel = true;
bool _canLeaveVoiceChannel = false;
String? _lostReason;
int? _reconnectAttempt;
@@ -312,7 +315,8 @@ class _BetaHomeState extends State<_BetaHome> {
bool _handleFocusedPttKey(KeyEvent event) {
final label = _pttDisplayLabelForKey(event.logicalKey);
final isBoundKey = _pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
final isBoundKey =
_pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) {
if (_focusedPttHeldKeys.remove(event.logicalKey)) {
unawaited(_setPtt(false));
@@ -423,6 +427,10 @@ class _BetaHomeState extends State<_BetaHome> {
:final transmitMode,
:final mute,
:final releaseTailMs,
:final currentChannelId,
:final pendingTargetChannelId,
:final canJoin,
:final canLeave,
):
setState(() {
_inChannel = inChannel;
@@ -430,6 +438,10 @@ class _BetaHomeState extends State<_BetaHome> {
_hardMute = mute;
_releaseTailMs = releaseTailMs;
_audioStarted = inChannel;
_currentVoiceChannelId = currentChannelId;
_pendingVoiceChannelId = pendingTargetChannelId;
_canJoinVoiceChannel = canJoin;
_canLeaveVoiceChannel = canLeave;
});
if (inChannel) {
_ensureStatsTimer();
@@ -438,7 +450,6 @@ class _BetaHomeState extends State<_BetaHome> {
_statsTimer?.cancel();
_statsTimer = null;
_releaseFocusedPttIfHeld();
setState(() => _currentVoiceChannelId = null);
}
if (transmitMode != rust.BridgeTransmitMode.ptt) {
_releaseFocusedPttIfHeld();
@@ -485,10 +496,7 @@ class _BetaHomeState extends State<_BetaHome> {
// remains the canonical Dart-side state holder (driven by
// the MethodChannel); a future revision may expose a
// setter so both paths converge on a single ValueNotifier.
case rust.BridgeEvent_PermissionState(
:final permission,
:final state,
):
case rust.BridgeEvent_PermissionState(:final permission, :final state):
debugPrint(
'bridge permission_state: permission=$permission state=$state',
);
@@ -568,27 +576,27 @@ class _BetaHomeState extends State<_BetaHome> {
void _showPermissionDeniedDialog(String rawError) {
final l10n = AppL10n.of(context);
final isNetwork =
rawError.contains('9987') || rawError.contains('connect');
final isNetwork = rawError.contains('9987') || rawError.contains('connect');
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title:
Text(isNetwork ? l10n.networkPermissionTitle : l10n.permissionDenied),
title: Text(
isNetwork ? l10n.networkPermissionTitle : l10n.permissionDenied,
),
content: Text(
isNetwork ? l10n.networkPermissionBody : l10n.microphonePermissionBody),
isNetwork
? l10n.networkPermissionBody
: l10n.microphonePermissionBody,
),
actions: [
if (Platform.isMacOS)
TextButton(
onPressed: () {
Navigator.pop(ctx);
try {
Process.run(
'open',
[
'x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork',
],
);
Process.run('open', [
'x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork',
]);
} catch (_) {}
},
child: Text(l10n.networkPermissionOpenSettings),
@@ -649,6 +657,8 @@ class _BetaHomeState extends State<_BetaHome> {
}
Future<void> _onJoinChannel(rust.BridgeChannel ch) async {
if (!_canJoinVoiceChannel) return;
if (_pendingVoiceChannelId != null) return;
if (ch.id == _currentVoiceChannelId) return;
final l10n = AppL10n.of(context);
final messenger = ScaffoldMessenger.of(context);
@@ -689,7 +699,7 @@ class _BetaHomeState extends State<_BetaHome> {
}
await rust.voiceJoin(channelId: ch.id, password: password ?? '');
if (!mounted) return;
setState(() => _currentVoiceChannelId = ch.id);
unawaited(_onRefresh());
} catch (e) {
if (!mounted) return;
// Surface as a SnackBar so the user sees it even while
@@ -753,10 +763,9 @@ class _BetaHomeState extends State<_BetaHome> {
// ignore: unused_element
Future<void> _onLeaveVoice() async {
if (!_canLeaveVoiceChannel) return;
try {
await rust.voiceLeave();
if (!mounted) return;
setState(() => _currentVoiceChannelId = null);
} catch (e) {
if (!mounted) return;
setState(() => _error = e.toString());
@@ -916,6 +925,9 @@ class _BetaHomeState extends State<_BetaHome> {
_outputMuted = false;
_inChannel = false;
_currentVoiceChannelId = null;
_pendingVoiceChannelId = null;
_canJoinVoiceChannel = true;
_canLeaveVoiceChannel = false;
});
_releaseFocusedPttIfHeld();
}
@@ -932,17 +944,6 @@ class _BetaHomeState extends State<_BetaHome> {
void _applySnapshot(rust.BridgeSnapshot snap) {
_snapshot = snap;
if (!_inChannel) {
_currentVoiceChannelId = null;
return;
}
for (final client in snap.clients) {
if (client.id == snap.ownClientId) {
_currentVoiceChannelId = client.channel;
return;
}
}
_currentVoiceChannelId = null;
}
Future<void> _onShowDiagnostics(BuildContext context) async {
@@ -1335,6 +1336,10 @@ class _BetaHomeState extends State<_BetaHome> {
final snapshotView = _SnapshotView(
snapshot: _snapshot!,
currentVoiceChannelId: _currentVoiceChannelId,
pendingVoiceChannelId: _pendingVoiceChannelId,
hasJoinPending: _pendingVoiceChannelId != null,
canJoinVoiceChannel: _canJoinVoiceChannel,
canLeaveVoiceChannel: _canLeaveVoiceChannel,
onJoinChannel: _onJoinChannel,
onLeaveVoice: _onLeaveVoice,
);
@@ -1346,8 +1351,7 @@ class _BetaHomeState extends State<_BetaHome> {
SizedBox(
width: voiceBarWidthWide,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
banner,
const SizedBox(height: 12),
@@ -1376,8 +1380,7 @@ class _BetaHomeState extends State<_BetaHome> {
onTap: () => _onOpenVoiceDetailsSheet(),
),
if (_inChannel &&
_transmitMode ==
rust.BridgeTransmitMode.ptt) ...[
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
const SizedBox(height: 8),
VoicePttButton(
active: _audioStats?.pttActive ?? false,
@@ -1410,7 +1413,12 @@ class _BetaHomeState extends State<_BetaHome> {
],
),
),
Expanded(child: Padding(padding: const EdgeInsets.all(16), child: bodyContent)),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: bodyContent,
),
),
],
),
),
@@ -1418,14 +1426,8 @@ class _BetaHomeState extends State<_BetaHome> {
}
return Scaffold(
appBar: AppBar(
title: headerTitle,
actions: headerActions,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: bodyContent,
),
appBar: AppBar(title: headerTitle, actions: headerActions),
body: Padding(padding: const EdgeInsets.all(16), child: bodyContent),
);
}
}
@@ -1991,12 +1993,20 @@ class _SnapshotView extends StatelessWidget {
const _SnapshotView({
required this.snapshot,
required this.currentVoiceChannelId,
required this.pendingVoiceChannelId,
required this.hasJoinPending,
required this.canJoinVoiceChannel,
required this.canLeaveVoiceChannel,
required this.onJoinChannel,
required this.onLeaveVoice,
});
final rust.BridgeSnapshot snapshot;
final BigInt? currentVoiceChannelId;
final BigInt? pendingVoiceChannelId;
final bool hasJoinPending;
final bool canJoinVoiceChannel;
final bool canLeaveVoiceChannel;
final ValueChanged<rust.BridgeChannel> onJoinChannel;
final VoidCallback onLeaveVoice;
@@ -2077,17 +2087,27 @@ class _SnapshotView extends StatelessWidget {
subtitle: Text('id=${ch.id} parent=${ch.parent}'),
trailing: IconButton(
icon: Icon(
ch.id == currentVoiceChannelId ? Icons.logout : Icons.login,
ch.id == currentVoiceChannelId
? Icons.logout
: ch.id == pendingVoiceChannelId
? Icons.hourglass_top
: Icons.login,
),
tooltip: ch.id == currentVoiceChannelId
? l10n.leaveChannelAction
: ch.id == pendingVoiceChannelId
? l10n.statusConnecting
: l10n.joinChannelAction,
onPressed: ch.id == currentVoiceChannelId
? onLeaveVoice
onPressed: hasJoinPending
? null
: ch.id == pendingVoiceChannelId
? null
: ch.id == currentVoiceChannelId
? (canLeaveVoiceChannel ? onLeaveVoice : null)
: () => onJoinChannel(ch),
),
selected: ch.id == currentVoiceChannelId,
onTap: ch.id == currentVoiceChannelId
onTap: hasJoinPending || !canJoinVoiceChannel || ch.id == currentVoiceChannelId
? null
: () => onJoinChannel(ch),
),
+42 -2
View File
@@ -9,8 +9,8 @@ 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 because they are not marked as `pub`: `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `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`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `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
@@ -454,6 +454,24 @@ sealed class BridgeEvent with _$BridgeEvent {
/// 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).
@@ -587,6 +605,28 @@ enum BridgeTransmitMode {
voiceActivity,
}
/// Bridge mirror of stable join error/status codes.
enum BridgeVoiceJoinErrorCode {
duplicateSameTargetCoalesced,
joinAlreadyPendingDifferentTarget,
joinDenied,
joinProtocolFailure,
joinNetworkFailure,
joinTimeout,
joinSupersededByLeave,
joinStaleOutcomeIgnored,
joinReconciledDifferentChannel,
joinCommandRejectedBeforeSend,
joinCannotStartWhileSynchronizing,
}
/// Bridge mirror of core join projection sync state.
enum BridgeVoiceJoinSyncState {
ready,
synchronizingInitialSnapshot,
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
@@ -146,7 +146,7 @@ return permissionState(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,required TResult orElse(),}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -157,7 +157,7 @@ return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState() when interruptionState != null:
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case _:
return orElse();
@@ -177,7 +177,7 @@ return permissionState(_that.permission,_that.state);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,}) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected():
return connected(_that.serverName);case BridgeEvent_Lost():
@@ -188,7 +188,7 @@ return audioStarted();case BridgeEvent_AudioStopped():
return audioStopped();case BridgeEvent_SnapshotChanged():
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability():
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState():
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState():
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState():
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState():
return permissionState(_that.permission,_that.state);}
}
@@ -204,7 +204,7 @@ return permissionState(_that.permission,_that.state);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,}) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,}) {final _that = this;
switch (_that) {
case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -215,7 +215,7 @@ return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState() when interruptionState != null:
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case _:
return null;
@@ -710,7 +710,7 @@ as String,
class BridgeEvent_VoiceState extends BridgeEvent {
const BridgeEvent_VoiceState({required this.inChannel, required this.transmitMode, required this.mute, required this.releaseTailMs}): super._();
const BridgeEvent_VoiceState({required this.inChannel, required this.transmitMode, required this.mute, required this.releaseTailMs, this.currentChannelId, this.pendingTargetChannelId, required this.canJoin, required this.canLeave, required this.joinSyncState, this.joinErrorCode}): super._();
/// True when the session is currently joined to a voice
@@ -722,6 +722,18 @@ class BridgeEvent_VoiceState extends BridgeEvent {
final bool mute;
/// Current release-tail in milliseconds (0..=500).
final int releaseTailMs;
/// Last confirmed authoritative channel id.
final BigInt? currentChannelId;
/// Non-authoritative pending join target channel id.
final BigInt? pendingTargetChannelId;
/// Whether a new join intent is currently allowed.
final bool canJoin;
/// Whether leave intent is currently allowed.
final bool canLeave;
/// Join projection sync state.
final BridgeVoiceJoinSyncState joinSyncState;
/// Last stable join error code, if any.
final BridgeVoiceJoinErrorCode? joinErrorCode;
/// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values.
@@ -733,16 +745,16 @@ $BridgeEvent_VoiceStateCopyWith<BridgeEvent_VoiceState> get copyWith => _$Bridge
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_VoiceState&&(identical(other.inChannel, inChannel) || other.inChannel == inChannel)&&(identical(other.transmitMode, transmitMode) || other.transmitMode == transmitMode)&&(identical(other.mute, mute) || other.mute == mute)&&(identical(other.releaseTailMs, releaseTailMs) || other.releaseTailMs == releaseTailMs));
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_VoiceState&&(identical(other.inChannel, inChannel) || other.inChannel == inChannel)&&(identical(other.transmitMode, transmitMode) || other.transmitMode == transmitMode)&&(identical(other.mute, mute) || other.mute == mute)&&(identical(other.releaseTailMs, releaseTailMs) || other.releaseTailMs == releaseTailMs)&&(identical(other.currentChannelId, currentChannelId) || other.currentChannelId == currentChannelId)&&(identical(other.pendingTargetChannelId, pendingTargetChannelId) || other.pendingTargetChannelId == pendingTargetChannelId)&&(identical(other.canJoin, canJoin) || other.canJoin == canJoin)&&(identical(other.canLeave, canLeave) || other.canLeave == canLeave)&&(identical(other.joinSyncState, joinSyncState) || other.joinSyncState == joinSyncState)&&(identical(other.joinErrorCode, joinErrorCode) || other.joinErrorCode == joinErrorCode));
}
@override
int get hashCode => Object.hash(runtimeType,inChannel,transmitMode,mute,releaseTailMs);
int get hashCode => Object.hash(runtimeType,inChannel,transmitMode,mute,releaseTailMs,currentChannelId,pendingTargetChannelId,canJoin,canLeave,joinSyncState,joinErrorCode);
@override
String toString() {
return 'BridgeEvent.voiceState(inChannel: $inChannel, transmitMode: $transmitMode, mute: $mute, releaseTailMs: $releaseTailMs)';
return 'BridgeEvent.voiceState(inChannel: $inChannel, transmitMode: $transmitMode, mute: $mute, releaseTailMs: $releaseTailMs, currentChannelId: $currentChannelId, pendingTargetChannelId: $pendingTargetChannelId, canJoin: $canJoin, canLeave: $canLeave, joinSyncState: $joinSyncState, joinErrorCode: $joinErrorCode)';
}
@@ -753,7 +765,7 @@ abstract mixin class $BridgeEvent_VoiceStateCopyWith<$Res> implements $BridgeEve
factory $BridgeEvent_VoiceStateCopyWith(BridgeEvent_VoiceState value, $Res Function(BridgeEvent_VoiceState) _then) = _$BridgeEvent_VoiceStateCopyWithImpl;
@useResult
$Res call({
bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs
bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode
});
@@ -770,13 +782,19 @@ class _$BridgeEvent_VoiceStateCopyWithImpl<$Res>
/// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? inChannel = null,Object? transmitMode = null,Object? mute = null,Object? releaseTailMs = null,}) {
@pragma('vm:prefer-inline') $Res call({Object? inChannel = null,Object? transmitMode = null,Object? mute = null,Object? releaseTailMs = null,Object? currentChannelId = freezed,Object? pendingTargetChannelId = freezed,Object? canJoin = null,Object? canLeave = null,Object? joinSyncState = null,Object? joinErrorCode = freezed,}) {
return _then(BridgeEvent_VoiceState(
inChannel: null == inChannel ? _self.inChannel : inChannel // ignore: cast_nullable_to_non_nullable
as bool,transmitMode: null == transmitMode ? _self.transmitMode : transmitMode // ignore: cast_nullable_to_non_nullable
as BridgeTransmitMode,mute: null == mute ? _self.mute : mute // ignore: cast_nullable_to_non_nullable
as bool,releaseTailMs: null == releaseTailMs ? _self.releaseTailMs : releaseTailMs // ignore: cast_nullable_to_non_nullable
as int,
as int,currentChannelId: freezed == currentChannelId ? _self.currentChannelId : currentChannelId // ignore: cast_nullable_to_non_nullable
as BigInt?,pendingTargetChannelId: freezed == pendingTargetChannelId ? _self.pendingTargetChannelId : pendingTargetChannelId // ignore: cast_nullable_to_non_nullable
as BigInt?,canJoin: null == canJoin ? _self.canJoin : canJoin // ignore: cast_nullable_to_non_nullable
as bool,canLeave: null == canLeave ? _self.canLeave : canLeave // ignore: cast_nullable_to_non_nullable
as bool,joinSyncState: null == joinSyncState ? _self.joinSyncState : joinSyncState // ignore: cast_nullable_to_non_nullable
as BridgeVoiceJoinSyncState,joinErrorCode: freezed == joinErrorCode ? _self.joinErrorCode : joinErrorCode // ignore: cast_nullable_to_non_nullable
as BridgeVoiceJoinErrorCode?,
));
}
@@ -1111,6 +1111,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_bridge_bookmark(raw);
}
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_bridge_voice_join_error_code(raw);
}
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_u_64(raw);
}
@protected
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1232,6 +1246,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
transmitMode: dco_decode_bridge_transmit_mode(raw[2]),
mute: dco_decode_bool(raw[3]),
releaseTailMs: dco_decode_u_32(raw[4]),
currentChannelId: dco_decode_opt_box_autoadd_u_64(raw[5]),
pendingTargetChannelId: dco_decode_opt_box_autoadd_u_64(raw[6]),
canJoin: dco_decode_bool(raw[7]),
canLeave: dco_decode_bool(raw[8]),
joinSyncState: dco_decode_bridge_voice_join_sync_state(raw[9]),
joinErrorCode:
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(raw[10]),
);
case 9:
return BridgeEvent_InterruptionState(
@@ -1283,6 +1304,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeTransmitMode.values[raw as int];
}
@protected
BridgeVoiceJoinErrorCode dco_decode_bridge_voice_join_error_code(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return BridgeVoiceJoinErrorCode.values[raw as int];
}
@protected
BridgeVoiceJoinSyncState dco_decode_bridge_voice_join_sync_state(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return BridgeVoiceJoinSyncState.values[raw as int];
}
@protected
double dco_decode_f_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1325,6 +1362,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as Uint8List;
}
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null
? null
: dco_decode_box_autoadd_bridge_voice_join_error_code(raw);
}
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_box_autoadd_u_64(raw);
}
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1415,6 +1467,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_bridge_bookmark(deserializer));
}
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_bridge_voice_join_error_code(deserializer));
}
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_u_64(deserializer));
}
@protected
BridgeAudioStats sse_decode_bridge_audio_stats(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -1554,11 +1620,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_transmitMode = sse_decode_bridge_transmit_mode(deserializer);
var var_mute = sse_decode_bool(deserializer);
var var_releaseTailMs = sse_decode_u_32(deserializer);
var var_currentChannelId = sse_decode_opt_box_autoadd_u_64(
deserializer,
);
var var_pendingTargetChannelId = sse_decode_opt_box_autoadd_u_64(
deserializer,
);
var var_canJoin = sse_decode_bool(deserializer);
var var_canLeave = sse_decode_bool(deserializer);
var var_joinSyncState = sse_decode_bridge_voice_join_sync_state(
deserializer,
);
var var_joinErrorCode =
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
deserializer,
);
return BridgeEvent_VoiceState(
inChannel: var_inChannel,
transmitMode: var_transmitMode,
mute: var_mute,
releaseTailMs: var_releaseTailMs,
currentChannelId: var_currentChannelId,
pendingTargetChannelId: var_pendingTargetChannelId,
canJoin: var_canJoin,
canLeave: var_canLeave,
joinSyncState: var_joinSyncState,
joinErrorCode: var_joinErrorCode,
);
case 9:
var var_began = sse_decode_bool(deserializer);
@@ -1627,6 +1714,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeTransmitMode.values[inner];
}
@protected
BridgeVoiceJoinErrorCode sse_decode_bridge_voice_join_error_code(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return BridgeVoiceJoinErrorCode.values[inner];
}
@protected
BridgeVoiceJoinSyncState sse_decode_bridge_voice_join_sync_state(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return BridgeVoiceJoinSyncState.values[inner];
}
@protected
double sse_decode_f_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -1694,6 +1799,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getUint8List(len_);
}
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_bridge_voice_join_error_code(
deserializer,
));
} else {
return null;
}
}
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_u_64(deserializer));
} else {
return null;
}
}
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -1794,6 +1926,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_bridge_bookmark(self, serializer);
}
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bridge_voice_join_error_code(self, serializer);
}
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_64(self, serializer);
}
@protected
void sse_encode_bridge_audio_stats(
BridgeAudioStats self,
@@ -1909,12 +2056,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
transmitMode: final transmitMode,
mute: final mute,
releaseTailMs: final releaseTailMs,
currentChannelId: final currentChannelId,
pendingTargetChannelId: final pendingTargetChannelId,
canJoin: final canJoin,
canLeave: final canLeave,
joinSyncState: final joinSyncState,
joinErrorCode: final joinErrorCode,
):
sse_encode_i_32(8, serializer);
sse_encode_bool(inChannel, serializer);
sse_encode_bridge_transmit_mode(transmitMode, serializer);
sse_encode_bool(mute, serializer);
sse_encode_u_32(releaseTailMs, serializer);
sse_encode_opt_box_autoadd_u_64(currentChannelId, serializer);
sse_encode_opt_box_autoadd_u_64(pendingTargetChannelId, serializer);
sse_encode_bool(canJoin, serializer);
sse_encode_bool(canLeave, serializer);
sse_encode_bridge_voice_join_sync_state(joinSyncState, serializer);
sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
joinErrorCode,
serializer,
);
case BridgeEvent_InterruptionState(
began: final began,
shouldResume: final shouldResume,
@@ -1974,6 +2136,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_bridge_voice_join_sync_state(
BridgeVoiceJoinSyncState self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected
void sse_encode_f_32(double self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -2038,6 +2218,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer.buffer.putUint8List(self);
}
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_bridge_voice_join_error_code(self, serializer);
}
}
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_u_64(self, serializer);
}
}
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -36,6 +36,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeBookmark dco_decode_box_autoadd_bridge_bookmark(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
);
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw);
@protected
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw);
@@ -66,6 +74,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeTransmitMode dco_decode_bridge_transmit_mode(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_bridge_voice_join_error_code(dynamic raw);
@protected
BridgeVoiceJoinSyncState dco_decode_bridge_voice_join_sync_state(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@@ -87,6 +101,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -127,6 +148,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@protected
BridgeAudioStats sse_decode_bridge_audio_stats(SseDeserializer deserializer);
@@ -163,6 +192,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinSyncState sse_decode_bridge_voice_join_sync_state(
SseDeserializer deserializer,
);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@@ -190,6 +229,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -241,6 +289,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_bridge_audio_stats(
BridgeAudioStats self,
@@ -289,6 +346,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_voice_join_sync_state(
BridgeVoiceJoinSyncState self,
SseSerializer serializer,
);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@@ -322,6 +391,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -38,6 +38,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeBookmark dco_decode_box_autoadd_bridge_bookmark(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw,
);
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw);
@protected
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw);
@@ -68,6 +76,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BridgeTransmitMode dco_decode_bridge_transmit_mode(dynamic raw);
@protected
BridgeVoiceJoinErrorCode dco_decode_bridge_voice_join_error_code(dynamic raw);
@protected
BridgeVoiceJoinSyncState dco_decode_bridge_voice_join_sync_state(dynamic raw);
@protected
double dco_decode_f_32(dynamic raw);
@@ -89,6 +103,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -129,6 +150,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@protected
BridgeAudioStats sse_decode_bridge_audio_stats(SseDeserializer deserializer);
@@ -165,6 +194,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinErrorCode sse_decode_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BridgeVoiceJoinSyncState sse_decode_bridge_voice_join_sync_state(
SseDeserializer deserializer,
);
@protected
double sse_decode_f_32(SseDeserializer deserializer);
@@ -192,6 +231,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer,
);
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer,
@@ -243,6 +291,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_bridge_audio_stats(
BridgeAudioStats self,
@@ -291,6 +348,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self,
SseSerializer serializer,
);
@protected
void sse_encode_bridge_voice_join_sync_state(
BridgeVoiceJoinSyncState self,
SseSerializer serializer,
);
@protected
void sse_encode_f_32(double self, SseSerializer serializer);
@@ -324,6 +393,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_permission_state_kind(
PermissionStateKind self,
@@ -353,9 +353,9 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
final levelActive = stats?.pttActive ?? false;
final isPtt = _mode == rust.BridgeTransmitMode.ptt;
// Route picker only meaningful on iOS + Android where the OS
// owns audio routing. Desktop hosts skip the tile entirely.
final showRoutePicker = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
// Route picker is iOS-only; the picker uses AVAudioSession-backed
// widgets that must not be constructed on Android.
final showRoutePicker = !kIsWeb && Platform.isIOS;
return SafeArea(
child: SingleChildScrollView(