fix(audio,storage,ui): allow PTT binding before audio is running

Save-binding before joining a voice channel used to return
BridgeError.invalidCommand(audio not started) because the
PttController only exists after start_audio runs and
set_ptt_binding required a live controller. Users naturally want
to bind their PTT key once on first launch, not every time they
join a channel — fix:

* chanora_storage::IdentityFileStore::set_ptt_binding /
  get_ptt_binding persist the privacy-safe binding triple
  (input_class, platform_key, key_label) into audio_meta.json
  next to transmit_mode and release_tail_ms.
* ChanoraSession holds pending_binding: Arc<Mutex<Option<PttBinding>>>.
  set_ptt_binding now (1) persists to storage best-effort, (2)
  stashes into pending_binding, (3) forwards live to the
  controller only if one exists. No more AudioNotStarted.
* init_storage loads the persisted binding into pending_binding
  so it survives app restarts.
* start_audio applies pending_binding immediately after constructing
  the PttController so the first key-press after join already works.
* supervisor_loop carries pending_binding and re-applies it after
  any reconnect-driven audio engine restart, so reconnects don't
  silently drop the hotkey.
* New bridge call get_ptt_binding() -> (input_class, key_label) plus
  a matching Flutter _hydratePttBinding() in initState lets the
  Voice Bar show the user's saved hotkey label on launch (e.g.
  'PTT: Space') before any voice channel is joined.

cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
FRB bindings regenerated.
This commit is contained in:
EdisonJwa
2026-05-15 23:45:20 +08:00
parent 6a41a0b4db
commit 8cd919cffc
9 changed files with 394 additions and 55 deletions
+21
View File
@@ -145,6 +145,27 @@ class _BetaHomeState extends State<_BetaHome> {
super.initState();
_eventsSub = rust.eventsStream().listen(_onEvent);
unawaited(_reloadBookmarks());
unawaited(_hydratePttBinding());
}
/// Hydrate the bound-key display state from the bridge so the
/// Voice Bar shows the user's persisted hotkey label immediately
/// at launch — without having to wait for them to open the
/// binding dialog. The bridge returns empty strings when no
/// binding has ever been saved.
Future<void> _hydratePttBinding() async {
try {
final (inputClass, keyLabel) = await rust.getPttBinding();
if (!mounted) return;
if (keyLabel.isNotEmpty || inputClass.isNotEmpty) {
setState(() {
_pttBoundKeyLabel = keyLabel;
_pttBoundInputClass = inputClass;
});
}
} catch (_) {
// No persisted binding or storage not yet wired; not an error.
}
}
Future<void> _reloadBookmarks() async {
@@ -105,6 +105,14 @@ Future<void> setPttBinding({
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.
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0';
@override
int get rustContentHash => 651188866;
int get rustContentHash => 1306308591;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -99,6 +99,8 @@ abstract class RustLibApi extends BaseApi {
String crateApiExportDiagnostics();
Future<(String, String)> crateApiGetPttBinding();
Future<int> crateApiGetReleaseTailMs();
Future<BridgeTransmitMode> crateApiGetTransmitMode();
@@ -387,7 +389,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
const TaskConstMeta(debugName: "export_diagnostics", argNames: []);
@override
Future<int> crateApiGetReleaseTailMs() {
Future<(String, String)> crateApiGetPttBinding() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
@@ -399,6 +401,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_record_string_string,
decodeErrorData: null,
),
constMeta: kCrateApiGetPttBindingConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiGetPttBindingConstMeta =>
const TaskConstMeta(debugName: "get_ptt_binding", argNames: []);
@override
Future<int> crateApiGetReleaseTailMs() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 10,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_u_32,
decodeErrorData: null,
@@ -422,7 +451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 10,
funcId: 11,
port: port_,
);
},
@@ -450,7 +479,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 11,
funcId: 12,
port: port_,
);
},
@@ -477,7 +506,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 12,
funcId: 13,
port: port_,
);
},
@@ -504,7 +533,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 13,
funcId: 14,
port: port_,
);
},
@@ -528,7 +557,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask(
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_String,
@@ -558,7 +587,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 15,
funcId: 16,
port: port_,
);
},
@@ -587,7 +616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 16,
funcId: 17,
port: port_,
);
},
@@ -615,7 +644,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 17,
funcId: 18,
port: port_,
);
},
@@ -643,7 +672,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 18,
funcId: 19,
port: port_,
);
},
@@ -668,7 +697,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_network_state(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!;
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!;
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
@@ -694,7 +723,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 20,
funcId: 21,
port: port_,
);
},
@@ -722,7 +751,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 21,
funcId: 22,
port: port_,
);
},
@@ -750,7 +779,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 22,
funcId: 23,
port: port_,
);
},
@@ -782,7 +811,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 23,
funcId: 24,
port: port_,
);
},
@@ -812,7 +841,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 24,
funcId: 25,
port: port_,
);
},
@@ -840,7 +869,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 25,
funcId: 26,
port: port_,
);
},
@@ -867,7 +896,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 26,
funcId: 27,
port: port_,
);
},
@@ -895,7 +924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 27,
funcId: 28,
port: port_,
);
},
@@ -927,7 +956,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 28,
funcId: 29,
port: port_,
);
},
@@ -956,7 +985,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 29,
funcId: 30,
port: port_,
);
},
@@ -1203,6 +1232,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as Uint8List;
}
@protected
(String, String) dco_decode_record_string_string(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 2) {
throw Exception('Expected 2 elements, got ${arr.length}');
}
return (dco_decode_String(arr[0]), dco_decode_String(arr[1]));
}
@protected
(String, String, String) dco_decode_record_string_string_string(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1530,6 +1569,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return deserializer.buffer.getUint8List(len_);
}
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_field0 = sse_decode_String(deserializer);
var var_field1 = sse_decode_String(deserializer);
return (var_field0, var_field1);
}
@protected
(String, String, String) sse_decode_record_string_string_string(
SseDeserializer deserializer,
@@ -1835,6 +1884,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
serializer.buffer.putUint8List(self);
}
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.$1, serializer);
sse_encode_String(self.$2, serializer);
}
@protected
void sse_encode_record_string_string_string(
(String, String, String) self,
@@ -87,6 +87,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
(String, String, String) dco_decode_record_string_string_string(dynamic raw);
@@ -184,6 +187,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
(String, String, String) sse_decode_record_string_string_string(
SseDeserializer deserializer,
@@ -306,6 +314,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string_string(
(String, String, String) self,
@@ -89,6 +89,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
(String, String, String) dco_decode_record_string_string_string(dynamic raw);
@@ -186,6 +189,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
(String, String, String) sse_decode_record_string_string_string(
SseDeserializer deserializer,
@@ -308,6 +316,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string_string(
(String, String, String) self,