diff --git a/ui/flutter/android/app/src/main/AndroidManifest.xml b/ui/flutter/android/app/src/main/AndroidManifest.xml
index 69e93226..968c7987 100644
--- a/ui/flutter/android/app/src/main/AndroidManifest.xml
+++ b/ui/flutter/android/app/src/main/AndroidManifest.xml
@@ -32,6 +32,12 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
+
+
diff --git a/ui/flutter/lib/app/application/app_deep_link_controller.dart b/ui/flutter/lib/app/application/app_deep_link_controller.dart
index c63ca5df..548287c9 100644
--- a/ui/flutter/lib/app/application/app_deep_link_controller.dart
+++ b/ui/flutter/lib/app/application/app_deep_link_controller.dart
@@ -37,10 +37,15 @@ class AppDeepLinkController extends AsyncNotifier {
@override
Future build() async {
- final runtime = ref.watch(appRuntimeControllerProvider).value;
- if (runtime == null || _started || kIsWeb) {
+ if (_started || kIsWeb) {
return AppDeepLinkState(started: _started);
}
+ // Deep-link handling depends on the runtime being ready, but it must not
+ // rebuild whenever runtime settings change. A rebuild disposes the active
+ // platform subscriptions while `_started` remains true, leaving Android
+ // unable to receive later browser takeover intents (for example after the
+ // API server is started or stopped from Settings).
+ await ref.read(appRuntimeControllerProvider.future);
_started = true;
final appLinks = AppLinks();
_subscription = appLinks.uriLinkStream.listen((uri) {
diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock
index 4922da34..1787289c 100644
--- a/ui/flutter/pubspec.lock
+++ b/ui/flutter/pubspec.lock
@@ -42,7 +42,7 @@ packages:
source: hosted
version: "1.0.3"
app_links_platform_interface:
- dependency: transitive
+ dependency: "direct dev"
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml
index 6bca2e53..588d8532 100644
--- a/ui/flutter/pubspec.yaml
+++ b/ui/flutter/pubspec.yaml
@@ -105,6 +105,7 @@ dependency_overrides:
path: permission_handler_windows
dev_dependencies:
+ app_links_platform_interface: ^2.0.2
flutter_test:
sdk: flutter
diff --git a/ui/flutter/test/app_deep_link_controller_test.dart b/ui/flutter/test/app_deep_link_controller_test.dart
index 388bc53f..324e026f 100644
--- a/ui/flutter/test/app_deep_link_controller_test.dart
+++ b/ui/flutter/test/app_deep_link_controller_test.dart
@@ -1,5 +1,13 @@
+import 'dart:async';
+
+import 'package:app_links_platform_interface/app_links_platform_interface.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:gopeed/api/model/downloader_config.dart';
+import 'package:gopeed/app/application/app_runtime_controller.dart';
import 'package:gopeed/app/application/app_deep_link_controller.dart';
+import 'package:gopeed/core/common/api_server_state.dart';
+import 'package:gopeed/core/common/start_config.dart';
import 'package:share_handler/share_handler.dart';
void main() {
@@ -27,4 +35,71 @@ void main() {
expect(sharedMediaUri(media), Uri.parse('magnet:?xt=urn:btih:example'));
});
+
+ test('keeps listening for browser takeover links after runtime settings change', () async {
+ final originalPlatform = AppLinksPlatform.instance;
+ final appLinksPlatform = _FakeAppLinksPlatform();
+ AppLinksPlatform.instance = appLinksPlatform;
+ final container = ProviderContainer(
+ overrides: [appRuntimeControllerProvider.overrideWith(_FakeRuntimeController.new)],
+ );
+
+ try {
+ await container.read(appDeepLinkControllerProvider.future);
+ expect(appLinksPlatform.hasListener, isTrue);
+
+ container.read(appRuntimeControllerProvider.notifier).replaceDownloaderConfig(DownloaderConfig());
+ await Future.delayed(Duration.zero);
+ await Future.delayed(Duration.zero);
+
+ expect(appLinksPlatform.hasListener, isTrue);
+ } finally {
+ container.dispose();
+ await Future.delayed(Duration.zero);
+ AppLinksPlatform.instance = originalPlatform;
+ await appLinksPlatform.close();
+ }
+ });
+}
+
+class _FakeAppLinksPlatform extends AppLinksPlatform {
+ final _links = StreamController.broadcast();
+
+ bool get hasListener => _links.hasListener;
+
+ @override
+ Future getInitialLink() async => null;
+
+ @override
+ Stream get uriLinkStream => _links.stream;
+
+ Future close() => _links.close();
+}
+
+class _FakeRuntimeController extends AppRuntimeController {
+ @override
+ Future build() async {
+ final config = StartConfig()
+ ..network = 'tcp'
+ ..address = '127.0.0.1:9999'
+ ..apiEnable = true
+ ..apiToken = ''
+ ..storage = 'bolt'
+ ..storageDir = ''
+ ..refreshInterval = 0;
+ return AppRuntimeState(
+ startConfig: config,
+ apiServerState: const ApiServerState(
+ enabled: true,
+ mcpEnabled: false,
+ running: true,
+ network: 'tcp',
+ address: '127.0.0.1:9999',
+ runningPort: 9999,
+ pendingApply: false,
+ lastError: '',
+ ),
+ downloaderConfig: DownloaderConfig(),
+ );
+ }
}