diff --git a/app/lib/core/database_registry.dart b/app/lib/core/database_registry.dart new file mode 100644 index 00000000..3ae157a0 --- /dev/null +++ b/app/lib/core/database_registry.dart @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:drift/drift.dart'; +import 'package:weblibre/core/logger.dart'; + +class DatabaseRegistry { + DatabaseRegistry._(); + static final instance = DatabaseRegistry._(); + + final _databases = {}; + + void register(String name, GeneratedDatabase db) { + _databases[name] = db; + } + + Future closeAll() async { + for (final entry in _databases.entries) { + try { + await entry.value.close(); + logger.i('${entry.key} database closed'); + } catch (e, st) { + logger.e( + 'Failed to close ${entry.key} database', + error: e, + stackTrace: st, + ); + } + } + _databases.clear(); + } +} diff --git a/app/lib/features/bangs/data/providers.dart b/app/lib/features/bangs/data/providers.dart index 44154662..3859bad3 100644 --- a/app/lib/features/bangs/data/providers.dart +++ b/app/lib/features/bangs/data/providers.dart @@ -24,7 +24,7 @@ import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/bangs/data/database/database.dart'; @@ -45,6 +45,8 @@ BangDatabase bangDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('bang', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/bangs/data/providers.g.dart b/app/lib/features/bangs/data/providers.g.dart index 3b05db7f..acb1d7e4 100644 --- a/app/lib/features/bangs/data/providers.g.dart +++ b/app/lib/features/bangs/data/providers.g.dart @@ -48,4 +48,4 @@ final class BangDatabaseProvider } } -String _$bangDatabaseHash() => r'86fed6bcc4a1e8a0621869c2886b1b80d353f14f'; +String _$bangDatabaseHash() => r'5a51a8c1db43e46f0adc0b08def0dc44b9b2dfd4'; diff --git a/app/lib/features/geckoview/features/tabs/data/providers.dart b/app/lib/features/geckoview/features/tabs/data/providers.dart index 5531bcf1..d88fee8e 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.dart @@ -24,7 +24,7 @@ import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart'; @@ -51,6 +51,8 @@ TabDatabase tabDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('tab', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/geckoview/features/tabs/data/providers.g.dart b/app/lib/features/geckoview/features/tabs/data/providers.g.dart index 30f2ad67..3628962d 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.g.dart @@ -48,4 +48,4 @@ final class TabDatabaseProvider } } -String _$tabDatabaseHash() => r'337dbcf30bd57dcee409aec0aa93f1f6f2783368'; +String _$tabDatabaseHash() => r'c40af8d5e17f61abcf14ea096ace3d0c2fb3d8d8'; diff --git a/app/lib/features/geckoview/features/top_sites/data/providers.dart b/app/lib/features/geckoview/features/top_sites/data/providers.dart index 29bb9209..3ff2e16e 100644 --- a/app/lib/features/geckoview/features/top_sites/data/providers.dart +++ b/app/lib/features/geckoview/features/top_sites/data/providers.dart @@ -24,7 +24,7 @@ import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; import 'package:weblibre/features/geckoview/features/top_sites/data/database/database.dart'; @@ -52,6 +52,8 @@ TopSiteDatabase topSiteDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('topSite', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/geckoview/features/top_sites/data/providers.g.dart b/app/lib/features/geckoview/features/top_sites/data/providers.g.dart index c3fc666e..73331a3c 100644 --- a/app/lib/features/geckoview/features/top_sites/data/providers.g.dart +++ b/app/lib/features/geckoview/features/top_sites/data/providers.g.dart @@ -49,4 +49,4 @@ final class TopSiteDatabaseProvider } } -String _$topSiteDatabaseHash() => r'6371e1784bd1272bdfc656caaa56e786e7503c78'; +String _$topSiteDatabaseHash() => r'bee041ef1b144ce63c38080b2534e4109399fa6f'; diff --git a/app/lib/features/onboarding/domain/providers.g.dart b/app/lib/features/onboarding/domain/providers.g.dart index ac153b12..310001b6 100644 --- a/app/lib/features/onboarding/domain/providers.g.dart +++ b/app/lib/features/onboarding/domain/providers.g.dart @@ -42,7 +42,7 @@ final class OnboardingModeNotifierProvider } String _$onboardingModeNotifierHash() => - r'bbb8e7ec1c85699566750d90c92c273e031d654b'; + r'84cd9a7436fd4ac20972c25ed015c1c6b20f5a00'; abstract class _$OnboardingModeNotifier extends $Notifier { OnboardingMode build(); diff --git a/app/lib/features/quotes/data/providers.dart b/app/lib/features/quotes/data/providers.dart index f286df7d..ae4486a2 100644 --- a/app/lib/features/quotes/data/providers.dart +++ b/app/lib/features/quotes/data/providers.dart @@ -25,7 +25,7 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/quotes/data/database/database.dart'; @@ -59,6 +59,8 @@ QuotesDatabase quotesDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('quotes', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/quotes/data/providers.g.dart b/app/lib/features/quotes/data/providers.g.dart index f73460d4..c5dc7ccf 100644 --- a/app/lib/features/quotes/data/providers.g.dart +++ b/app/lib/features/quotes/data/providers.g.dart @@ -48,4 +48,4 @@ final class QuotesDatabaseProvider } } -String _$quotesDatabaseHash() => r'e5a15556bef876f3a4941957f01bc1457a12a956'; +String _$quotesDatabaseHash() => r'be623fb69eb03f4c264d1a4fd07300fa669ecc86'; diff --git a/app/lib/features/user/data/providers.dart b/app/lib/features/user/data/providers.dart index bac5a7cb..a1821954 100644 --- a/app/lib/features/user/data/providers.dart +++ b/app/lib/features/user/data/providers.dart @@ -25,7 +25,7 @@ import 'package:path/path.dart' as p; import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/data/database/functions/lexo_rank_functions.dart'; import 'package:weblibre/features/user/data/database/database.dart'; @@ -53,6 +53,8 @@ UserDatabase userDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('user', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/user/data/providers.g.dart b/app/lib/features/user/data/providers.g.dart index 253e4c62..6106cb2e 100644 --- a/app/lib/features/user/data/providers.g.dart +++ b/app/lib/features/user/data/providers.g.dart @@ -48,7 +48,7 @@ final class UserDatabaseProvider } } -String _$userDatabaseHash() => r'1d14f39dd506ec8890a0a15cc2546b3294a34a1c'; +String _$userDatabaseHash() => r'ebc37c5e9604e02f820c733b19f8b7678b335493'; @ProviderFor(riverpodDatabaseStorage) final riverpodDatabaseStorageProvider = RiverpodDatabaseStorageProvider._(); diff --git a/app/lib/features/user/domain/repositories/onboarding.g.dart b/app/lib/features/user/domain/repositories/onboarding.g.dart index 590917fa..1967009f 100644 --- a/app/lib/features/user/domain/repositories/onboarding.g.dart +++ b/app/lib/features/user/domain/repositories/onboarding.g.dart @@ -42,7 +42,7 @@ final class OnboardingRepositoryProvider } String _$onboardingRepositoryHash() => - r'd16657963415392840cdba9f4bcc724963f6da63'; + r'5d583af3ae38b351357b16809ef32bb6807f5c05'; abstract class _$OnboardingRepository extends $Notifier { void build(); diff --git a/app/lib/features/web_feed/data/providers.dart b/app/lib/features/web_feed/data/providers.dart index bca66927..847e0bf7 100644 --- a/app/lib/features/web_feed/data/providers.dart +++ b/app/lib/features/web_feed/data/providers.dart @@ -24,7 +24,7 @@ import 'package:drift/native.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; - +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/filesystem.dart'; import 'package:weblibre/features/web_feed/data/database/database.dart'; @@ -45,6 +45,8 @@ FeedDatabase feedDatabase(Ref ref) { }), ); + DatabaseRegistry.instance.register('feed', db); + ref.onDispose(() async { await db.close(); }); diff --git a/app/lib/features/web_feed/data/providers.g.dart b/app/lib/features/web_feed/data/providers.g.dart index bd8b2180..47f7a2d6 100644 --- a/app/lib/features/web_feed/data/providers.g.dart +++ b/app/lib/features/web_feed/data/providers.g.dart @@ -48,4 +48,4 @@ final class FeedDatabaseProvider } } -String _$feedDatabaseHash() => r'8f24d15d6da7e498bfcd9303698f109028b492fc'; +String _$feedDatabaseHash() => r'182dd4203290835fc2188627db08fb0491bd91b2'; diff --git a/app/lib/utils/exit_app.dart b/app/lib/utils/exit_app.dart index f10b96df..dbdf6ae0 100644 --- a/app/lib/utils/exit_app.dart +++ b/app/lib/utils/exit_app.dart @@ -20,25 +20,67 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod/riverpod.dart'; +import 'package:weblibre/core/database_registry.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; Future exitApp(ProviderContainer container) async { logger.i('Preparing exit'); - await container - .read(tabDataRepositoryProvider.notifier) - .closeAllTabs(includeRegular: false); + // 1. Close private/isolated tabs (clears browsing data for those contexts) + try { + await container + .read(tabDataRepositoryProvider.notifier) + .closeAllTabs(includeRegular: false); + logger.i('Private tabs closed'); + } catch (e, st) { + logger.e('Failed to close tabs', error: e, stackTrace: st); + } - await SystemNavigator.pop(); - logger.i('SystemNavigator popped'); + // 2. Stop Tor proxy (only if it was initialized) + if (container.exists(torProxyServiceProvider)) { + try { + await container.read(torProxyServiceProvider.notifier).disconnect(); + logger.i('Tor proxy stopped'); + } catch (e, st) { + logger.e('Failed to stop Tor proxy', error: e, stackTrace: st); + } + } + // 3. Shutdown GeckoView engine. Must happen while the activity is still + // attached so shutdown() can access the FragmentManager. Internally it: + // a) removes the BrowserFragment via commitNow() (view teardown with + // the runtime still alive), + // b) stops component-level services (FxA, account manager), + // c) shuts down GeckoRuntime (safe — no views reference it anymore). + try { + await GeckoBrowserService().shutdown(); + logger.i('GeckoView engine shut down'); + } catch (e, st) { + logger.e('Failed to shut down GeckoView', error: e, stackTrace: st); + } + + // 4. Close all registered databases + try { + await DatabaseRegistry.instance.closeAll(); + } catch (e, st) { + logger.e('Failed to close databases', error: e, stackTrace: st); + } + + // 5. Dispose the Riverpod container (remaining sync cleanup). + // This fires async onDispose callbacks (e.g. stream cancellations in + // GeckoView services, viewport service) as fire-and-forget futures. container.dispose(); logger.i('Provider container disposed'); - await Future.delayed(const Duration(seconds: 1)).whenComplete(() { - logger.i('Bye !!1'); - exit(0); - }); + // 6. Signal the system to finish the activity and give fire-and-forget + // async onDispose callbacks time to settle. + await SystemNavigator.pop(); + await Future.delayed(const Duration(seconds: 1)); + + logger.i('Bye !!1'); + exit(0); } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt index ac6764a6..f9a9d25f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt @@ -31,9 +31,8 @@ import org.mozilla.geckoview.GeckoRuntimeSettings object EngineProvider { private var runtime: GeckoRuntime? = null - private val components by lazy { - requireNotNull(GlobalComponents.components) { "Components not initialized" } - } + private val components: Components + get() = requireNotNull(GlobalComponents.components) { "Components not initialized" } @Synchronized fun getOrCreateRuntime(context: Context): GeckoRuntime { @@ -148,4 +147,13 @@ object EngineProvider { val runtime = getOrCreateRuntime(context) return GeckoViewFetchClient(context, runtime) } + + @Synchronized + fun shutdown() { + runtime?.let { + Logger.debug("Shutting down GeckoRuntime") + it.shutdown() + runtime = null + } + } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index b2305161..de012518 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -12,6 +12,7 @@ import android.view.View import androidx.fragment.app.FragmentActivity import eu.weblibre.flutter_mozilla_components.BrowserFragment import eu.weblibre.flutter_mozilla_components.GeckoViewFactory +import eu.weblibre.flutter_mozilla_components.EngineProvider import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.ProfileContext import eu.weblibre.flutter_mozilla_components.activities.ExternalAppBrowserActivity @@ -455,4 +456,49 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { currentActivity.startActivity(intent) } + override fun shutdown() { + logger.debug("$TAG: Shutting down GeckoView engine") + + // 1. Remove the browser fragment so its onDestroyView runs while the + // runtime is still alive. This tears down EngineView, features, and + // clears component references cleanly. + try { + val fragmentActivity = activity as? FragmentActivity + if (fragmentActivity != null && !fragmentActivity.isFinishing && !fragmentActivity.isDestroyed) { + val fm = fragmentActivity.supportFragmentManager + if (!fm.isStateSaved) { + val existing = fm.findFragmentById(FRAGMENT_CONTAINER_ID) + if (existing != null) { + fm.beginTransaction().remove(existing).commitNow() + logger.debug("$TAG: Browser fragment removed") + } + } + } + } catch (e: Exception) { + logger.error("$TAG: Error removing browser fragment", e) + } + + // 2. Stop component-level services + try { + GlobalComponents.components?.let { components -> + // Stop the FxA web channel feature + runCatching { components.services.fxaWebChannelFeature.stop() } + + // Close the account manager + runCatching { components.backgroundServices.accountManager.close() } + } + } catch (e: Exception) { + logger.error("$TAG: Error during component shutdown", e) + } + + // 3. Shutdown GeckoRuntime (safe now that no views reference it) + try { + EngineProvider.shutdown() + } catch (e: Exception) { + logger.error("$TAG: Error shutting down GeckoRuntime", e) + } + + isGeckoInitialized = false + } + } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 6c85efc0..fc798972 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -4847,6 +4847,7 @@ interface GeckoBrowserApi { fun openInCustomTab(url: String, private: Boolean, contextId: String?) fun isDefaultBrowser(): Boolean fun requestDefaultBrowser() + fun shutdown() companion object { /** The codec used by GeckoBrowserApi. */ @@ -4980,6 +4981,22 @@ interface GeckoBrowserApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.shutdown$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.shutdown() + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart index 60fe518d..0bd881fb 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart @@ -64,4 +64,8 @@ class GeckoBrowserService { Future requestDefaultBrowser() { return _api.requestDefaultBrowser(); } + + Future shutdown() { + return _api.shutdown(); + } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 5514fcca..7371683f 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -5805,6 +5805,28 @@ class GeckoBrowserApi { return; } } + + Future shutdown() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.shutdown$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } class GeckoSyncApi { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index cbc76601..ad09b330 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1151,6 +1151,7 @@ abstract class GeckoBrowserApi { }); bool isDefaultBrowser(); void requestDefaultBrowser(); + void shutdown(); } enum SyncEngineValue { history, bookmarks, tabs }