Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android - Long file uploads in suspended state stop silently until app is reopened in foreground #445

Open
G7r7 opened this issue Jan 24, 2025 · 3 comments
Labels

Comments

@G7r7
Copy link
Contributor

G7r7 commented Jan 24, 2025

Hello,

On version 8.9.3, Huawei P40 Pro, EMUI 12 (Android 10):

I am working on implementing really long background uploads.

I am now struggling a bit to keep the tasks running and to be able to restart them when keeping the app in a suspended state.

I use the enquing method for the scheduling of tasks:

@pragma('vm:entry-point')
Future<String> testWebAPIUpload(Map<String, dynamic> json) async {
  try {
    UploadParams params = UploadParams.fromJson(json);
    File persistentFile = File(params.localFilePath);

    if (!persistentFile.existsSync()) {
      debugPrint('File does not exist after moving: ${persistentFile.path}');
      return UploadFileResult.fileNotFound.toJson();
    }

    final (baseDirectory, directory, filename) = await Task.split(filePath: persistentFile.path);

    await FileDownloader().enqueue(
      UploadTask(
        filename: filename,
        directory: directory,
        baseDirectory: baseDirectory,
        url: '${params.webApiBaseUrl}/projects/${params.projectId}/rushfile',
        urlQueryParameters: { 'jwt': params.token, 'filepath': params.distantFilePath },
        post: 'binary',
        httpRequestMethod: 'POST',
        mimeType: 'application/octet-stream',
        updates: Updates.statusAndProgress,
        metaData: jsonEncode({'localFilePath': persistentFile.path, 'distantFilePath': params.distantFilePath, 'projectId': params.projectId, 'webApiBaseUrl': params.webApiBaseUrl, 'token': params.token}),
        // options: TaskOptions(onTaskStart: onUploadTaskStart)
        options: TaskOptions(onTaskStart: onUploadTaskStart)
      )
    );

    return UploadFileResult.uploadEnqueued.toJson();      
  } catch (e) {
    debugPrint('Enqueueing of upload of file failed for an unkown reason: $e');
    return UploadFileResult.unknownError.toJson();
  }
}
I/BackgroundDownloader(31515): Enqueuing task with id 1140406881
D/HwCustConnectivityManagerImpl(31515): isBlockNetworkRequestByNonAis, INVALID_SUBSCRIPTION_ID
I/ConnectivityManager(31515): requestNetwork and the calling app is: com.bbflight.background_downloader_example
I/TaskWorker(31515): onTaskStart callback for taskId 1140406881
I/TaskWorker(31515): Starting task with taskId 1140406881
I/NotificationManager(31515): com.bbflight.background_downloader_example: cancel(0)
I/flutter (31515): Task with id 1140406881 is now running
D/TaskWorker(31515): Binary upload for taskId 1140406881

I use the runInForeground option, to allow me to go longer than the 9 minutes mark, but it doesn't have an impact yet, since the stops occurs after 6 minutes:

    FileDownloader().configure(globalConfig: [
      (Config.requestTimeout, const Duration(seconds: 30)),
      (Config.runInForeground, Config.always)
    ], androidConfig: [
      (Config.useCacheDir, Config.whenAble),
    ], iOSConfig: [
      (Config.localize, {'Cancel': 'StopIt'}),
    ]).then((result) => {
    });

I am currently trying this approach for failed tasks (which all eventualy do while suspended after few minutes in suspended state):

