import 'dart:io' show Directory, File; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import '../src/rust/api.dart' as rust; const String appSemverBaseline = 'v0.1.0'; const String _sileroVadAsset = 'assets/models/silero_vad.onnx'; typedef StorageDirectoryProvider = Future Function(); typedef StorageInitializer = Future Function(String dir); Future? _storageInitFuture; Future? _cacheInitFuture; Future? _vadBootstrapFuture; StorageDirectoryProvider _storageDirectoryProvider = getApplicationSupportDirectory; StorageDirectoryProvider _cacheDirectoryProvider = getApplicationCacheDirectory; StorageInitializer _storageInitializer = _defaultStorageInitializer; StorageInitializer _cacheInitializer = _defaultCacheInitializer; Future _defaultStorageInitializer(String dir) { return rust.initStorage(dir: dir); } Future _defaultCacheInitializer(String dir) { return rust.initCache(dir: dir); } Future _copyBundledAssetToDocuments({ required String assetPath, required String fileName, }) async { final dir = await getApplicationDocumentsDirectory(); final file = File('${dir.path}/$fileName'); final data = await rootBundle.load(assetPath); final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); if (await file.exists() && await file.length() == bytes.length) { return file; } await file.writeAsBytes(bytes, flush: true); return file; } Future configureBundledVadModels() async { final existing = _vadBootstrapFuture; if (existing != null) { await existing; return; } final bootstrap = _configureBundledVadModelsImpl(); _vadBootstrapFuture = bootstrap; try { await bootstrap; } catch (_) { _vadBootstrapFuture = null; rethrow; } } Future _configureBundledVadModelsImpl() async { final silero = await _copyBundledAssetToDocuments( assetPath: _sileroVadAsset, fileName: 'silero_vad.onnx', ); await rust.setVadModelPath(path: silero.path); } /// Resolve the human-readable app version displayed in About. /// /// The semver baseline is kept in code because iOS strips pre-release /// identifiers from `CFBundleShortVersionString`; `package_info_plus` /// still provides the platform build counter. Future resolveAppVersion({ String semverBaseline = appSemverBaseline, }) async { try { final info = await PackageInfo.fromPlatform(); return appVersionFromBuildNumber( semverBaseline: semverBaseline, buildNumber: info.buildNumber, ); } catch (_) { return semverBaseline; } } String appVersionFromBuildNumber({ required String semverBaseline, required String buildNumber, }) { final build = buildNumber.isEmpty ? '' : '+$buildNumber'; return '$semverBaseline$build'; } Future wireStorage() async { final existing = _storageInitFuture; if (existing != null) { await existing; return; } final initFuture = _wireStorageImpl(); _storageInitFuture = initFuture; await initFuture; } Future _wireStorageImpl() async { var initialized = false; try { final dir = await _storageDirectoryProvider(); await _storageInitializer(dir.path); initialized = true; } catch (_) { // Best-effort; missing storage just means no identity persistence // and no bookmark list this session. } finally { if (!initialized) { _storageInitFuture = null; } } } Future wireCache() async { final existing = _cacheInitFuture; if (existing != null) { await existing; return; } final initFuture = _wireCacheImpl(); _cacheInitFuture = initFuture; await initFuture; } Future _wireCacheImpl() async { var initialized = false; try { final dir = await _cacheDirectoryProvider(); await _cacheInitializer(dir.path); initialized = true; } catch (_) { // Best-effort; missing cache just means protocol-owned assets are // re-downloaded this session. } finally { if (!initialized) { _cacheInitFuture = null; } } } @visibleForTesting void debugResetStorageBootstrap({ StorageDirectoryProvider? storageDirectoryProvider, StorageDirectoryProvider? cacheDirectoryProvider, StorageInitializer? storageInitializer, StorageInitializer? cacheInitializer, }) { _storageInitFuture = null; _cacheInitFuture = null; _vadBootstrapFuture = null; _storageDirectoryProvider = storageDirectoryProvider ?? getApplicationSupportDirectory; _cacheDirectoryProvider = cacheDirectoryProvider ?? getApplicationCacheDirectory; _storageInitializer = storageInitializer ?? _defaultStorageInitializer; _cacheInitializer = cacheInitializer ?? _defaultCacheInitializer; } rust.BridgeNetworkState _mapConnectivity(List results) { if (results.isEmpty) return rust.BridgeNetworkState.unknown; final allNone = results.every((r) => r == ConnectivityResult.none); if (allNone) return rust.BridgeNetworkState.offline; return rust.BridgeNetworkState.online; } Future wireConnectivity() async { final connectivity = Connectivity(); try { final initial = await connectivity.checkConnectivity(); rust.setNetworkState(state: _mapConnectivity(initial)); } catch (_) {} connectivity.onConnectivityChanged.listen((results) { rust.setNetworkState(state: _mapConnectivity(results)); }); }