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