// Listen to updates and process
    FileDownloader().updates.listen((update) async {
      switch (update) {
        case TaskStatusUpdate():
          switch (update.status) {
            case TaskStatus.paused:
              loadUploadResult = 'Paused ...';
              break;
            case TaskStatus.canceled:
              debugPrint('Task with id ${update.task.taskId} has been cancelled');
              break;
            case TaskStatus.enqueued:
              debugPrint('Task with id ${update.task.taskId} is enqueued');
              break;
            case TaskStatus.complete:
              debugPrint('Task with id ${update.task.taskId} is complete');
              try {
                var path = jsonDecode(update.task.metaData)['localFilePath'];
                debugPrint('Deleting file from persitent storage: $path');
                var file = File(path);
                if(!file.existsSync()) {
                  debugPrint("Could not delete file, file not found");
                  break;
                }
                file.deleteSync();
                debugPrint("File deleted from persitent storage");
                break;
              } catch (e) {
                debugPrint('Could not delete file from persitent storage: $e');
                break;
              }
            case TaskStatus.failed:
              debugPrint('Task with id ${update.task.taskId} has failed.');
              debugPrint('Task with id ${update.task.taskId} failure exeption: ${update.exception}');
              debugPrint('Task with id ${update.task.taskId} failure responseBody: ${update.responseBody}');
              debugPrint('Trying to re-enqueue task with id ${update.task.taskId}');
              try {
                bool enqueued = await FileDownloader().enqueue(update.task);
                debugPrint('Task with id ${update.task.taskId} re-enqueued successfuly : $enqueued');
              } catch (e) {
                debugPrint('Could not re-enqueue task with id ${update.task.taskId}');
                break;
              }
              break;
            case TaskStatus.running:
              debugPrint('Task with id ${update.task.taskId} is now running');
              break;
            default:
          }

With this setup, the typical big file upload goes as follow with task running in the background (app reduced and phone locked):

  • File uploaded steadily for around (~6 minutes).
  • Uploading stops silently (no more bytes sent to server, no errors, no new logs).
  • Then I could wait indefinetly, but nothing else happens.
  • Then finally when reopening the app to the foreground, I get an java.net.ProtocolException: unexpected end of stream:
W/TaskWorker(25065): Error for taskId 1140406881: unexpected end of stream
W/TaskWorker(25065): java.net.ProtocolException: unexpected end of stream
W/TaskWorker(25065): 	at com.android.okhttp.internal.http.Http1xStream$FixedLengthSink.close(Http1xStream.java:302)
W/TaskWorker(25065): 	at com.android.okhttp.okio.RealBufferedSink.close(RealBufferedSink.java:242)
W/TaskWorker(25065): 	at com.android.okhttp.okio.RealBufferedSink$1.close(RealBufferedSink.java:210)
2
W/TaskWorker(25065): 	at java.io.FilterOutputStream.close(FilterOutputStream.java:159)
W/TaskWorker(25065): 	at kotlin.io.CloseableKt.closeFinally(Closeable.kt:56)
W/TaskWorker(25065): 	at com.bbflight.background_downloader.UploadTaskWorker$processBinaryUpload$3.invokeSuspend(UploadTaskWorker.kt:216)
W/TaskWorker(25065): 	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
W/TaskWorker(25065): 	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
W/TaskWorker(25065): 	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
W/TaskWorker(25065): 	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:100)
W/TaskWorker(25065): 	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
W/TaskWorker(25065): 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
W/TaskWorker(25065): 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
W/TaskWorker(25065): 	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
I/flutter (25065): Task with id 1140406881 has failed.
I/flutter (25065): Task with id 1140406881 failure exeption: TaskException: Error for url https://redacted&filepath=VID_20250115_110801.mp4&range=bytes%3D1747361792-4224690610&x-total-size=4224690611 and /data/user/0/com.bbflight.background_downloader_example/files/VID_20250115_110801.mp4: unexpe
I/flutter (25065): Task with id 1140406881 failure responseBody: null
  • Then my re-enquing mechanism kicks-in:
I/flutter (25065): Trying to re-enqueue task with id 1140406881

With a simple (as stated above):

    bool result = await FileDownloader().enqueue(task);

But what would be the best scenario, would be to allow the renquing process and failure detection to take place all while the app is suspended.


For reference , when staying with the app opened the whole time, the uploading also stops after 6 minutes:

  • Task stop uploading silently after 6 minutes.
  • After ~3 minutes the task fail is detected with the following error:
I/TaskWorker(31515): Could not read response body from httpResponseCode 502: java.io.FileNotFoundException: https://redacted&filepath=VID_20250115_110801.mp4&range=bytes%3D3097788416-4224690610&x-total-size=4224690611
I/TaskWorker(31515): Response code 502 for upload of /data/user/0/com.bbflight.background_downloader_example/files/VID_20250115_110801.mp4 to https://redacted&filepath=VID_20250115_110801.mp4&range=bytes%3D3097788416-4224690610&x-total-size=4224690611
I/flutter (31515): Task with id 2316321278 has failed.
I/flutter (31515): Task with id 2316321278 failure exeption: TaskHttpException, response code 502: <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
I/flutter (31515): <html><head>
I/flutter (31515): <title>502 Bad Gateway</title>
I/flutter (31515): </head><body>
I/flutter (31515): <h1>Bad Gateway</h1>
I/flutter (31515): <p>The proxy server received an invalid
I/flutter (31515): response from an upstream server.<br />
I/flutter (31515): </p>
I/flutter (31515): </body></html>
I/flutter (31515): 
I/WM-WorkerWrapper(31515): Work [ id=958e0f38-0f22-4e15-983e-64563af733a9, tags={ com.bbflight.background_downloader.UploadTaskWorker, BackgroundDownloader, taskId=2316321278, group=default } ] was cancelled
I/WM-WorkerWrapper(31515): java.util.concurrent.CancellationException: Task was cancelled.
I/WM-WorkerWrapper(31515): 	at androidx.work.impl.utils.futures.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1183)
I/WM-WorkerWrapper(31515): 	at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:513)
I/WM-WorkerWrapper(31515): 	at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:474)
I/WM-WorkerWrapper(31515): 	at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:316)
I/WM-WorkerWrapper(31515): 	at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
I/WM-WorkerWrapper(31515): 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
I/WM-WorkerWrapper(31515): 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
I/WM-WorkerWrapper(31515): 	at java.lang.Thread.run(Thread.java:929)
I/flutter (31515): Task with id 2316321278 failure responseBody: null
I/flutter (31515): Trying to re-enqueue task with id 2316321278
D/BackgroundDownloader(31515): Registering callbackDispatcher handle 6720582723587629712
  • Then the task is re-enqueued without issues:

