Skip to content

Commit 7a6bcbc

Browse files
committed
feat: Add isTimeout param to the onDestroy callback
1 parent b23c177 commit 7a6bcbc

8 files changed

Lines changed: 53 additions & 24 deletions

File tree

README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,23 @@ As mentioned in the Android guidelines, to start a FG service on Android 14+, yo
7171
android:exported="false" />
7272
```
7373
74-
Check runtime requirements before starting the service. If this requirement is not met, the foreground service cannot be started.
75-
76-
<img src="https://github.com/Dev-hwang/flutter_foreground_task/assets/47127353/2a35dada-2c82-41f4-8a45-56776c88e9d3" width="700">
74+
> [!CAUTION]
75+
> Check [runtime requirements](https://developer.android.com/about/versions/14/changes/fgs-types-required#system-runtime-checks) before starting the service. If this requirement is not met, the foreground service cannot be started.
76+
77+
> [!CAUTION]
78+
> Android 15 introduces a new timeout behavior to `dataSync` for apps targeting Android 15 (API level 35) or higher.
79+
> The system permits an app's `dataSync` services to run for a total of 6 hours in a 24-hour period.
80+
> However, if the user brings the app to the foreground, the timer resets and the app has 6 hours available.
81+
>
82+
> There are new restrictions on `BOOT_COMPLETED(autoRunOnBoot)` broadcast receivers launching foreground services.
83+
> `BOOT_COMPLETED` receivers are not allowed to launch the following types of foreground services:
84+
> - [dataSync](https://developer.android.com/develop/background-work/services/fg-service-types#data-sync)
85+
> - [camera](https://developer.android.com/develop/background-work/services/fg-service-types#camera)
86+
> - [mediaPlayback](https://developer.android.com/develop/background-work/services/fg-service-types#media)
87+
> - [phoneCall](https://developer.android.com/develop/background-work/services/fg-service-types#phone-call)
88+
> - [microphone](https://developer.android.com/about/versions/14/changes/fgs-types-required#microphone)
89+
>
90+
> You can find how to test this behavior and more details at this [link](https://developer.android.com/about/versions/15/behavior-changes-15#fgs-hardening).
7791
7892
### :baby_chick: iOS
7993
@@ -212,8 +226,8 @@ class MyTaskHandler extends TaskHandler {
212226
213227
// Called when the task is destroyed.
214228
@override
215-
Future<void> onDestroy(DateTime timestamp) async {
216-
print('onDestroy');
229+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
230+
print('onDestroy(isTimeout: $isTimeout)');
217231
}
218232
219233
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
@@ -428,7 +442,7 @@ class FirstTaskHandler extends TaskHandler {
428442
}
429443
430444
@override
431-
Future<void> onDestroy(DateTime timestamp) async {
445+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
432446
// some code
433447
}
434448
}
@@ -459,7 +473,7 @@ class SecondTaskHandler extends TaskHandler {
459473
}
460474
461475
@override
462-
Future<void> onDestroy(DateTime timestamp) async {
476+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
463477
// some code
464478
}
465479
}
@@ -554,7 +568,7 @@ class MyTaskHandler extends TaskHandler {
554568
}
555569
556570
@override
557-
Future<void> onDestroy(DateTime timestamp) async {
571+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
558572
_streamSubscription?.cancel();
559573
_streamSubscription = null;
560574
}

android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ class ForegroundService : Service() {
9696
private var wakeLock: PowerManager.WakeLock? = null
9797
private var wifiLock: WifiManager.WifiLock? = null
9898

99+
private var isTimeout: Boolean = false
100+
99101
// A broadcast receiver that handles intents that occur in the foreground service.
100102
private var broadcastReceiver = object : BroadcastReceiver() {
101103
override fun onReceive(context: Context?, intent: Intent?) {
@@ -125,6 +127,7 @@ class ForegroundService : Service() {
125127
}
126128

127129
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
130+
isTimeout = false
128131
loadDataFromPreferences()
129132

130133
var action = foregroundServiceStatus.action
@@ -195,7 +198,8 @@ class ForegroundService : Service() {
195198

196199
override fun onDestroy() {
197200
super.onDestroy()
198-
destroyForegroundTask()
201+
val isTimeout = this.isTimeout
202+
destroyForegroundTask(isTimeout)
199203
stopForegroundService()
200204
unregisterBroadcastReceiver()
201205

@@ -221,13 +225,17 @@ class ForegroundService : Service() {
221225

222226
override fun onTimeout(startId: Int) {
223227
super.onTimeout(startId)
228+
isTimeout = true
224229
stopForegroundService()
230+
Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.")
225231
}
226232

227233
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
228234
override fun onTimeout(startId: Int, fgsType: Int) {
229235
super.onTimeout(startId, fgsType)
236+
isTimeout = true
230237
stopForegroundService()
238+
Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.")
231239
}
232240

233241
private fun loadDataFromPreferences() {
@@ -480,8 +488,8 @@ class ForegroundService : Service() {
480488
task?.update(taskEventAction = foregroundTaskOptions.eventAction)
481489
}
482490

483-
private fun destroyForegroundTask() {
484-
task?.destroy()
491+
private fun destroyForegroundTask(isTimeout: Boolean = false) {
492+
task?.destroy(isTimeout)
485493
task = null
486494
}
487495

android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class ForegroundTask(
152152
}
153153
}
154154

155-
fun destroy() {
155+
fun destroy(isTimeout: Boolean) {
156156
runIfNotDestroyed {
157157
stopRepeatTask()
158158

@@ -161,7 +161,7 @@ class ForegroundTask(
161161
taskLifecycleListener.onEngineWillDestroy()
162162
flutterEngine.destroy()
163163
} else {
164-
backgroundChannel.invokeMethod(ACTION_TASK_DESTROY, null) {
164+
backgroundChannel.invokeMethod(ACTION_TASK_DESTROY, isTimeout) {
165165
flutterEngine.destroy()
166166
}
167167
taskLifecycleListener.onTaskDestroy()

android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,19 @@ class RestartReceiver : BroadcastReceiver() {
8585
val nIntent = Intent(context, ForegroundService::class.java)
8686
ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART)
8787
ContextCompat.startForegroundService(context, nIntent)
88-
} catch (e: ForegroundServiceStartNotAllowedException){
88+
} catch (e: ForegroundServiceStartNotAllowedException) {
8989
Log.e(TAG, "Foreground service start not allowed exception: ${e.message}")
90+
} catch (e: Exception) {
91+
Log.e(TAG, e.toString())
9092
}
9193
} else {
92-
val nIntent = Intent(context, ForegroundService::class.java)
93-
ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART)
94-
ContextCompat.startForegroundService(context, nIntent)
94+
try {
95+
val nIntent = Intent(context, ForegroundService::class.java)
96+
ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART)
97+
ContextCompat.startForegroundService(context, nIntent)
98+
} catch (e: Exception) {
99+
Log.e(TAG, e.toString())
100+
}
95101
}
96102
}
97103
}

example/lib/main.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class MyTaskHandler extends TaskHandler {
4949

5050
// Called when the task is destroyed.
5151
@override
52-
Future<void> onDestroy(DateTime timestamp) async {
53-
print('onDestroy');
52+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
53+
print('onDestroy(isTimeout: $isTimeout)');
5454
}
5555

5656
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.

lib/flutter_foreground_task_method_channel.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform {
133133
handler.onRepeatEvent(timestamp);
134134
break;
135135
case 'onDestroy':
136-
await handler.onDestroy(timestamp);
136+
final bool isTimeout = call.arguments ?? false;
137+
await handler.onDestroy(timestamp, isTimeout);
137138
break;
138139
case 'onReceiveData':
139140
dynamic data = call.arguments;

lib/task_handler.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ abstract class TaskHandler {
1313
void onRepeatEvent(DateTime timestamp);
1414

1515
/// Called when the task is destroyed.
16-
Future<void> onDestroy(DateTime timestamp);
16+
Future<void> onDestroy(DateTime timestamp, bool isTimeout);
1717

1818
/// Called when data is sent using [FlutterForegroundTask.sendDataToTask].
1919
void onReceiveData(Object data) {}

test/task_handler_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ void main() {
6969
const String method = TaskEventMethod.onDestroy;
7070

7171
await platformChannel.mBGChannel.invokeMethod(method);
72-
expect(taskHandler.log.last, isTaskEvent(method));
72+
expect(taskHandler.log.last, isTaskEvent(method, false));
7373
});
7474

7575
test('onReceiveData', () async {
@@ -294,8 +294,8 @@ class TestTaskHandler extends TaskHandler {
294294
}
295295

296296
@override
297-
Future<void> onDestroy(DateTime timestamp) async {
298-
log.add(const TaskEvent(method: TaskEventMethod.onDestroy));
297+
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {
298+
log.add(TaskEvent(method: TaskEventMethod.onDestroy, data: isTimeout));
299299
}
300300

301301
@override

0 commit comments

Comments
 (0)