diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c349f..ce4acf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,4 +30,10 @@ ## 0.1.5 -* Add `isBackgroundExecutionEnabled` property to enable checking the current background execution state \ No newline at end of file +* Add `isBackgroundExecutionEnabled` property to enable checking the current background execution state. + +## 0.1.6 + +* Improve initialize method on Android +* Add ability to specify custom notification icons +* Update documentation accordingly \ No newline at end of file diff --git a/README.md b/README.md index 57b51ac..b5187cd 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,20 @@ final androidConfig = FlutterBackgroundAndroidConfig( notificationTitle: "Title of the notification", notificationText: "Text of the notification", notificationImportance: AndroidNotificationImportance.Default, + notificationIcon: AndroidResource(name: 'background_icon', defType: 'drawable'), // Default is ic_launcher from folder mipmap ); bool success = await FlutterBackground.initialize(androidConfig: androidconfig); ``` -In order to function correctly, this plugin needs a few permissions. `FlutterBackground.initialize(...)` will request permissions from the user if necessary. +This results in the following notification (where `background_icon` is a drawable resource, see example app): +![The foreground notification created by the code above.](./images/notification.png "Foreground notification created by the code above.") + +The notification icon is for the small icon displayed in the top left of a notification and must be a drawable Android Resource (see [here](https://developer.android.com/reference/android/app/Notification.Builder#setSmallIcon(int,%20int)) for more). +Check out the example app or the [Android documentation for creating notification icons](https://developer.android.com/studio/write/image-asset-studio#create-notification) for more information how to create and store an icon. + +In order to function correctly, this plugin needs a few permissions. +`FlutterBackground.initialize(...)` will request permissions from the user if necessary. +You can call initialize more than one time, so you can call `initalize()` every time before you call `enableBackgroundExecution()` (see below). In order to notify the user about upcoming permission requests by the system, you need to know, whether or not the app already has these permissions. You can find out by calling @@ -88,24 +97,12 @@ To check whether background execution is currently enabled, use bool enabled = FlutterBackground.isBackgroundExecutionEnabled ``` -### Notes - -The plugin is currently hard-coded to load the icon for the foreground service notification from a drawable resource with the identifier `ic_launcher`. -So if you want to change the logo for the notification, you have to change this resource. I'm planning to allow for custom resource names. - ## Example The example is a TCP chat app: It can connect to a TCP server and send and receive messages. The user is notified about incoming messages by notifications created with the plugin [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications). Using this plugin, the example app can maintain the TCP connection with the server, receiving messages and creating notifications for the user even when in the background. -## ToDo - -- Add automated tests -- On android, add [ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS](https://developer.android.com/reference/android/provider/Settings#ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) as an option to obtain excemption from battery optimizations, as declaring [REQUEST_IGNORE_BATTERY_OPTIMIZATIONS](https://developer.android.com/reference/android/Manifest.permission.html#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) might lead to a ban of the app in the Play Store, "unless the core function of the app is adversely affected" (see the note [here](https://developer.android.com/training/monitoring-device-state/doze-standby.html#support_for_other_use_cases)) -- On android, allow for other notification icon resource names than `ic_launcher` -- Explore options of background execution for iOS (help needed, I don't have any Mac/iOS devices or experience with programming for them) - ## Maintainer [Julian Aßmann](https://github.com/JulianAssmann) diff --git a/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt b/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt index 51afe22..73805cd 100644 --- a/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt +++ b/android/src/main/kotlin/de/julianassmann/flutter_background/FlutterBackgroundPlugin.kt @@ -38,6 +38,20 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { var notificationText: String? = "Keeps the flutter app running in the background" @JvmStatic var notificationImportance: Int? = NotificationCompat.PRIORITY_DEFAULT + + @JvmStatic + var notificationIconName: String? = "ic_launcher" + @JvmStatic + var notificationIconDefType: String? = "mipmap" + } + + private fun isValidResource(context: Context, name: String, defType: String, result: Result, errorCode: String): Boolean { + val resourceId = context.getResources().getIdentifier(name, defType, context.getPackageName()) + if (resourceId == 0) { + result.error("ResourceError", "The resource $defType/$name could not be found. Please make sure it has been added as a resource to your Android head project.", null) + return false + } + return true } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { @@ -58,23 +72,34 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { val title = call.argument("android.notificationTitle") val text = call.argument("android.notificationText") val importance = call.argument("android.notificationImportance") + val iconName = call.argument("android.notificationIconName") + val iconDefType = call.argument("android.notificationIconDefType") // Set static values so the IsolateHolderService can use them later on to configure the notification - notificationImportance = importance ?: NotificationCompat.PRIORITY_DEFAULT - notificationTitle = title ?: "flutter_background foreground service" - notificationText = text ?: "Keeps the flutter app running in the background" + notificationImportance = importance ?: notificationImportance + notificationTitle = title ?: notificationTitle + notificationText = text ?: text + notificationIconName = iconName ?: notificationIconName + notificationIconDefType = iconDefType ?: notificationIconDefType - // Ensure all the necessary permissions are granted, otherwise request them + if (permissionHandler!!.isWakeLockPermissionGranted() && permissionHandler!!.isIgnoringBatteryOptimizations()) { + result.success(true) + return + } + + // Ensure wake lock permissions are granted if (!permissionHandler!!.isWakeLockPermissionGranted()) { - result.error("PermissionError","Please add the WAKE_LOCK permission to the AndroidManifest.xml in order to use background_sockets.", "") - } else if (!permissionHandler!!.isIgnoringBatteryOptimizations()) { + result.error("PermissionError", "Please add the WAKE_LOCK permission to the AndroidManifest.xml in order to use background_sockets.", "") + return + } + + // Ensure ignoring battery optimizations is enabled + if (!permissionHandler!!.isIgnoringBatteryOptimizations()) { if (activity != null) { permissionHandler!!.requestBatteryOptimizationsOff(result, activity!!) } else { result.error("NoActivityError", "The plugin is not attached to an activity", "The plugin is not attached to an activity. This is required in order to request battery optimization to be off.") } - } else { - result.success(true) } } "enableBackgroundExecution" -> { diff --git a/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt b/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt index b31852f..4498c2b 100644 --- a/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt +++ b/android/src/main/kotlin/de/julianassmann/flutter_background/IsolateHolderService.kt @@ -53,7 +53,7 @@ class IsolateHolderService : Service() { notificationManager.createNotificationChannel(channel) } - val imageId = resources.getIdentifier("ic_launcher", "mipmap", packageName) + val imageId = resources.getIdentifier(FlutterBackgroundPlugin.notificationIconName, FlutterBackgroundPlugin.notificationIconDefType, packageName) val notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(FlutterBackgroundPlugin.notificationTitle) .setContentText(FlutterBackgroundPlugin.notificationText) diff --git a/example/android/app/src/main/res/drawable-anydpi-v24/background_icon.xml b/example/android/app/src/main/res/drawable-anydpi-v24/background_icon.xml new file mode 100644 index 0000000..92b52f5 --- /dev/null +++ b/example/android/app/src/main/res/drawable-anydpi-v24/background_icon.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/example/android/app/src/main/res/drawable-hdpi/background_icon.png b/example/android/app/src/main/res/drawable-hdpi/background_icon.png new file mode 100644 index 0000000..e3d29ad Binary files /dev/null and b/example/android/app/src/main/res/drawable-hdpi/background_icon.png differ diff --git a/example/android/app/src/main/res/drawable-mdpi/background_icon.png b/example/android/app/src/main/res/drawable-mdpi/background_icon.png new file mode 100644 index 0000000..812a131 Binary files /dev/null and b/example/android/app/src/main/res/drawable-mdpi/background_icon.png differ diff --git a/example/android/app/src/main/res/drawable-xhdpi/background_icon.png b/example/android/app/src/main/res/drawable-xhdpi/background_icon.png new file mode 100644 index 0000000..3478b6e Binary files /dev/null and b/example/android/app/src/main/res/drawable-xhdpi/background_icon.png differ diff --git a/example/android/app/src/main/res/drawable-xxhdpi/background_icon.png b/example/android/app/src/main/res/drawable-xxhdpi/background_icon.png new file mode 100644 index 0000000..8b5f869 Binary files /dev/null and b/example/android/app/src/main/res/drawable-xxhdpi/background_icon.png differ diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/example/lib/app.dart b/example/lib/app.dart index 80a7c43..da68673 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -5,11 +5,12 @@ import 'bloc/tcp_client_bloc/tcp_client_bloc.dart'; class BackgroundSocketsExampleApp extends StatefulWidget { @override - _BackgroundSocketsExampleAppState createState() => _BackgroundSocketsExampleAppState(); + _BackgroundSocketsExampleAppState createState() => + _BackgroundSocketsExampleAppState(); } -class _BackgroundSocketsExampleAppState extends State { - +class _BackgroundSocketsExampleAppState + extends State { @override void initState() { super.initState(); @@ -26,4 +27,4 @@ class _BackgroundSocketsExampleAppState extends State { print('hasPermissions: $hasPermissions'); } try { - hasPermissions = await FlutterBackground.initialize( - androidConfig: FlutterBackgroundAndroidConfig( - notificationTitle: 'flutter_background example app', - notificationText: 'Background notification for keeping the example app running in the background' - ) + final config = FlutterBackgroundAndroidConfig( + notificationTitle: 'flutter_background example app', + notificationText: + 'Background notification for keeping the example app running in the background', + notificationIcon: AndroidResource(name: 'background_icon'), ); + // Demonstrate calling initialize twice in a row is possible without causing problems. + hasPermissions = + await FlutterBackground.initialize(androidConfig: config); + hasPermissions = + await FlutterBackground.initialize(androidConfig: config); } catch (ex) { print(ex); } if (hasPermissions) { - final backgroundExecution = await FlutterBackground.enableBackgroundExecution(); + final backgroundExecution = + await FlutterBackground.enableBackgroundExecution(); if (backgroundExecution) { try { _socket = await Socket.connect(event.host, event.port); _socketStreamSub = _socket.asBroadcastStream().listen((event) { - add( - MessageReceived( + add(MessageReceived( message: Message( - message: String.fromCharCodes(event), - timestamp: DateTime.now(), - origin: MessageOrigin.Server, - ) - ) - ); + message: String.fromCharCodes(event), + timestamp: DateTime.now(), + origin: MessageOrigin.Server, + ))); }); _socket.handleError(() { add(ErrorOccured()); }); - yield state.copywith(connectionState: SocketConnectionState.Connected); + yield state.copywith( + connectionState: SocketConnectionState.Connected); } catch (err) { print(err); yield state.copywith(connectionState: SocketConnectionState.Failed); @@ -88,18 +92,18 @@ class TcpClientBloc extends Bloc { await _socketStreamSub?.cancel(); await _socket?.close(); await FlutterBackground.disableBackgroundExecution(); - yield state.copywith(connectionState: SocketConnectionState.None, messages: []); + yield state + .copywith(connectionState: SocketConnectionState.None, messages: []); } Stream _mapSendMessageToState(SendMessage event) async* { if (_socket != null) { yield state.copyWithNewMessage( - message: Message( - message: event.message, - timestamp: DateTime.now(), - origin: MessageOrigin.Client, - ) - ); + message: Message( + message: event.message, + timestamp: DateTime.now(), + origin: MessageOrigin.Client, + )); _socket.write(event.message); } } @@ -117,8 +121,9 @@ class TcpClientBloc extends Bloc { await _socket?.close(); } - Stream _mapMessageReceivedToState(MessageReceived event) async* { + Stream _mapMessageReceivedToState( + MessageReceived event) async* { await NotificationService().newNotification(event.message.message, false); yield state.copyWithNewMessage(message: event.message); } -} \ No newline at end of file +} diff --git a/example/lib/bloc/tcp_client_bloc/tcp_client_event.dart b/example/lib/bloc/tcp_client_bloc/tcp_client_event.dart index c0a2f53..12b3b8b 100644 --- a/example/lib/bloc/tcp_client_bloc/tcp_client_event.dart +++ b/example/lib/bloc/tcp_client_bloc/tcp_client_event.dart @@ -7,11 +7,13 @@ abstract class TcpClientEvent {} class Connect extends TcpClientEvent { /// The host of the server to connect to. final dynamic host; + /// The port of the server to connect to. final int port; Connect({@required this.host, @required this.port}) - : assert(host != null), assert(port != null); + : assert(host != null), + assert(port != null); @override String toString() => '''Connect { @@ -53,4 +55,4 @@ class SendMessage extends TcpClientEvent { @override String toString() => 'SendMessage { }'; -} \ No newline at end of file +} diff --git a/example/lib/bloc/tcp_client_bloc/tcp_client_state.dart b/example/lib/bloc/tcp_client_bloc/tcp_client_state.dart index 729ae90..047aa7d 100644 --- a/example/lib/bloc/tcp_client_bloc/tcp_client_state.dart +++ b/example/lib/bloc/tcp_client_bloc/tcp_client_state.dart @@ -12,9 +12,7 @@ class TcpClientState { factory TcpClientState.initial() { return TcpClientState( - connectionState: SocketConnectionState.None, - messages: [] - ); + connectionState: SocketConnectionState.None, messages: []); } TcpClientState copywith({ @@ -33,4 +31,4 @@ class TcpClientState { messages: List.from(messages)..add(message), ); } -} \ No newline at end of file +} diff --git a/example/lib/main.dart b/example/lib/main.dart index fb412b1..902f183 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,6 @@ - import 'package:flutter/material.dart'; import 'package:flutter_background_example/app.dart'; void main() { runApp(BackgroundSocketsExampleApp()); -} \ No newline at end of file +} diff --git a/example/lib/models/message.dart b/example/lib/models/message.dart index 64e2711..6ee78b0 100644 --- a/example/lib/models/message.dart +++ b/example/lib/models/message.dart @@ -1,18 +1,14 @@ import 'package:flutter/foundation.dart'; -enum MessageOrigin { - Client, - Server -} +enum MessageOrigin { Client, Server } class Message { final DateTime timestamp; final String message; final MessageOrigin origin; - Message({ - @required this.timestamp, - @required this.message, - @required this.origin - }); -} \ No newline at end of file + Message( + {@required this.timestamp, + @required this.message, + @required this.origin}); +} diff --git a/example/lib/models/socket_connection_state.dart b/example/lib/models/socket_connection_state.dart index 6f7825a..78a3664 100644 --- a/example/lib/models/socket_connection_state.dart +++ b/example/lib/models/socket_connection_state.dart @@ -4,4 +4,4 @@ enum SocketConnectionState { Connected, Failed, None -} \ No newline at end of file +} diff --git a/example/lib/pages/about_page.dart b/example/lib/pages/about_page.dart index ffd1831..ebe286c 100644 --- a/example/lib/pages/about_page.dart +++ b/example/lib/pages/about_page.dart @@ -29,4 +29,4 @@ class AboutPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index c26a881..9afa3fc 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -24,16 +24,14 @@ class _MainPageState extends State { @override void initState() { super.initState(); - _tcpBloc = BlocProvider.of(context); + _tcpBloc = BlocProvider.of(context); _hostEditingController = TextEditingController(text: '10.0.2.2'); _portEditingController = TextEditingController(text: '5555'); _chatTextEditingController = TextEditingController(text: ''); _chatTextEditingController.addListener(() { - setState(() { - - }); + setState(() {}); }); } @@ -46,107 +44,105 @@ class _MainPageState extends State { IconButton( icon: Icon(Icons.info_outline), onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) { - return AboutPage(); - } - )); + Navigator.of(context) + .push(MaterialPageRoute(builder: (BuildContext context) { + return AboutPage(); + })); }, ) ], ), body: BlocConsumer( - cubit: _tcpBloc, - listener: (BuildContext context, TcpClientState state) { - if (state.connectionState == SocketConnectionState.Connected) { - Scaffold.of(context) - ..hideCurrentSnackBar(); - } else if (state.connectionState == SocketConnectionState.Failed) { - Scaffold.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text('Connection failed'), Icon(Icons.error)], - ), - backgroundColor: Colors.red, - ), - ); - } else { - return Container(); - } - }, - builder: (context, state) { - if (state.connectionState == SocketConnectionState.None || state.connectionState == SocketConnectionState.Failed) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0), - child: ListView( - children: [ - TextFormField( - controller: _hostEditingController, - autovalidateMode: AutovalidateMode.always, - validator: (str) => isValidHost(str) ? null : 'Invalid hostname', - decoration: InputDecoration( - helperText: 'The ip address or hostname of the TCP server', - hintText: 'Enter the address here, e.g. 10.0.2.2', + cubit: _tcpBloc, + listener: (BuildContext context, TcpClientState state) { + if (state.connectionState == SocketConnectionState.Connected) { + Scaffold.of(context)..hideCurrentSnackBar(); + } else if (state.connectionState == SocketConnectionState.Failed) { + Scaffold.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('Connection failed'), Icon(Icons.error)], ), + backgroundColor: Colors.red, ), - TextFormField( - controller: _portEditingController, - autovalidateMode: AutovalidateMode.always, - validator: (str) => isValidPort(str) ? null : 'Invalid port', - decoration: InputDecoration( - helperText: 'The port the TCP server is listening on', - hintText: 'Enter the port here, e. g. 8000', + ); + } else { + return Container(); + } + }, + builder: (context, state) { + if (state.connectionState == SocketConnectionState.None || + state.connectionState == SocketConnectionState.Failed) { + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0), + child: ListView( + children: [ + TextFormField( + controller: _hostEditingController, + autovalidateMode: AutovalidateMode.always, + validator: (str) => + isValidHost(str) ? null : 'Invalid hostname', + decoration: InputDecoration( + helperText: + 'The ip address or hostname of the TCP server', + hintText: 'Enter the address here, e.g. 10.0.2.2', + ), ), - ), - RaisedButton( - child: Text('Connect'), - onPressed: isValidHost(_hostEditingController.text) && isValidPort(_portEditingController.text) - ? () { - _tcpBloc.add( - Connect( - host: _hostEditingController.text, - port: int.parse(_portEditingController.text) - ) - ); - } - : null, - ) - ], - ), - ); - } else if (state.connectionState == SocketConnectionState.Connecting) { - return Center( - child: Column( - children: [ - CircularProgressIndicator(), - Text('Connecting...'), - RaisedButton( - child: Text('Abort'), - onPressed: () { - _tcpBloc.add(Disconnect()); - }, - ) - ], - ), - ); - } else if (state.connectionState == SocketConnectionState.Connected) { - return Column( - children: [ + TextFormField( + controller: _portEditingController, + autovalidateMode: AutovalidateMode.always, + validator: (str) => + isValidPort(str) ? null : 'Invalid port', + decoration: InputDecoration( + helperText: 'The port the TCP server is listening on', + hintText: 'Enter the port here, e. g. 8000', + ), + ), + RaisedButton( + child: Text('Connect'), + onPressed: isValidHost(_hostEditingController.text) && + isValidPort(_portEditingController.text) + ? () { + _tcpBloc.add(Connect( + host: _hostEditingController.text, + port: + int.parse(_portEditingController.text))); + } + : null, + ) + ], + ), + ); + } else if (state.connectionState == + SocketConnectionState.Connecting) { + return Center( + child: Column( + children: [ + CircularProgressIndicator(), + Text('Connecting...'), + ], + ), + ); + } else if (state.connectionState == + SocketConnectionState.Connected) { + return Column(children: [ Expanded( child: Container( child: ListView.builder( - itemCount: state.messages.length, - itemBuilder: (context, idx) { - final m = state.messages[idx]; - return Bubble( - child: Text(m.message), - alignment: m.origin == MessageOrigin.Client ? Alignment.centerRight : Alignment.centerLeft, - ); - } - ), + itemCount: state.messages.length, + itemBuilder: (context, idx) { + final m = state.messages[idx]; + return Bubble( + child: Text(m.message), + alignment: m.origin == MessageOrigin.Client + ? Alignment.centerRight + : Alignment.centerLeft, + ); + }), ), ), Padding( @@ -155,20 +151,19 @@ class _MainPageState extends State { children: [ Expanded( child: TextField( - decoration: InputDecoration( - hintText: 'Message' - ), + decoration: InputDecoration(hintText: 'Message'), controller: _chatTextEditingController, ), ), IconButton( icon: Icon(Icons.send), onPressed: _chatTextEditingController.text.isEmpty - ? null - : () { - _tcpBloc.add(SendMessage(message: _chatTextEditingController.text)); - _chatTextEditingController.text = ''; - }, + ? null + : () { + _tcpBloc.add(SendMessage( + message: _chatTextEditingController.text)); + _chatTextEditingController.text = ''; + }, ) ], ), @@ -178,14 +173,12 @@ class _MainPageState extends State { onPressed: () { _tcpBloc.add(Disconnect()); }, - ), - ] - ); - } else { - return Container(); - } - } - ), + ), + ]); + } else { + return Container(); + } + }), ); } -} \ No newline at end of file +} diff --git a/example/lib/utils/notification_service.dart b/example/lib/utils/notification_service.dart index 42bfb99..bdf78e9 100644 --- a/example/lib/utils/notification_service.dart +++ b/example/lib/utils/notification_service.dart @@ -3,20 +3,19 @@ import 'dart:typed_data'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; /// The service used to display notifications and handle callbacks when the user taps on the notification. -/// +/// /// This is a singleton. Just call NotificationService() to get the singleton. class NotificationService { static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; FlutterLocalNotificationsPlugin plugin; NotificationService._internal() { - final initializationSettings = InitializationSettings( - AndroidInitializationSettings('@mipmap/ic_launcher'), - IOSInitializationSettings() - ); + AndroidInitializationSettings('@mipmap/ic_launcher'), + IOSInitializationSettings()); plugin = FlutterLocalNotificationsPlugin(); plugin.initialize(initializationSettings); @@ -33,27 +32,22 @@ class NotificationService { AndroidNotificationDetails androidNotificationDetails; final channelName = 'Text messages'; - + androidNotificationDetails = AndroidNotificationDetails( - channelName, channelName, channelName, - importance: Importance.Max, - priority: Priority.High, + channelName, channelName, channelName, + importance: Importance.Max, + priority: Priority.High, + vibrationPattern: vibration ? vibrationPattern : null, + enableVibration: vibration); - vibrationPattern: vibration ? vibrationPattern : null, - enableVibration: vibration); - var iOSPlatformChannelSpecifics = IOSNotificationDetails(); var notificationDetails = NotificationDetails( androidNotificationDetails, iOSPlatformChannelSpecifics); - + try { - await plugin.show( - 0, - msg, - msg, - notificationDetails); + await plugin.show(0, msg, msg, notificationDetails); } catch (ex) { print(ex); } } -} \ No newline at end of file +} diff --git a/example/lib/utils/validators.dart b/example/lib/utils/validators.dart index 1647828..f837c2b 100644 --- a/example/lib/utils/validators.dart +++ b/example/lib/utils/validators.dart @@ -1,14 +1,17 @@ /// Validates Hostname bool isValidHost(String str) { if (str == null || str.isEmpty) return false; - final ipAddressExp = RegExp(r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'); - final hostnameExp = RegExp(r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'); + final ipAddressExp = RegExp( + r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'); + final hostnameExp = RegExp( + r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'); return ipAddressExp.hasMatch(str) || hostnameExp.hasMatch(str); } /// Validates a TCP port bool isValidPort(String str) { if (str == null || str.isEmpty) return false; - final regex = RegExp(r'^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'); + final regex = RegExp( + r'^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'); return regex.hasMatch(str); -} \ No newline at end of file +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 498e8f7..8825e8e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -82,7 +82,7 @@ packages: path: ".." relative: true source: path - version: "0.1.5" + version: "0.1.6" flutter_bloc: dependency: "direct main" description: diff --git a/example/server/server.dart b/example/server/server.dart index 6dba3f9..87e99ec 100644 --- a/example/server/server.dart +++ b/example/server/server.dart @@ -14,7 +14,7 @@ Future startServer() async { 'New TCP client ${socket.address.address}:${socket.port} connected.'); var totalSeconds = 0; - final timer = Timer.periodic(Duration(seconds: 5), (timer) { + final timer = Timer.periodic(Duration(seconds: 60), (timer) { totalSeconds += timer.tick; socket.add(totalSeconds.toString().codeUnits); }); diff --git a/images/notification.png b/images/notification.png new file mode 100755 index 0000000..1a987ce Binary files /dev/null and b/images/notification.png differ diff --git a/lib/flutter_background.dart b/lib/flutter_background.dart index 91846a8..cc4b2f2 100644 --- a/lib/flutter_background.dart +++ b/lib/flutter_background.dart @@ -1,2 +1,2 @@ -export 'src/flutter_background.dart'; export 'src/android_config.dart'; +export 'src/flutter_background.dart'; diff --git a/lib/src/android_config.dart b/lib/src/android_config.dart index 3cff817..e54e855 100644 --- a/lib/src/android_config.dart +++ b/lib/src/android_config.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + /// Represents the importance of an android notification as described /// under https://developer.android.com/training/notify-user/channels#importance. enum AndroidNotificationImportance { @@ -8,6 +10,19 @@ enum AndroidNotificationImportance { Max, } +// Represents the information required to get an Android resource. +// See https://developer.android.com/reference/android/content/res/Resources for reference. +class AndroidResource { + // The name of the desired resource. + final String name; + + // Optional default resource type to find, if "type/" is not included in the name. Can be null to require an explicit type. + final String defType; + + const AndroidResource({@required this.name, this.defType = 'drawable'}) + : assert(name != null); +} + /// Android configuration for the [FlutterBackground] plugin. class FlutterBackgroundAndroidConfig { /// The importance of the notification used for the foreground service. @@ -19,16 +34,24 @@ class FlutterBackgroundAndroidConfig { /// The body used for the foreground service notification. final String notificationText; + /// The resource name of the icon to be used for the foreground notification. + final AndroidResource notificationIcon; + /// Creates an Android specific configuration for the [FlutterBackground] plugin. /// /// [notificationTitle] is the title used for the foreground service notification. /// [notificationText] is the body used for the foreground service notification. /// [notificationImportance] is the importance of the foreground service notification. + /// [notificationIcon] must be a drawable resource. + /// E. g. if the icon with name "background_icon" is in the "drawable" resource folder, + /// [notificationIcon] should be of value `AndroidResource(name: 'background_icon', defType: 'drawable'). /// It must be greater than [AndroidNotificationImportance.Min]. const FlutterBackgroundAndroidConfig( {this.notificationTitle = 'Notification title', this.notificationText = 'Notification text', - this.notificationImportance = AndroidNotificationImportance.Default}) + this.notificationImportance = AndroidNotificationImportance.Default, + this.notificationIcon = + const AndroidResource(name: 'ic_launcher', defType: 'mipmap')}) : assert(notificationTitle != null), assert(notificationText != null), assert(notificationImportance != null); diff --git a/lib/src/flutter_background.dart b/lib/src/flutter_background.dart index f9641be..363e178 100644 --- a/lib/src/flutter_background.dart +++ b/lib/src/flutter_background.dart @@ -23,7 +23,9 @@ class FlutterBackground { 'android.notificationText': androidConfig.notificationText, 'android.notificationImportance': _androidNotificationImportanceToInt( androidConfig.notificationImportance), - }) as bool; + 'android.notificationIconName': androidConfig.notificationIcon.name, + 'android.notificationIconDefType': androidConfig.notificationIcon.defType, + }); return _isInitialized; } @@ -43,7 +45,8 @@ class FlutterBackground { /// May throw a [PlatformException]. static Future enableBackgroundExecution() async { if (_isInitialized) { - final success = await _channel.invokeMethod('enableBackgroundExecution') as bool; + final success = + await _channel.invokeMethod('enableBackgroundExecution') as bool; _isBackgroundExecutionEnabled = true; return success; } else { diff --git a/pubspec.yaml b/pubspec.yaml index c04bb1b..18ccc57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_background description: A plugin to keep flutter apps running in the background by using foreground service, wake lock and disabling battery optimizations -version: 0.1.5 +version: 0.1.6 repository: https://github.com/JulianAssmann/flutter_background homepage: https://julianassmann.de/