From dcefd53bfedc9a10cf78fb44a3fd697af71eb7d4 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Tue, 1 Nov 2022 03:02:06 +0100 Subject: [PATCH] fix(mobile,Android): throttle detail progress notifications & wait on foregroundInfo (#907) --- .../kotlin/com/example/mobile/BackupWorker.kt | 18 +++++- .../background.service.dart | 60 ++++++++++++++----- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 116422634..8337dce29 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -50,6 +50,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private var timeBackupStarted: Long = 0L private var notificationBuilder: NotificationCompat.Builder? = null private var notificationDetailBuilder: NotificationCompat.Builder? = null + private var fgFuture: ListenableFuture? = null override fun startWork(): ListenableFuture { @@ -112,6 +113,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct Handler(Looper.getMainLooper()).postAtFrontOfQueue { backgroundChannel.invokeMethod("systemStop", null) } + waitOnSetForegroundAsync() // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) // instead, wait for 5 seconds until forcefully stopping backup work Handler(Looper.getMainLooper()).postDelayed({ @@ -119,6 +121,17 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct }, 5000) } + private fun waitOnSetForegroundAsync() { + val fgFuture = this.fgFuture + if (fgFuture != null && !fgFuture.isCancelled() && !fgFuture.isDone()) { + try { + fgFuture.get(500, TimeUnit.MILLISECONDS) + } + catch (e: Exception) { + // ignored, there is nothing to be done + } + } + } private fun stopEngine(result: Result?) { if (result != null) { @@ -128,6 +141,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct engine?.destroy() engine = null clearBackgroundNotification() + waitOnSetForegroundAsync() } override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { @@ -207,8 +221,8 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private fun showInfo(notification: Notification, isDetail: Boolean = false) { val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - if (isIgnoringBatteryOptimizations) { - setForegroundAsync(ForegroundInfo(id, notification)) + if (isIgnoringBatteryOptimizations && !isDetail) { + fgFuture = setForegroundAsync(ForegroundInfo(id, notification)) } else { notificationManager.notify(id, notification) } diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index be0f3991e..377824ec0 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -43,8 +43,9 @@ class BackgroundService { bool _errorGracePeriodExceeded = true; int _uploadedAssetsCount = 0; int _assetsToUploadCount = 0; - int _lastDetailProgressUpdate = 0; String _lastPrintedProgress = ""; + late final _Throttle _throttleNotificationUpdates = + _Throttle(_updateDetailProgress, const Duration(milliseconds: 400)); bool get isBackgroundInitialized { return _isBackgroundInitialized; @@ -447,21 +448,20 @@ class BackgroundService { } void _onProgress(int sent, int total) { - final int now = Timeline.now; - // limit updates to 10 per second (or Android drops important notifications) - if (now > _lastDetailProgressUpdate + 100000) { - final String msg = _humanReadableBytesProgress(sent, total); - // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) - if (msg != _lastPrintedProgress) { - _lastDetailProgressUpdate = now; - _lastPrintedProgress = msg; - _updateNotification( - progress: sent, - max: total, - isDetail: true, - content: msg, - ); - } + _throttleNotificationUpdates(sent, total); + } + + void _updateDetailProgress(int sent, int total) { + final String msg = _humanReadableBytesProgress(sent, total); + // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) + if (msg != _lastPrintedProgress) { + _lastPrintedProgress = msg; + _updateNotification( + progress: sent, + max: total, + isDetail: true, + content: msg, + ); } } @@ -532,6 +532,34 @@ class BackgroundService { } } +class _Throttle { + _Throttle(this._fun, Duration interval) : _interval = interval.inMicroseconds; + final void Function(int, int) _fun; + final int _interval; + int _invokedAt = 0; + Timer? _timer; + int _progress = 0; + int _total = 0; + + void call(int progress, int total) { + final time = Timeline.now; + _progress = progress; + _total = total; + if (time > _invokedAt + _interval) { + _timer?.cancel(); + _onTimeElapsed(); + } else { + _timer ??= Timer(Duration(microseconds: _interval), _onTimeElapsed); + } + } + + void _onTimeElapsed() { + _invokedAt = Timeline.now; + _fun(_progress, _total); + _timer = null; + } +} + /// entry point called by Kotlin/Java code; needs to be a top-level function @pragma('vm:entry-point') void _nativeEntry() {