Skip to content

Commit

Permalink
Android: Improve initialize method, add custom notification icon.
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianAssmann committed Feb 23, 2021
1 parent 9ad25ca commit 1132324
Show file tree
Hide file tree
Showing 27 changed files with 282 additions and 216 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@

## 0.1.5

* Add `isBackgroundExecutionEnabled` property to enable checking the current background execution state
* 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
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,31 @@ 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.
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 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
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

```dart
bool hasPermissions = await FlutterBackground.hasPermissions;
```

before calling `FlutterBackground.initialize(...)`. If the app already has all necessary permissions, no permission requests will be displayed to the user.
before calling `FlutterBackground.initialize(...)`. If the app already has all necessary permissions,
no permission requests will be displayed to the user.

### Run app in background

Expand All @@ -72,40 +84,30 @@ With
bool success = await FlutterBackground.enableBackgroundExecution();
```

you can try to get the app running in the background. You must call `FlutterBackground.initialize()` before calling `FlutterBackground.enableBackgroundExecution()`.
you can try to get the app running in the background. You must call `FlutterBackground.initialize()`
before calling `FlutterBackground.enableBackgroundExecution()`.

With

```dart
await FlutterBackground.disableBackgroundExecution();
```

you can stop the background execution of the app. You must call `FlutterBackground.initialize()` before calling `FlutterBackground.disableBackgroundExecution()`.
you can stop the background execution of the app. You must call `FlutterBackground.initialize()`
before calling `FlutterBackground.disableBackgroundExecution()`.

To check whether background execution is currently enabled, use

```dart
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -58,23 +72,34 @@ class FlutterBackgroundPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
val title = call.argument<String>("android.notificationTitle")
val text = call.argument<String>("android.notificationText")
val importance = call.argument<Int>("android.notificationImportance")
val iconName = call.argument<String>("android.notificationIconName")
val iconDefType = call.argument<String>("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" -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</group>
</vector>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/android/settings_aar.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ':app'
9 changes: 5 additions & 4 deletions example/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<BackgroundSocketsExampleApp> {

class _BackgroundSocketsExampleAppState
extends State<BackgroundSocketsExampleApp> {
@override
void initState() {
super.initState();
Expand All @@ -26,4 +27,4 @@ class _BackgroundSocketsExampleAppState extends State<BackgroundSocketsExampleAp
),
);
}
}
}
53 changes: 29 additions & 24 deletions example/lib/bloc/tcp_client_bloc/tcp_client_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,37 +42,41 @@ class TcpClientBloc extends Bloc<TcpClientEvent, TcpClientState> {
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);
Expand All @@ -88,18 +92,18 @@ class TcpClientBloc extends Bloc<TcpClientEvent, TcpClientState> {
await _socketStreamSub?.cancel();
await _socket?.close();
await FlutterBackground.disableBackgroundExecution();
yield state.copywith(connectionState: SocketConnectionState.None, messages: []);
yield state
.copywith(connectionState: SocketConnectionState.None, messages: []);
}

Stream<TcpClientState> _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);
}
}
Expand All @@ -117,8 +121,9 @@ class TcpClientBloc extends Bloc<TcpClientEvent, TcpClientState> {
await _socket?.close();
}

Stream<TcpClientState> _mapMessageReceivedToState(MessageReceived event) async* {
Stream<TcpClientState> _mapMessageReceivedToState(
MessageReceived event) async* {
await NotificationService().newNotification(event.message.message, false);
yield state.copyWithNewMessage(message: event.message);
}
}
}
6 changes: 4 additions & 2 deletions example/lib/bloc/tcp_client_bloc/tcp_client_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -53,4 +55,4 @@ class SendMessage extends TcpClientEvent {

@override
String toString() => 'SendMessage { }';
}
}
6 changes: 2 additions & 4 deletions example/lib/bloc/tcp_client_bloc/tcp_client_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class TcpClientState {

factory TcpClientState.initial() {
return TcpClientState(
connectionState: SocketConnectionState.None,
messages: <Message>[]
);
connectionState: SocketConnectionState.None, messages: <Message>[]);
}

TcpClientState copywith({
Expand All @@ -33,4 +31,4 @@ class TcpClientState {
messages: List.from(messages)..add(message),
);
}
}
}
3 changes: 1 addition & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import 'package:flutter/material.dart';
import 'package:flutter_background_example/app.dart';

void main() {
runApp(BackgroundSocketsExampleApp());
}
}
16 changes: 6 additions & 10 deletions example/lib/models/message.dart
Original file line number Diff line number Diff line change
@@ -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
});
}
Message(
{@required this.timestamp,
@required this.message,
@required this.origin});
}
2 changes: 1 addition & 1 deletion example/lib/models/socket_connection_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ enum SocketConnectionState {
Connected,
Failed,
None
}
}
2 changes: 1 addition & 1 deletion example/lib/pages/about_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ class AboutPage extends StatelessWidget {
),
);
}
}
}
Loading

0 comments on commit 1132324

Please sign in to comment.