diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index a383722e..6bee138f 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'10768bd2c880edd40f82ed49532d9e2e2043e61f'; +String _$tabRepositoryHash() => r'9bdd6ddc0254c830b783eb2a987d98d4f1d313fb'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.dart b/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.dart new file mode 100644 index 00000000..b25e2890 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'lifecycle.g.dart'; + +@Riverpod(keepAlive: true) +class BrowserViewLifecycle extends _$BrowserViewLifecycle { + // ignore: use_setters_to_change_properties + void update(AppLifecycleState? newState) { + state = newState; + } + + @override + AppLifecycleState? build() { + return null; + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.g.dart b/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.g.dart new file mode 100644 index 00000000..b72ac7d2 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/providers/lifecycle.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'lifecycle.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$browserViewLifecycleHash() => + r'9082d058611aaaae268b60b8ce130150f73d453d'; + +/// See also [BrowserViewLifecycle]. +@ProviderFor(BrowserViewLifecycle) +final browserViewLifecycleProvider = + NotifierProvider.internal( + BrowserViewLifecycle.new, + name: r'browserViewLifecycleProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$browserViewLifecycleHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$BrowserViewLifecycle = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index 5e20d4cb..5a249890 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,6 +15,7 @@ import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers/lifecycle.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/browser_data.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings_replication.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart'; @@ -135,6 +137,12 @@ class _BrowserViewState extends ConsumerState void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref + .read(browserViewLifecycleProvider.notifier) + .update(SchedulerBinding.instance.lifecycleState); + }); + WidgetsBinding.instance.addObserver(this); //Initialize and register dependencies @@ -172,6 +180,8 @@ class _BrowserViewState extends ConsumerState void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); + ref.read(browserViewLifecycleProvider.notifier).update(state); + switch (state) { case AppLifecycleState.detached: case AppLifecycleState.inactive: diff --git a/app/lib/features/tor/domain/services/tor_proxy.dart b/app/lib/features/tor/domain/services/tor_proxy.dart index 55b4609f..2c86019b 100644 --- a/app/lib/features/tor/domain/services/tor_proxy.dart +++ b/app/lib/features/tor/domain/services/tor_proxy.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers/lifecycle.dart'; import 'package:lensai/features/tor/utils/tor_entrypoint.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -18,6 +20,14 @@ class _TorService { await service.startService(); } + Future requestSync() async { + service.invoke("sync"); + } + + Future sendHeartbeat() async { + service.invoke("heartbeat"); + } + Future stop() async { service.invoke("stop"); } @@ -39,9 +49,9 @@ class _TorService { }); } - void dispose() { - unawaited(stop()); - unawaited(_portSubject.close()); + Future dispose() async { + await stop(); + await _portSubject.close(); } } @@ -49,17 +59,44 @@ class _TorService { class TorProxyService extends _$TorProxyService { final _tor = _TorService(); + Timer? _heartbeatUpdate; + //This is managed by widget state changes to resume a timer + bool _timerPaused = false; + + void _enableHeartbeatTimer() { + _heartbeatUpdate?.cancel(); + _timerPaused = false; + _heartbeatUpdate = Timer.periodic(const Duration(seconds: 30), ( + timer, + ) async { + await _tor.sendHeartbeat(); + }); + } + Future connect({bool forceReconnect = false}) async { - final currentPort = _tor.portStream.valueOrNull; + final currentPort = await requestSync(); if (currentPort == null || forceReconnect) { state = const AsyncLoading(); await _tor.start(); + _enableHeartbeatTimer(); } } + Future requestSync() async { + final nextPortUpdate = _tor.portStream.first; + + await _tor.requestSync(); + return nextPortUpdate; + } + Future disconnect() async { state = const AsyncLoading(); + + _heartbeatUpdate?.cancel(); + _heartbeatUpdate = null; + _timerPaused = false; + await _tor.stop(); } @@ -71,9 +108,31 @@ class TorProxyService extends _$TorProxyService { await _tor.initializeService(); + ref.listen(browserViewLifecycleProvider, (previous, next) { + switch (next) { + case AppLifecycleState.resumed: + if (_timerPaused) { + _enableHeartbeatTimer(); + } + case AppLifecycleState.detached: + case AppLifecycleState.inactive: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + case null: + if (_heartbeatUpdate?.isActive == true) { + _heartbeatUpdate?.cancel(); + _timerPaused = true; + } + } + }); + ref.onDispose(() async { + _heartbeatUpdate?.cancel(); + _heartbeatUpdate = null; + _timerPaused = false; + await portSub.cancel(); - _tor.dispose(); + await _tor.dispose(); }); return _tor.portStream.valueOrNull; diff --git a/app/lib/features/tor/domain/services/tor_proxy.g.dart b/app/lib/features/tor/domain/services/tor_proxy.g.dart index 98256ba5..4d2c01a2 100644 --- a/app/lib/features/tor/domain/services/tor_proxy.g.dart +++ b/app/lib/features/tor/domain/services/tor_proxy.g.dart @@ -6,7 +6,7 @@ part of 'tor_proxy.dart'; // RiverpodGenerator // ************************************************************************** -String _$torProxyServiceHash() => r'782708b8e5159931cecf8d32b843cb96a929fb1f'; +String _$torProxyServiceHash() => r'1f0c7611fd939ff25ad0acbdbd205b7e6e83067c'; /// See also [TorProxyService]. @ProviderFor(TorProxyService) diff --git a/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart b/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart index 9eae7ad8..6eb055bd 100644 --- a/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart +++ b/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart @@ -12,24 +12,28 @@ class StartProxyController extends _$StartProxyController { // ignore: document_ignores is used for dialog // ignore: avoid_build_context_in_providers Future maybeStartProxy(BuildContext context) async { - final torProxyRunning = ref.read(torProxyServiceProvider); + final torProxyRunning = + await ref.read(torProxyServiceProvider.notifier).requestSync(); - if (torProxyRunning.valueOrNull == null) { - final result = await showDialog( - context: context, - builder: (context) { - return TorDialog(); - }, - ); + if (torProxyRunning == null) { + if (context.mounted) { + final result = await showDialog( + context: context, + builder: (context) { + return TorDialog(); + }, + ); - if (result == true) { - final connection = ref.read(torProxyServiceProvider.notifier).connect(); + if (result == true) { + final connection = + ref.read(torProxyServiceProvider.notifier).connect(); - ref - .read(overlayControllerProvider.notifier) - .show(Positioned(top: 0, left: 0, child: TorNotification())); + ref + .read(overlayControllerProvider.notifier) + .show(Positioned(top: 0, left: 0, child: TorNotification())); - await connection; + await connection; + } } } } diff --git a/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart b/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart index 757ee438..aa4cb393 100644 --- a/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart +++ b/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart @@ -7,7 +7,7 @@ part of 'start_tor_proxy.dart'; // ************************************************************************** String _$startProxyControllerHash() => - r'7ba8d6eded4664a8aa3eb1360926c56e9185cac9'; + r'b83cdc1b6d1c358a2042ff2b7cc2f19f82e5d5ed'; /// See also [StartProxyController]. @ProviderFor(StartProxyController) diff --git a/app/lib/features/tor/presentation/screens/tor_proxy.dart b/app/lib/features/tor/presentation/screens/tor_proxy.dart index 37abaa11..34ee1da9 100644 --- a/app/lib/features/tor/presentation/screens/tor_proxy.dart +++ b/app/lib/features/tor/presentation/screens/tor_proxy.dart @@ -3,6 +3,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:flutter_svg/flutter_svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/tor/domain/services/tor_proxy.dart'; +import 'package:lensai/presentation/hooks/on_initialization.dart'; class TorProxyScreen extends HookConsumerWidget { const TorProxyScreen(); @@ -11,6 +12,10 @@ class TorProxyScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final torProxyPort = ref.watch(torProxyServiceProvider); + useOnInitialization(() async { + await ref.read(torProxyServiceProvider.notifier).requestSync(); + }); + return Scaffold( appBar: AppBar(title: const Text('Tor Proxy')), body: ColoredBox( diff --git a/app/lib/features/tor/utils/tor_entrypoint.dart b/app/lib/features/tor/utils/tor_entrypoint.dart index d5279306..902946b6 100644 --- a/app/lib/features/tor/utils/tor_entrypoint.dart +++ b/app/lib/features/tor/utils/tor_entrypoint.dart @@ -1,22 +1,50 @@ +import 'dart:async'; + import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:tor/tor.dart'; @pragma('vm:entry-point') Future onStart(ServiceInstance service) async { + Timer? timeout; + await Tor.init(); final portSub = Tor.instance.events.stream.listen((port) { service.invoke('portUpdate', {'port': port}); }); - await Tor.instance.start(); + Future stopService() async { + timeout?.cancel(); + timeout = null; - service.on("stop").listen((event) async { Tor.instance.stop(); service.invoke('portUpdate', {'port': -1}); await portSub.cancel(); await service.stopSelf(); + } + + void addHeartbeat() { + timeout?.cancel(); + timeout = Timer(const Duration(minutes: 15), () async { + // logger.i('Terminating tor due to timeout'); + await stopService(); + }); + } + + await Tor.instance.start(); + + service.on("heartbeat").listen((event) { + // logger.d('Received tor heartbeat'); + addHeartbeat(); + }); + + service.on("sync").listen((event) { + service.invoke('portUpdate', {'port': Tor.instance.port}); + }); + + service.on("stop").listen((event) async { + await stopService(); }); }