I/NotificationManager(31515): com.bbflight.background_downloader_example: cancel(0)
I/flutter (31515): BaseDownloader>WARNING: 2025-01-24 15:26:47.747305: Could not register callbackDispatcher
I/BackgroundDownloader(31515): Enqueuing task with id 2316321278
D/HwCustConnectivityManagerImpl(31515): isBlockNetworkRequestByNonAis, INVALID_SUBSCRIPTION_ID
I/ConnectivityManager(31515): requestNetwork and the calling app is: com.bbflight.background_downloader_example
I/flutter (31515): Task with id 2316321278 re-enqueued successfuly : true
I/flutter (31515): Task with id 2316321278 is enqueued
I/TaskWorker(31515): onTaskStart callback for taskId 2316321278
I/TaskWorker(31515): Starting task with taskId 2316321278
I/flutter (31515): Task with id 2316321278 is now running
I/TaskWorker(31515): TaskId 2316321278 will run in foreground
D/TaskWorker(31515): Binary upload for taskId 2316321278

A thing to be noted in the log is that the renqueued task is marked as it will run in the foreground but not at initial enqueue. Maybe a configuration bad timing on my end ?

Edit:

I changed my use of flutterCompute for the initial enqueueing process, for a regural function call and now the task is started in foreground.

Thanks a lot for your work.

@781flyingdutchman
Copy link
Owner

781flyingdutchman commented Jan 24, 2025

The biggest issue unfortunately is this: https://www.reddit.com/r/Huawei/comments/1bj0vic/what_is_this_shit/?rdt=58878

and that is outside of the control of Flutter or the plugin. What happens when the task is killed is complete silence - there is no way to react to that (i.e. no callback are called, no status updates given, etc). In the latest version of the package, if you track tasks in the database and use start or rescheduleKilledTasks (one or the other, not both) then all killed tasks will automatically be rescheduled.

You should not use the plugin from anything other than the main isolate, so you cannot call it using compute.

@G7r7
Copy link
Contributor Author

G7r7 commented Jan 27, 2025

Hello,

I see, I will limit myself to the main isolate then. I will experiment with the database tracking to see if I can make my tasks finish.

Thank you

Copy link

This issue is stale because it has been open for 14 days with no activity.

@github-actions github-actions bot added the stale label Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants