diff --git a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt index 2165b17d..c2085d30 100644 --- a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt +++ b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt @@ -27,19 +27,30 @@ import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterFragmentActivity() { - private val CHANNEL = "eu.weblibre.flutter_mozilla_components/trim_memory" + private val TRIM_MEMORY_CHANNEL = "eu.weblibre.flutter_mozilla_components/trim_memory" + private val ACTIVITY_CHANNEL = "eu.weblibre.gecko/activity" private val ENGINE_ID = "engine_id" - private var channel: MethodChannel? = null + private var trimMemoryChannel: MethodChannel? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + trimMemoryChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, TRIM_MEMORY_CHANNEL) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, ACTIVITY_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "moveTaskToBack" -> { + moveTaskToBack(true) + result.success(null) + } + else -> result.notImplemented() + } + } } override fun onTrimMemory(level: Int) { super.onTrimMemory(level) - channel?.invokeMethod("onTrimMemory", level) + trimMemoryChannel?.invokeMethod("onTrimMemory", level) } override fun provideFlutterEngine(context: Context): FlutterEngine { diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index dc9c37d0..8e31a3a5 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -54,6 +54,10 @@ class TabRepository extends _$TabRepository { return _tabFromIntent.contains(tabId); } + void clearLaunchedFromIntent(String tabId) { + _tabFromIntent.remove(tabId); + } + Future _resolveParentIdForContext({ required String? parentId, required String? targetContextId, diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index a9ceed42..2ba3cd20 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabRepositoryProvider } } -String _$tabRepositoryHash() => r'f0ff983df4115bfed7ff3dbc8994592bc46a1daa'; +String _$tabRepositoryHash() => r'b300b88bc7817b533be46d8d0d933786b10212fb'; abstract class _$TabRepository extends $Notifier { void build(); diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart new file mode 100644 index 00000000..7e2173f6 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart @@ -0,0 +1,45 @@ +/* + * 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:flutter/material.dart'; + +/// Shows a dialog asking the user whether to keep a tab that was opened from another app. +/// +/// Returns true if the user wants to keep the tab, false if they want to discard it. +Future showKeepTabDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Keep tab?'), + content: const Text( + 'This tab was opened from another app. Do you want to keep it or discard it?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Discard'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Keep'), + ), + ], + ), + ); +} diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index bccb1c0e..3b8c399e 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -21,7 +21,6 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -42,6 +41,7 @@ import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart'; @@ -57,6 +57,7 @@ import 'package:weblibre/features/geckoview/features/find_in_page/presentation/w import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; +import 'package:weblibre/utils/move_to_background.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; /// Callback for toolbar animation progress updates. @@ -810,9 +811,26 @@ class _Browser extends HookConsumerWidget { if (ref .read(tabRepositoryProvider.notifier) .hasLaunchedFromIntent(tabState?.id)) { - //Mark back as unhandled and navigator will pop - await SystemNavigator.pop(); - return false; + if (!context.mounted) return false; + + final keep = await showKeepTabDialog(context); + if (keep == true) { + ref + .read(tabRepositoryProvider.notifier) + .clearLaunchedFromIntent(tabState!.id); + + await moveToBackground(); + return true; + } + + if (tabState != null) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(tabState.id); + } + + await moveToBackground(); + return true; } // Handle double back to close (if enabled) @@ -836,9 +854,8 @@ class _Browser extends HookConsumerWidget { return true; } else { - //Mark back as unhandled and navigator will pop - await SystemNavigator.pop(); - return false; + await moveToBackground(); + return true; } } else { lastBackButtonPress.value = DateTime.now(); diff --git a/app/lib/utils/move_to_background.dart b/app/lib/utils/move_to_background.dart new file mode 100644 index 00000000..6a044234 --- /dev/null +++ b/app/lib/utils/move_to_background.dart @@ -0,0 +1,29 @@ +/* + * 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:flutter/services.dart'; + +const _channel = MethodChannel('eu.weblibre.gecko/activity'); + +/// Moves the app to the background without finishing the activity. +/// Unlike [SystemNavigator.pop], this keeps the Flutter engine attached, +/// avoiding crashes when the user returns to the app. +Future moveToBackground() async { + await _channel.invokeMethod('moveTaskToBack'); +}