fix(android): clear permission-owned hard mute

This commit is contained in:
Edison Jwa
2026-05-20 14:52:34 +09:00
parent eb1f4b895e
commit 30ff955439
+39 -6
View File
@@ -251,6 +251,7 @@ class _BetaHomeState extends State<_BetaHome> {
rust.BridgeTransmitMode _transmitMode = rust.BridgeTransmitMode.ptt;
bool _hardMute = false;
bool _hardMuteByPermission = false;
bool _permissionHardMuteClearInFlight = false;
int _releaseTailMs = 200;
BigInt? _currentVoiceChannelId;
BigInt? _pendingVoiceChannelId;
@@ -310,10 +311,38 @@ class _BetaHomeState extends State<_BetaHome> {
// events as early as possible so the listen-only banner reflects
// the system state on first frame.
_androidPermissions.start();
_androidPermissions.recordAudioState.addListener(
_onRecordAudioPermissionChanged,
);
unawaited(_reloadBookmarks());
unawaited(_hydratePttBinding());
}
void _onRecordAudioPermissionChanged() {
if (_androidPermissions.recordAudioState.value ==
AndroidRecordAudioPermissionState.granted) {
unawaited(_clearPermissionHardMute());
}
}
Future<void> _clearPermissionHardMute() async {
if (!_hardMuteByPermission || _permissionHardMuteClearInFlight) return;
_permissionHardMuteClearInFlight = true;
try {
await rust.setHardMute(muted: false);
if (!mounted || !_hardMuteByPermission) return;
setState(() {
_hardMute = false;
_hardMuteByPermission = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _error = e.toString());
} finally {
_permissionHardMuteClearInFlight = false;
}
}
bool _handleFocusedPttKey(KeyEvent event) {
final label = _pttDisplayLabelForKey(event.logicalKey);
final isBoundKey =
@@ -492,16 +521,17 @@ class _BetaHomeState extends State<_BetaHome> {
// SDD-106 §5/§6 / SRS-209: defensive observer of the
// authoritative Rust-side permission stream. The transmit
// clamp is already applied inside `chanora_bridge` before
// this event is broadcast; here we merely surface the
// event so the UI stays consistent if the MethodChannel
// path is ever delayed. The existing `AndroidPermissionsService`
// 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.
// this event is broadcast. When permission later becomes
// granted, release only the permission-owned hard-mute; do
// not touch server-side input mute, which may be user-owned.
case rust.BridgeEvent_PermissionState(:final permission, :final state):
debugPrint(
'bridge permission_state: permission=$permission state=$state',
);
if (permission == 'android.permission.RECORD_AUDIO' &&
state == rust.PermissionStateKind.granted) {
unawaited(_clearPermissionHardMute());
}
}
}
@@ -533,6 +563,9 @@ class _BetaHomeState extends State<_BetaHome> {
_hostCtl.dispose();
_nickCtl.dispose();
_passwordCtl.dispose();
_androidPermissions.recordAudioState.removeListener(
_onRecordAudioPermissionChanged,
);
// SDD-106: detach the Kotlin -> Dart MethodChannel handler so a
// late invokeMethod from the platform side cannot land on this
// disposed state.