feat: Android Oboe voice backend — WebRTC APM, VAD, HW/SW toggle, BBCode welcome, link trust, foreground task
Audio engine (Rust): - Android Oboe: WebRTC APM (AEC/NS/AGC/HPF) + TEN/Silero ONNX VAD - Hardware effects (JNI) with software fallback per-effect - Render reference buffer for AEC between output/capture callbacks - Voice activity gate: suppress transmission when speaker muted (all platforms) - Audio focus (SDD-109) + Bluetooth SCO (SDD-110) via JNI - ONNX Runtime 1.26 via ort 2.0.0-rc.12 (down from rc.10, ndarray 0.17) - VAD worker channel capacity 8→32, initial seq u64::MAX (warm-up fix) - TEN VAD default backend (was Silero) - Platform→WebrtcApm resolution after hardware binding - oboe-rs edisonjwa fork with get_raw_session_id() Android Kotlin: - AndroidAudioFocusController + AndroidBluetoothScoController - AndroidAudioLifecycleController (route changes to Flutter) - ProGuard rules for new controllers Flutter UI: - VoiceSettings: Android HW/SW toggle (Platform auto / WebRTC APM) - VoiceStatusChip: mute warning border + Speaker muted label - BBCode welcome message parser (BbCodeText, case-insensitive) - Welcome message foldable (expanded by default) - Link trust dialog (domain wildcards, SharedPreferences) - HapticFeedback on voice sheet opener - Server name in AppBar, version v0.1.0 - Default channel (id=1) visible, serverquery clients hidden - flutter_foreground_task integration Config: - ort load-dynamic on all non-iOS (Android/Linux/Windows) - ONNX Runtime AAR 1.26.0 - ndarray moved to common deps (was Apple-only)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LinkTrustService extends ChangeNotifier {
|
||||
static LinkTrustService? _instance;
|
||||
final Set<String> _trusted = {};
|
||||
bool _loaded = false;
|
||||
|
||||
static LinkTrustService get instance {
|
||||
_instance ??= LinkTrustService._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
LinkTrustService._() {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
if (_loaded) return;
|
||||
_loaded = true;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final domains = prefs.getStringList('trusted_domains') ?? [];
|
||||
_trusted.addAll(domains);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isTrusted(String host) {
|
||||
host = host.toLowerCase();
|
||||
for (final pattern in _trusted) {
|
||||
if (_matches(host, pattern)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> addTrustedDomain(String host) async {
|
||||
host = host.toLowerCase();
|
||||
_trusted.add(host);
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList('trusted_domains', _trusted.toList());
|
||||
}
|
||||
|
||||
bool _matches(String host, String pattern) {
|
||||
if (pattern.startsWith('*.')) {
|
||||
final suffix = pattern.substring(2);
|
||||
return host == suffix || host.endsWith('.$suffix');
|
||||
}
|
||||
return host == pattern;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> showLinkTrustDialog(BuildContext context, String domain) async {
|
||||
bool remember = false;
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) => AlertDialog(
|
||||
title: const Text('Open external link?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('You are about to open a link to:\n\n$domain'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(
|
||||
value: remember,
|
||||
onChanged: (v) => setDialogState(() => remember = v ?? false),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Flexible(
|
||||
child: Text('Trust all links from this domain'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(null),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(remember),
|
||||
child: const Text('Open'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user