import 'dart:async'; import 'package:flutter/foundation.dart'; import '../../data/models/app_entry.dart'; import '../../data/models/slot_assignment.dart'; import '../../data/models/slot_type.dart'; import '../../data/repositories/settings_repository.dart'; import '../../data/services/native_bridge.dart'; /// Owns installed apps, shortcut assignments and the default-home state. class LauncherController extends ChangeNotifier { LauncherController(this._platform, this._settings); final LauncherPlatform _platform; final SettingsRepository _settings; final Map _slots = {}; List _apps = const []; bool _appsLoaded = false; bool _loadingApps = false; Object? _appsError; DateTime? _lastAppsLoad; bool _defaultHome = false; bool _defaultHomeKnown = false; Timer? _stateTimer; bool _disposed = false; List get apps => _apps; bool get isLoadingApps => _loadingApps; bool get appsLoaded => _appsLoaded; Object? get appsError => _appsError; bool get isDefaultHome => _defaultHome; bool get defaultHomeKnown => _defaultHomeKnown; SlotAssignment? assignmentFor(SlotType slot) => _slots[slot]; /// Loads persisted choices synchronously, then refreshes platform state in /// the background so the first frame is not blocked by package enumeration. void initialize() { for (final slot in SlotType.values) { final assignment = _settings.slot(slot); if (assignment != null) _slots[slot] = assignment; } _notify(); unawaited(refreshDefaultHome()); unawaited(loadApps()); } Future loadApps({bool force = false}) async { if (_loadingApps) return; if (_appsLoaded && !force) return; _loadingApps = true; _appsError = null; _notify(); try { final loaded = await _platform.listApps(); _apps = List.unmodifiable(loaded); _appsLoaded = true; _lastAppsLoad = DateTime.now(); await _validateAssignments(); } catch (error) { _appsError = error; } finally { _loadingApps = false; _notify(); } } /// Called when the launcher returns to the foreground. The package list is /// refreshed at most once a minute unless [forceApps] is set. Future refresh({bool forceApps = false}) async { await refreshDefaultHome(); final last = _lastAppsLoad; if (forceApps || last == null || DateTime.now().difference(last).inSeconds > 60) { await loadApps(force: true); } } Future _validateAssignments() async { final installed = _apps .map((app) => '${app.packageName}/${app.activityName}') .toSet(); final missing = []; _slots.forEach((slot, assignment) { if (!installed.contains( '${assignment.packageName}/${assignment.activityName}', )) { missing.add(slot); } }); for (final slot in missing) { _slots.remove(slot); await _settings.clearSlot(slot); } } Future assign(SlotType slot, AppEntry app) async { final assignment = app.toAssignment(); _slots[slot] = assignment; await _settings.setSlot(slot, assignment); _notify(); } Future clearSlot(SlotType slot) async { _slots.remove(slot); await _settings.clearSlot(slot); _notify(); } Future launchSlot(SlotType slot) async { final assignment = _slots[slot]; if (assignment == null) return null; return launchAssignment(assignment); } Future launchAssignment(SlotAssignment assignment) async { try { await _platform.launchApp( packageName: assignment.packageName, activityName: assignment.activityName, ); return null; } on LauncherPlatformException catch (error) { return error.message; } catch (_) { return 'Could not open ${assignment.label.isEmpty ? 'this app' : assignment.label}.'; } } Future launchAppEntry(AppEntry app) async { try { await _platform.launchApp( packageName: app.packageName, activityName: app.activityName, ); return null; } on LauncherPlatformException catch (error) { return error.message; } catch (_) { return 'Could not open ${app.label}.'; } } Future refreshDefaultHome() async { try { _defaultHome = await _platform.isDefaultHome(); } catch (_) { _defaultHome = false; } _defaultHomeKnown = true; _notify(); } Future requestDefaultHome() async { final launched = await _platform.requestDefaultHome(); if (launched) { // The system dialog will pause us; refresh again after it closes. _stateTimer?.cancel(); _stateTimer = Timer(const Duration(seconds: 2), () { unawaited(refreshDefaultHome()); }); } return launched; } void _notify() { if (!_disposed) notifyListeners(); } @override void dispose() { _disposed = true; _stateTimer?.cancel(); super.dispose(); } }