From aae79c244992a241dac04a8110a9fcc942d3e902 Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 7 Nov 2023 17:52:10 +0800 Subject: [PATCH] feat: running foreground service on mobile (#261) --- ui/flutter/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 6 + ui/flutter/assets/locales/en_US.json | 7 +- ui/flutter/assets/locales/zh_CN.json | 4 +- ui/flutter/ios/Runner/AppDelegate.swift | 9 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../app/controllers/app_controller.dart | 64 ++++- .../lib/app/modules/home/views/home_view.dart | 269 +++++++++--------- ui/flutter/lib/generated/locales.g.dart | 6 + ui/flutter/pubspec.lock | 8 + ui/flutter/pubspec.yaml | 1 + 11 files changed, 224 insertions(+), 153 deletions(-) diff --git a/ui/flutter/android/app/build.gradle b/ui/flutter/android/app/build.gradle index 8e1a322d..11472895 100644 --- a/ui/flutter/android/app/build.gradle +++ b/ui/flutter/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.gopeed" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/ui/flutter/android/app/src/main/AndroidManifest.xml b/ui/flutter/android/app/src/main/AndroidManifest.xml index 15d4a893..e6dfc888 100644 --- a/ui/flutter/android/app/src/main/AndroidManifest.xml +++ b/ui/flutter/android/app/src/main/AndroidManifest.xml @@ -72,6 +72,12 @@ + + + + diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index f0463f91..90da5ec1 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -79,21 +80,18 @@ class AppController extends GetxController with WindowListener, TrayListener { @override void onReady() { super.onReady(); - try { - _initDeepLinks(); - } catch (e) { - logger.w("initDeepLinks error", e); - } - try { - _initWindows(); - } catch (e) { - logger.w("initWindows error", e); - } - try { - _initTray(); - } catch (e) { - logger.w("initTray error", e); - } + + _initDeepLinks().onError((error, stackTrace) => + logger.w("initDeepLinks error", error, stackTrace)); + + _initWindows().onError((error, stackTrace) => + logger.w("initWindows error", error, stackTrace)); + + _initTray().onError( + (error, stackTrace) => logger.w("initTray error", error, stackTrace)); + + _initForegroundTask().onError((error, stackTrace) => + logger.w("initForegroundTask error", error, stackTrace)); } @override @@ -210,6 +208,42 @@ class AppController extends GetxController with WindowListener, TrayListener { trayManager.addListener(this); } + Future _initForegroundTask() async { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'gopeed_service', + channelName: 'Gopeed Background Service', + channelImportance: NotificationChannelImportance.LOW, + showWhen: true, + priority: NotificationPriority.LOW, + iconData: const NotificationIconData( + resType: ResourceType.mipmap, + resPrefix: ResourcePrefix.ic, + name: 'launcher', + )), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: true, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 5000, + isOnceEvent: false, + autoRunOnBoot: true, + allowWakeLock: true, + allowWifiLock: true, + ), + ); + + if (await FlutterForegroundTask.isRunningService) { + FlutterForegroundTask.restartService(); + } else { + FlutterForegroundTask.startService( + notificationTitle: "serviceTitle".tr, + notificationText: "serviceText".tr, + ); + } + } + Future _toCreate(Uri uri) async { final path = uri.scheme == "magnet" ? uri.toString() diff --git a/ui/flutter/lib/app/modules/home/views/home_view.dart b/ui/flutter/lib/app/modules/home/views/home_view.dart index fbd25d48..1f98f7fd 100644 --- a/ui/flutter/lib/app/modules/home/views/home_view.dart +++ b/ui/flutter/lib/app/modules/home/views/home_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; import '../../../routes/app_pages.dart'; @@ -23,139 +24,141 @@ class HomeView extends GetView { break; } - return Scaffold( - // extendBody: true, - body: Row( - // crossAxisAlignment: CrossAxisAlignment.start, - children: [ - !ResponsiveBuilder.isNarrow(context) - ? NavigationRail( - extended: true, - labelType: NavigationRailLabelType.none, - minExtendedWidth: 170, - groupAlignment: 0, - // useIndicator: false, - onDestinationSelected: (int index) { - controller.currentIndex.value = index; - switch (index) { - case 0: - delegate.offAndToNamed(Routes.TASK); - break; - case 1: - delegate.offAndToNamed(Routes.EXTENSION); - break; - case 2: - delegate.offAndToNamed(Routes.SETTING); - break; - } - }, - destinations: [ - NavigationRailDestination( - icon: const Icon(Icons.task), - selectedIcon: const Icon(Icons.task), - label: Text('task'.tr), - ), - NavigationRailDestination( - icon: const Icon(Icons.extension), - selectedIcon: const Icon(Icons.extension), - label: Text('extensions'.tr), - ), - NavigationRailDestination( - icon: const Icon(Icons.settings), - selectedIcon: const Icon(Icons.settings), - label: Text('setting'.tr), - ), - ], - selectedIndex: controller.currentIndex.value, - leading: const Icon(Icons.menu), - // trailing: const Icon(Icons.info_outline), - ) - : const SizedBox.shrink(), - Expanded( - child: GetRouterOutlet( - initialRoute: Routes.TASK, - // anchorRoute: '/', - // filterPages: (afterAnchor) { - // logger.w(afterAnchor); - // logger.w(afterAnchor.take(1)); - // return afterAnchor.take(1); - // }, - )) - ]), - bottomNavigationBar: ResponsiveBuilder.isNarrow(context) - ? BottomNavigationBar( - items: [ - BottomNavigationBarItem( - icon: const Icon(Icons.task), - label: 'task'.tr, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.extension), - label: 'extensions'.tr, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.settings), - label: 'setting'.tr, - ), - ], - currentIndex: controller.currentIndex.value, - // selectedItemColor: Get.theme.highlightColor, - onTap: (index) { - controller.currentIndex.value = index; - switch (index) { - case 0: - delegate.offAndToNamed(Routes.TASK); - break; - case 1: - delegate.offAndToNamed(Routes.EXTENSION); - break; - case 2: - delegate.offAndToNamed(Routes.SETTING); - break; - } - }, - ) - // StylishBottomBar( - // option: AnimatedBarOptions( - // iconSize: 32, - // barAnimation: BarAnimation.blink, - // iconStyle: IconStyle.Default, - // opacity: 0.3, - // ), - // items: [ - // BottomBarItem( - // icon: const Icon(Icons.file_download), - // selectedColor: Get.theme.primaryColor, - // title: Text('downloading'.tr)), - // BottomBarItem( - // icon: const Icon(Icons.done), - // selectedColor: Get.theme.primaryColor, - // title: Text('downloaded'.tr)), - // BottomBarItem( - // icon: const Icon(Icons.settings), - // selectedColor: Get.theme.primaryColor, - // title: Text('setting'.tr)), - // ], - // // hasNotch: true, - // currentIndex: controller.currentIndex.value, - // onTap: (index) { - // switch (index) { - // case 0: - // delegate.toNamed(Routes.DOWNLOADING); - // controller.currentIndex.value = 0; - // break; - // case 1: - // delegate.toNamed(Routes.DOWNLOADED); - // controller.currentIndex.value = 1; - // break; - // case 2: - // delegate.toNamed(Routes.SETTING); - // controller.currentIndex.value = 2; - // break; - // } - // }, - // ) - : const SizedBox.shrink(), + return WithForegroundTask( + child: Scaffold( + // extendBody: true, + body: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + children: [ + !ResponsiveBuilder.isNarrow(context) + ? NavigationRail( + extended: true, + labelType: NavigationRailLabelType.none, + minExtendedWidth: 170, + groupAlignment: 0, + // useIndicator: false, + onDestinationSelected: (int index) { + controller.currentIndex.value = index; + switch (index) { + case 0: + delegate.offAndToNamed(Routes.TASK); + break; + case 1: + delegate.offAndToNamed(Routes.EXTENSION); + break; + case 2: + delegate.offAndToNamed(Routes.SETTING); + break; + } + }, + destinations: [ + NavigationRailDestination( + icon: const Icon(Icons.task), + selectedIcon: const Icon(Icons.task), + label: Text('task'.tr), + ), + NavigationRailDestination( + icon: const Icon(Icons.extension), + selectedIcon: const Icon(Icons.extension), + label: Text('extensions'.tr), + ), + NavigationRailDestination( + icon: const Icon(Icons.settings), + selectedIcon: const Icon(Icons.settings), + label: Text('setting'.tr), + ), + ], + selectedIndex: controller.currentIndex.value, + leading: const Icon(Icons.menu), + // trailing: const Icon(Icons.info_outline), + ) + : const SizedBox.shrink(), + Expanded( + child: GetRouterOutlet( + initialRoute: Routes.TASK, + // anchorRoute: '/', + // filterPages: (afterAnchor) { + // logger.w(afterAnchor); + // logger.w(afterAnchor.take(1)); + // return afterAnchor.take(1); + // }, + )) + ]), + bottomNavigationBar: ResponsiveBuilder.isNarrow(context) + ? BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.task), + label: 'task'.tr, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.extension), + label: 'extensions'.tr, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.settings), + label: 'setting'.tr, + ), + ], + currentIndex: controller.currentIndex.value, + // selectedItemColor: Get.theme.highlightColor, + onTap: (index) { + controller.currentIndex.value = index; + switch (index) { + case 0: + delegate.offAndToNamed(Routes.TASK); + break; + case 1: + delegate.offAndToNamed(Routes.EXTENSION); + break; + case 2: + delegate.offAndToNamed(Routes.SETTING); + break; + } + }, + ) + // StylishBottomBar( + // option: AnimatedBarOptions( + // iconSize: 32, + // barAnimation: BarAnimation.blink, + // iconStyle: IconStyle.Default, + // opacity: 0.3, + // ), + // items: [ + // BottomBarItem( + // icon: const Icon(Icons.file_download), + // selectedColor: Get.theme.primaryColor, + // title: Text('downloading'.tr)), + // BottomBarItem( + // icon: const Icon(Icons.done), + // selectedColor: Get.theme.primaryColor, + // title: Text('downloaded'.tr)), + // BottomBarItem( + // icon: const Icon(Icons.settings), + // selectedColor: Get.theme.primaryColor, + // title: Text('setting'.tr)), + // ], + // // hasNotch: true, + // currentIndex: controller.currentIndex.value, + // onTap: (index) { + // switch (index) { + // case 0: + // delegate.toNamed(Routes.DOWNLOADING); + // controller.currentIndex.value = 0; + // break; + // case 1: + // delegate.toNamed(Routes.DOWNLOADED); + // controller.currentIndex.value = 1; + // break; + // case 2: + // delegate.toNamed(Routes.SETTING); + // controller.currentIndex.value = 2; + // break; + // } + // }, + // ) + : const SizedBox.shrink(), + ), ); }); } diff --git a/ui/flutter/lib/generated/locales.g.dart b/ui/flutter/lib/generated/locales.g.dart index 7c8e82a7..292f1784 100644 --- a/ui/flutter/lib/generated/locales.g.dart +++ b/ui/flutter/lib/generated/locales.g.dart @@ -87,6 +87,8 @@ class LocaleKeys { static const history = 'history'; static const clearHistory = 'clearHistory'; static const noHistoryFound = 'noHistoryFound'; + static const serviceTitle = 'serviceTitle'; + static const serviceText = 'serviceText'; } class Locales { @@ -165,6 +167,8 @@ class Locales { 'history': 'History', 'clearHistory': 'Clear History', 'noHistoryFound': 'No History Found', + 'serviceTitle': 'Download Service', + 'serviceText': 'Running', }; static const fa_IR = { 'label': 'فارسی', @@ -434,6 +438,8 @@ class Locales { 'history': '历史记录', 'clearHistory': '清空历史记录', 'noHistoryFound': '暂无历史记录', + 'serviceTitle': '下载服务', + 'serviceText': '运行中', }; static const zh_TW = { 'label': '中文 (正體) ', diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index ac275573..aaaf6399 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -310,6 +310,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: e48d2d810a2d643362e64de41146ed8e95d4dd282bae6abbb32309d9f0bf5d67 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.2" flutter_form_builder: dependency: "direct main" description: diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 04faa5c5..1bc26b82 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: open_file: ^3.3.2 flutter_form_builder: ^9.1.1 form_builder_validators: ^9.0.0 + flutter_foreground_task: ^6.1.2 dev_dependencies: flutter_test: sdk: flutter