From 0153079fdab946f3780d24c7694dc386c7a5ef0b Mon Sep 17 00:00:00 2001 From: Curvel Date: Fri, 21 Aug 2020 10:49:30 +0200 Subject: [PATCH 01/31] Keep progress and result stream instance --- lib/src/flutter_uploader.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/flutter_uploader.dart b/lib/src/flutter_uploader.dart index 5e5a6fc..515195a 100644 --- a/lib/src/flutter_uploader.dart +++ b/lib/src/flutter_uploader.dart @@ -5,6 +5,9 @@ class FlutterUploader { final EventChannel _progressChannel; final EventChannel _resultChannel; + Stream _progressStream; + Stream _resultStream; + static FlutterUploader _instance; factory FlutterUploader() { @@ -40,7 +43,7 @@ class FlutterUploader { /// stream to listen on upload progress /// Stream get progress { - return _progressChannel + return _progressStream ??= _progressChannel .receiveBroadcastStream() .map>((event) => Map.from(event)) .transform(StreamTransformer, @@ -62,7 +65,7 @@ class FlutterUploader { /// stream to listen on upload result /// Stream get result { - return _resultChannel + return _resultStream ??= _resultChannel .receiveBroadcastStream() .map>((event) => Map.from(event)) .transform( From 13bf97db57a245bb0d39c2c6971f62385653f5fa Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 15:35:12 +0100 Subject: [PATCH 02/31] Bump to Flutter 1.20.2 stable --- .ci/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 8549a95..43e631f 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,5 +1,5 @@ # https://github.com/cirruslabs/docker-images-flutter/blob/master/.cirrus.yml -FROM cirrusci/flutter:1.17.5 +FROM cirrusci/flutter:1.20.2 RUN yes | sdkmanager \ "platforms;android-29" \ From 863222f0757efda299525df82c36da10fae84a7d Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 15:35:36 +0100 Subject: [PATCH 03/31] Support custom "Accept" header --- .../flutteruploader/UploadWorker.java | 6 ++-- example/README.md | 8 ++++++ example/ios/Runner.xcodeproj/project.pbxproj | 5 ---- .../test_driver/flutter_uploader_test.dart | 28 +++++++++++++++++++ lib/flutter_uploader.dart | 2 +- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java index 45463f6..abec7e1 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java @@ -195,11 +195,13 @@ public Result doWorkInternal() { RequestBody requestBody = new CountingRequestBody(innerRequestBody, getId().toString(), this); Request.Builder requestBuilder = new Request.Builder(); + requestBuilder.addHeader("Accept", "*/*"); + if (headers != null) { for (String key : headers.keySet()) { String header = headers.get(key); if (header != null && !header.isEmpty()) { - requestBuilder = requestBuilder.addHeader(key, header); + requestBuilder = requestBuilder.header(key, header); } } } @@ -214,8 +216,6 @@ public Result doWorkInternal() { null)); } - requestBuilder.addHeader("Accept", "application/json; charset=utf-8"); - Request request; switch (method.toUpperCase()) { diff --git a/example/README.md b/example/README.md index 154cbff..b1f7e5b 100644 --- a/example/README.md +++ b/example/README.md @@ -29,3 +29,11 @@ firebase deploy ``` 6. run example app + +## Driver tests + +Run the current end to end test suite: + +``` +flutter drive --driver=test_driver/flutter_uploader_e2e_test.dart test_driver/flutter_uploader_test.dart +``` \ No newline at end of file diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 1b61add..b5aeb65 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -296,7 +296,6 @@ "${BUILT_PRODUCTS_DIR}/e2e/e2e.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", - "${BUILT_PRODUCTS_DIR}/flutter_plugin_android_lifecycle/flutter_plugin_android_lifecycle.framework", "${BUILT_PRODUCTS_DIR}/flutter_uploader/flutter_uploader.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", @@ -313,7 +312,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/e2e.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_plugin_android_lifecycle.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_uploader.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", @@ -360,7 +358,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -441,7 +438,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -499,7 +495,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; diff --git a/example/test_driver/flutter_uploader_test.dart b/example/test_driver/flutter_uploader_test.dart index 0eea75e..a6276e7 100644 --- a/example/test_driver/flutter_uploader_test.dart +++ b/example/test_driver/flutter_uploader_test.dart @@ -58,9 +58,25 @@ void main() { expect(json['message'], 'Successfully uploaded'); expect(res.statusCode, 200); + expect(json['request']['headers']['accept'], '*/*'); expect(res.status, UploadTaskStatus.complete); }); + testWidgets("can overwrite 'Accept' header", (WidgetTester tester) async { + var fileItem = FileItem(path: await _tmpFile(), field: "file"); + + final taskId = await uploader.enqueue( + url: url.toString(), + files: [fileItem], + headers: {'Accept': 'application/json, charset=utf-8'}, + ); + final res = await uploader.result.firstWhere(isCompleted(taskId)); + final json = jsonDecode(res.response); + + expect(json['request']['headers']['accept'], + 'application/json, charset=utf-8'); + }); + testWidgets("multiple files", (WidgetTester tester) async { final taskId = await uploader.enqueue(url: url.toString(), files: [ FileItem(path: await _tmpFile(256), field: "file1"), @@ -129,9 +145,21 @@ void main() { expect(json['message'], 'Successfully uploaded'); expect(res.statusCode, 200); + expect(json['headers']['accept'], '*/*'); expect(res.status, UploadTaskStatus.complete); }); + testWidgets("can overwrite 'Accept' header", (WidgetTester tester) async { + final taskId = await uploader.enqueueBinary( + url: url.toString(), + path: await _tmpFile(), + headers: {'Accept': 'application/json, charset=utf-8'}, + ); + final res = await uploader.result.firstWhere(isCompleted(taskId)); + final json = jsonDecode(res.response); + + expect(json['headers']['accept'], 'application/json, charset=utf-8'); + }); testWidgets("fowards errors", (WidgetTester tester) async { final taskId = await uploader.enqueueBinary( url: url.replace(queryParameters: {'simulate': 'error500'}).toString(), diff --git a/lib/flutter_uploader.dart b/lib/flutter_uploader.dart index 310f206..f7850c2 100644 --- a/lib/flutter_uploader.dart +++ b/lib/flutter_uploader.dart @@ -1,7 +1,7 @@ library flutter_uploader; import 'dart:async'; -import 'dart:ui'; +import 'dart:ui' show PluginUtilities; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; From 8e90e949806c6ec69318a368ad15675b58f282e4 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 17:43:44 +0100 Subject: [PATCH 04/31] Refactor plugin a bit & add unit test for multiple subscribers to result/progress --- lib/src/flutter_uploader.dart | 68 +++++++++++++++------------------ test/flutter_uploader_test.dart | 62 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/lib/src/flutter_uploader.dart b/lib/src/flutter_uploader.dart index 515195a..6905887 100644 --- a/lib/src/flutter_uploader.dart +++ b/lib/src/flutter_uploader.dart @@ -46,19 +46,19 @@ class FlutterUploader { return _progressStream ??= _progressChannel .receiveBroadcastStream() .map>((event) => Map.from(event)) - .transform(StreamTransformer, - UploadTaskProgress>.fromHandlers( - handleData: - (Map map, EventSink sink) { - String id = map['taskId']; - int status = map['status']; - int uploadProgress = map['progress']; - final t = UploadTaskProgress( - id, uploadProgress, UploadTaskStatus.from(status)); - - sink.add(t); - }, - )); + .map(_parseProgress); + } + + UploadTaskProgress _parseProgress(Map map) { + String id = map['taskId']; + int status = map['status']; + int uploadProgress = map['progress']; + + return UploadTaskProgress( + id, + uploadProgress, + UploadTaskStatus.from(status), + ); } /// @@ -68,30 +68,24 @@ class FlutterUploader { return _resultStream ??= _resultChannel .receiveBroadcastStream() .map>((event) => Map.from(event)) - .transform( - StreamTransformer, UploadTaskResponse>.fromHandlers( - handleData: - (Map value, EventSink sink) { - String id = value['taskId']; - String message = value['message']; - // String code = value['code']; - int status = value["status"]; - int statusCode = value["statusCode"]; - Map headers = value['headers'] != null - ? Map.from(value['headers']) - : {}; - - final r = UploadTaskResponse( - taskId: id, - status: UploadTaskStatus.from(status), - statusCode: statusCode, - headers: headers, - response: message, - ); - - sink.add(r); - }, - ), + .map(_parseResult); + } + + UploadTaskResponse _parseResult(Map map) { + String id = map['taskId']; + String message = map['message']; + // String code = value['code']; + int status = map["status"]; + int statusCode = map["statusCode"]; + Map headers = + map['headers'] != null ? Map.from(map['headers']) : {}; + + return UploadTaskResponse( + taskId: id, + status: UploadTaskStatus.from(status), + statusCode: statusCode, + headers: headers, + response: message, ); } diff --git a/test/flutter_uploader_test.dart b/test/flutter_uploader_test.dart index 29e2872..50b383b 100644 --- a/test/flutter_uploader_test.dart +++ b/test/flutter_uploader_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/services.dart'; @@ -16,6 +18,9 @@ void main() { EventChannel progressChannel; EventChannel resultChannel; + StreamController> progressController; + StreamController> resultController; + final List log = []; setUp(() { @@ -27,12 +32,25 @@ void main() { progressChannel = MockEventChannel(); resultChannel = MockEventChannel(); + progressController = StreamController(); + resultController = StreamController(); + + when(progressChannel.receiveBroadcastStream()) + .thenAnswer((_) => progressController.stream.asBroadcastStream()); + when(resultChannel.receiveBroadcastStream()) + .thenAnswer((_) => resultController.stream.asBroadcastStream()); + uploader = FlutterUploader.private(methodChannel, progressChannel, resultChannel); log.clear(); }); + tearDown(() { + progressController.close(); + resultController.close(); + }); + group('FlutterUploader', () { group('setBackgroundHandler', () { test('passes the arguments correctly', () async { @@ -142,6 +160,50 @@ void main() { ]); }); }); + group("progress stream", () { + testWidgets("supports multiple subscriptions", + (WidgetTester tester) async { + const fakeTaskId = '123123'; + + final Completer c1 = Completer(); + final Completer c2 = Completer(); + + uploader.progress.take(1).listen((event) => c1.complete(event.taskId)); + uploader.progress.take(1).listen((event) => c2.complete(event.taskId)); + + progressController.add({ + 'taskId': fakeTaskId, + 'message': '123', + 'status': 200, + 'statusCode': 120, + }); + + expect(await c1.future, fakeTaskId); + expect(await c2.future, fakeTaskId); + }); + }); + }); + + group("result stream", () { + testWidgets("supports multiple subscriptions", (WidgetTester tester) async { + const fakeTaskId = '123123'; + + final Completer c1 = Completer(); + final Completer c2 = Completer(); + + uploader.result.take(1).listen((event) => c1.complete(event.taskId)); + uploader.result.take(1).listen((event) => c2.complete(event.taskId)); + + resultController.add({ + 'taskId': fakeTaskId, + 'message': '123', + 'status': 200, + 'statusCode': 120, + }); + + expect(await c1.future, fakeTaskId); + expect(await c2.future, fakeTaskId); + }); }); } From 44625773b7667d88a1b137500232f6e1b75785d2 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 17:51:06 +0100 Subject: [PATCH 05/31] resolve analyzer warnings --- test/flutter_uploader_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/flutter_uploader_test.dart b/test/flutter_uploader_test.dart index 50b383b..47f11b5 100644 --- a/test/flutter_uploader_test.dart +++ b/test/flutter_uploader_test.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/services.dart'; From 8db09f5e019f70159c19930288768e485c50bc1f Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 18:24:39 +0100 Subject: [PATCH 06/31] Basic refactor from multiple methods to enqueue an upload to one --- example/lib/upload_screen.dart | 40 ++++---- .../test_driver/flutter_uploader_test.dart | 49 ++++++---- lib/flutter_uploader.dart | 2 + lib/src/flutter_uploader.dart | 92 +++++-------------- lib/src/upload.dart | 67 ++++++++++++++ test/flutter_uploader_test.dart | 34 ++++--- 6 files changed, 166 insertions(+), 118 deletions(-) create mode 100644 lib/src/upload.dart diff --git a/example/lib/upload_screen.dart b/example/lib/upload_screen.dart index 3bd6444..4582baa 100644 --- a/example/lib/upload_screen.dart +++ b/example/lib/upload_screen.dart @@ -191,6 +191,12 @@ class _UploadScreenState extends State { final prefs = await SharedPreferences.getInstance(); final binary = prefs.getBool('binary') ?? false; + await widget.uploader.enqueue(_buildUpload(binary, paths)); + + widget.onUploadStarted(); + } + + Upload _buildUpload(bool binary, List paths) { final tag = "upload"; Uri url = binary @@ -201,23 +207,21 @@ class _UploadScreenState extends State { 'simulate': _serverBehavior.name, }); - print('URL: $url'); - - binary - ? await widget.uploader.enqueueBinary( - url: url.toString(), - path: paths.first, - method: UploadMethod.POST, - tag: tag, - ) - : await widget.uploader.enqueue( - url: url.toString(), - data: {"name": "john"}, - files: paths.map((e) => FileItem(path: e, field: 'file')).toList(), - method: UploadMethod.POST, - tag: tag, - ); - - widget.onUploadStarted(); + if (binary) { + return RawUpload( + url: url.toString(), + path: paths.first, + method: UploadMethod.POST, + tag: tag, + ); + } else { + return MultipartFormDataUpload( + url: url.toString(), + data: {"name": "john"}, + files: paths.map((e) => FileItem(path: e, field: 'file')).toList(), + method: UploadMethod.POST, + tag: tag, + ); + } } } diff --git a/example/test_driver/flutter_uploader_test.dart b/example/test_driver/flutter_uploader_test.dart index 0eea75e..7190d5d 100644 --- a/example/test_driver/flutter_uploader_test.dart +++ b/example/test_driver/flutter_uploader_test.dart @@ -48,8 +48,9 @@ void main() { testWidgets("single file", (WidgetTester tester) async { var fileItem = FileItem(path: await _tmpFile(), field: "file"); - final taskId = - await uploader.enqueue(url: url.toString(), files: [fileItem]); + final taskId = await uploader.enqueue( + MultipartFormDataUpload(url: url.toString(), files: [fileItem]), + ); expect(taskId, isNotNull); @@ -62,10 +63,12 @@ void main() { }); testWidgets("multiple files", (WidgetTester tester) async { - final taskId = await uploader.enqueue(url: url.toString(), files: [ - FileItem(path: await _tmpFile(256), field: "file1"), - FileItem(path: await _tmpFile(257), field: "file2"), - ]); + final taskId = await uploader.enqueue( + MultipartFormDataUpload(url: url.toString(), files: [ + FileItem(path: await _tmpFile(256), field: "file1"), + FileItem(path: await _tmpFile(257), field: "file2"), + ]), + ); expect(taskId, isNotNull); @@ -81,10 +84,12 @@ void main() { var fileItem = FileItem(path: await _tmpFile(), field: "file"); final taskId = await uploader.enqueue( - url: url.replace(queryParameters: { - 'simulate': 'ok201', - }).toString(), - files: [fileItem], + MultipartFormDataUpload( + url: url.replace(queryParameters: { + 'simulate': 'ok201', + }).toString(), + files: [fileItem], + ), ); expect(taskId, isNotNull); @@ -100,8 +105,11 @@ void main() { var fileItem = FileItem(path: await _tmpFile(), field: "file"); final taskId = await uploader.enqueue( - url: url.replace(queryParameters: {'simulate': 'error500'}).toString(), - files: [fileItem], + MultipartFormDataUpload( + url: + url.replace(queryParameters: {'simulate': 'error500'}).toString(), + files: [fileItem], + ), ); expect(taskId, isNotNull); @@ -116,9 +124,11 @@ void main() { final url = baseUrl.replace(path: baseUrl.path + 'Binary'); testWidgets("single file", (WidgetTester tester) async { - final taskId = await uploader.enqueueBinary( - url: url.toString(), - path: await _tmpFile(), + final taskId = await uploader.enqueue( + RawUpload( + url: url.toString(), + path: await _tmpFile(), + ), ); expect(taskId, isNotNull); @@ -133,9 +143,12 @@ void main() { }); testWidgets("fowards errors", (WidgetTester tester) async { - final taskId = await uploader.enqueueBinary( - url: url.replace(queryParameters: {'simulate': 'error500'}).toString(), - path: await _tmpFile(), + final taskId = await uploader.enqueue( + RawUpload( + url: + url.replace(queryParameters: {'simulate': 'error500'}).toString(), + path: await _tmpFile(), + ), ); expect(taskId, isNotNull); diff --git a/lib/flutter_uploader.dart b/lib/flutter_uploader.dart index 310f206..1587c72 100644 --- a/lib/flutter_uploader.dart +++ b/lib/flutter_uploader.dart @@ -11,6 +11,8 @@ part 'src/file_item.dart'; part 'src/flutter_uploader.dart'; +part 'src/upload.dart'; + part 'src/upload_exception.dart'; part 'src/upload_method.dart'; diff --git a/lib/src/flutter_uploader.dart b/lib/src/flutter_uploader.dart index 5e5a6fc..38deba9 100644 --- a/lib/src/flutter_uploader.dart +++ b/lib/src/flutter_uploader.dart @@ -96,73 +96,31 @@ class FlutterUploader { _platform.setMethodCallHandler(null); } - /// Create a new multipart/form-data upload task - /// - /// **parameters:** - /// - /// * `url`: upload link - /// * `files`: files to be uploaded - /// * `method`: HTTP method to use for upload (POST,PUT,PATCH) - /// * `headers`: HTTP headers - /// * `data`: additional data to be uploaded together with file - /// * `tag`: name of the upload request (only used on Android) - /// - /// **return:** - /// - /// an unique identifier of the new upload task - Future enqueue({ - @required String url, - @required List files, - UploadMethod method = UploadMethod.POST, - Map headers, - Map data, - String tag, - }) async { - assert(method != null); - - List f = files != null && files.length > 0 - ? files.map((f) => f.toJson()).toList() - : []; - - return await _platform.invokeMethod('enqueue', { - 'url': url, - 'method': describeEnum(method), - 'files': f, - 'headers': headers, - 'data': data, - 'tag': tag - }); - } - - /// Create a new binary data upload task - /// - /// **parameters:** - /// - /// * `url`: upload link - /// * `path`: single file to upload - /// * `method`: HTTP method to use for upload (POST,PUT,PATCH) - /// * `headers`: HTTP headers - /// * `tag`: name of the upload request (only used on Android) - /// - /// **return:** - /// - /// an unique identifier of the new upload task - Future enqueueBinary({ - @required String url, - @required String path, - UploadMethod method = UploadMethod.POST, - Map headers, - String tag, - }) async { - assert(method != null); - - return await _platform.invokeMethod('enqueueBinary', { - 'url': url, - 'method': describeEnum(method), - 'path': path, - 'headers': headers, - 'tag': tag - }); + /// Enqueues a new upload task described by [upload]. + /// + /// See [MultipartFormDataUpload], [RawUpload] for available configuration. + Future enqueue(Upload upload) async { + if (upload is MultipartFormDataUpload) { + return await _platform.invokeMethod('enqueue', { + 'url': upload.url, + 'method': describeEnum(upload.method), + 'files': (upload.files ?? []).map((e) => e.toJson()).toList(), + 'headers': upload.headers, + 'data': upload.data, + 'tag': upload.tag, + }); + } + if (upload is RawUpload) { + return await _platform.invokeMethod('enqueueBinary', { + 'url': upload.url, + 'method': describeEnum(upload.method), + 'path': upload.path, + 'headers': upload.headers, + 'tag': upload.tag + }); + } + + throw 'Invalid upload type'; } /// diff --git a/lib/src/upload.dart b/lib/src/upload.dart new file mode 100644 index 0000000..83fc72d --- /dev/null +++ b/lib/src/upload.dart @@ -0,0 +1,67 @@ +part of flutter_uploader; + +abstract class Upload { + const Upload({ + @required this.url, + @required this.method, + this.headers = const {}, + this.tag, + }) : assert(url != null), + assert(method != null); + + /// upload link + final String url; + + /// HTTP method to use for upload (POST,PUT,PATCH) + final UploadMethod method; + + /// HTTP headers. + final Map headers; + + /// name of the upload request (only used on Android) + final String tag; +} + +class MultipartFormDataUpload extends Upload { + MultipartFormDataUpload({ + @required String url, + UploadMethod method = UploadMethod.POST, + Map headers, + String tag, + this.files, + this.data, + }) : assert(files != null || data != null), + super( + url: url, + method: method, + headers: headers, + tag: tag, + ) { + // Need to specify either files or data. + assert(files.isNotEmpty || data.isNotEmpty); + } + + /// files to be uploaded + final List files; + + /// additional data. Each entry will be sent as a form field. + final Map data; +} + +class RawUpload extends Upload { + const RawUpload({ + @required String url, + UploadMethod method = UploadMethod.POST, + Map headers, + String tag, + this.path, + }) : super( + url: url, + method: method, + headers: headers, + tag: tag, + ); + + /// single file to upload + final String path; +} diff --git a/test/flutter_uploader_test.dart b/test/flutter_uploader_test.dart index 29e2872..ccf8d0c 100644 --- a/test/flutter_uploader_test.dart +++ b/test/flutter_uploader_test.dart @@ -51,15 +51,17 @@ void main() { group('enqueue', () { test('passes the arguments correctly', () async { await uploader.enqueue( - url: 'http://www.somewhere.com', - files: [ - FileItem(path: '/path/to/file1'), - FileItem(path: '/path/to/file2', field: 'field2'), - ], - method: UploadMethod.PATCH, - headers: {'header1': 'value1'}, - data: {'data1': 'value1'}, - tag: 'tag1', + MultipartFormDataUpload( + url: 'http://www.somewhere.com', + files: [ + FileItem(path: '/path/to/file1'), + FileItem(path: '/path/to/file2', field: 'field2'), + ], + method: UploadMethod.PATCH, + headers: {'header1': 'value1'}, + data: {'data1': 'value1'}, + tag: 'tag1', + ), ); expect(log, [ @@ -90,12 +92,14 @@ void main() { group('enqueueBinary', () { test('passes the arguments correctly', () async { - await uploader.enqueueBinary( - url: 'http://www.somewhere.com', - path: '/path/to/file1', - method: UploadMethod.PATCH, - headers: {'header1': 'value1'}, - tag: 'tag1', + await uploader.enqueue( + RawUpload( + url: 'http://www.somewhere.com', + path: '/path/to/file1', + method: UploadMethod.PATCH, + headers: {'header1': 'value1'}, + tag: 'tag1', + ), ); expect(log, [ From 645bc2ae3c16200d1b6ff89f7926fc91928df54d Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Thu, 3 Sep 2020 18:26:08 +0100 Subject: [PATCH 07/31] Update README --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7336ab4..a2a1f6a 100644 --- a/README.md +++ b/README.md @@ -137,24 +137,28 @@ To see how it all works, check out the example. ``` dart final taskId = await FlutterUploader().enqueue( - url: "your upload link", //required: url to upload to - files: [FileItem(path: '/path/to/file', fieldname:"file")], // required: list of files that you want to upload - method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH) - headers: {"apikey": "api_123456", "userkey": "userkey_123456"}, - data: {"name": "john"}, // any data you want to send in upload request - tag: 'my tag', // custom tag which is returned in result/progress + MultipartFormDataUpload( + url: "your upload link", //required: url to upload to + files: [FileItem(path: '/path/to/file', fieldname:"file")], // required: list of files that you want to upload + method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH) + headers: {"apikey": "api_123456", "userkey": "userkey_123456"}, + data: {"name": "john"}, // any data you want to send in upload request + tag: 'my tag', // custom tag which is returned in result/progress + ), ); ``` **binary uploads:** ``` dart -final taskId = await FlutterUploader().enqueueBinary( - url: "your upload link", // required: url to upload to - path: '/path/to/file', // required: list of files that you want to upload - method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH) - headers: {"apikey": "api_123456", "userkey": "userkey_123456"}, - tag: 'my tag', // custom tag which is returned in result/progress +final taskId = await FlutterUploader().enqueue( + RawUpload( + url: "your upload link", // required: url to upload to + path: '/path/to/file', // required: list of files that you want to upload + method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH) + headers: {"apikey": "api_123456", "userkey": "userkey_123456"}, + tag: 'my tag', // custom tag which is returned in result/progress + ), ); ``` From 70a5e5eea8db5d20640463cc78f0c614ecd4d8ca Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 4 Sep 2020 15:30:22 +0100 Subject: [PATCH 08/31] Split UploadWorker into specific raw & form uploader --- .../MethodCallHandlerImpl.java | 12 +- .../flutteruploader/MimeTypeDetector.java | 22 +++ .../MultipartFormDataUploadWorker.java | 92 ++++++++++ .../flutteruploader/RawUploadWorker.java | 48 ++++++ .../flutteruploader/UploadTask.java | 12 -- .../flutteruploader/UploadWorker.java | 157 ++++-------------- 6 files changed, 201 insertions(+), 142 deletions(-) create mode 100644 android/src/main/java/com/bluechilli/flutteruploader/MimeTypeDetector.java create mode 100644 android/src/main/java/com/bluechilli/flutteruploader/MultipartFormDataUploadWorker.java create mode 100644 android/src/main/java/com/bluechilli/flutteruploader/RawUploadWorker.java diff --git a/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java b/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java index 3a1dda1..79687de 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java @@ -108,7 +108,7 @@ private void enqueue(MethodCall call, MethodChannel.Result result) { WorkRequest request = buildRequest( - new UploadTask(url, method, items, headers, parameters, connectionTimeout, false, tag)); + new UploadTask(url, method, items, headers, parameters, connectionTimeout, tag), false); WorkManager.getInstance(context).enqueue(request); String taskId = request.getId().toString(); result.success(taskId); @@ -145,8 +145,8 @@ private void enqueueBinary(MethodCall call, MethodChannel.Result result) { headers, Collections.emptyMap(), connectionTimeout, - true, - tag)); + tag), + true); WorkManager.getInstance(context).enqueue(request); String taskId = request.getId().toString(); @@ -170,7 +170,7 @@ private void clearUploads(MethodCall call, MethodChannel.Result result) { result.success(null); } - private WorkRequest buildRequest(UploadTask task) { + private WorkRequest buildRequest(UploadTask task, boolean binaryUpload) { Gson gson = new Gson(); Data.Builder dataBuilder = @@ -178,7 +178,6 @@ private WorkRequest buildRequest(UploadTask task) { .putString(UploadWorker.ARG_URL, task.getURL()) .putString(UploadWorker.ARG_METHOD, task.getMethod()) .putInt(UploadWorker.ARG_REQUEST_TIMEOUT, task.getTimeout()) - .putBoolean(UploadWorker.ARG_BINARY_UPLOAD, task.isBinaryUpload()) .putString(UploadWorker.ARG_UPLOAD_REQUEST_TAG, task.getTag()); List files = task.getFiles(); @@ -196,7 +195,8 @@ private WorkRequest buildRequest(UploadTask task) { dataBuilder.putString(UploadWorker.ARG_DATA, parametersJson); } - return new OneTimeWorkRequest.Builder(UploadWorker.class) + return new OneTimeWorkRequest.Builder( + binaryUpload ? RawUploadWorker.class : MultipartFormDataUploadWorker.class) .setConstraints( new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) .addTag(FLUTTER_UPLOAD_WORK_TAG) diff --git a/android/src/main/java/com/bluechilli/flutteruploader/MimeTypeDetector.java b/android/src/main/java/com/bluechilli/flutteruploader/MimeTypeDetector.java new file mode 100644 index 0000000..b901444 --- /dev/null +++ b/android/src/main/java/com/bluechilli/flutteruploader/MimeTypeDetector.java @@ -0,0 +1,22 @@ +package com.bluechilli.flutteruploader; + +import android.util.Log; +import android.webkit.MimeTypeMap; + +public class MimeTypeDetector { + static final String TAG = "MimeTypeDetector"; + + public static String detect(String url) { + String type = "application/octet-stream"; + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + try { + if (extension != null && !extension.isEmpty()) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); + } + } catch (Exception ex) { + Log.d(TAG, "getMimeType", ex); + } + + return type; + } +} diff --git a/android/src/main/java/com/bluechilli/flutteruploader/MultipartFormDataUploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/MultipartFormDataUploadWorker.java new file mode 100644 index 0000000..b50650f --- /dev/null +++ b/android/src/main/java/com/bluechilli/flutteruploader/MultipartFormDataUploadWorker.java @@ -0,0 +1,92 @@ +package com.bluechilli.flutteruploader; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.work.WorkerParameters; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class MultipartFormDataUploadWorker extends UploadWorker { + private static final String TAG = "MultipartFormDataUpload"; + + public MultipartFormDataUploadWorker( + @NonNull Context appContext, @NonNull WorkerParameters workerParams) { + super(appContext, workerParams); + } + + @Override + @Nullable + RequestBody buildRequestBody() { + final Gson gson = new Gson(); + + String parametersJson = getInputData().getString(ARG_DATA); + String filesJson = getInputData().getString(ARG_FILES); + + final Type mapStringStringType = new TypeToken>() {}.getType(); + final Type listFileItemType = new TypeToken>() {}.getType(); + + Map parameters = new HashMap<>(); + List files = new ArrayList<>(); + + if (parametersJson != null) { + parameters = gson.fromJson(parametersJson, mapStringStringType); + } + + if (filesJson != null) { + files = gson.fromJson(filesJson, listFileItemType); + } + + MultipartBody.Builder formRequestBuilder = prepareRequest(parameters, null); + int fileExistsCount = 0; + for (FileItem item : files) { + File file = new File(item.getPath()); + + if (file.exists() && file.isFile()) { + fileExistsCount++; + String mimeType = MimeTypeDetector.detect(item.getPath()); + MediaType contentType = MediaType.parse(mimeType); + RequestBody fileBody = RequestBody.create(file, contentType); + formRequestBuilder.addFormDataPart(item.getFieldname(), file.getName(), fileBody); + } else { + Log.w(TAG, "File does not exists -> file:" + item.getPath()); + } + } + + if (fileExistsCount == 0 && parameters.isEmpty()) { + return null; + } + + return formRequestBuilder.build(); + } + + private MultipartBody.Builder prepareRequest(Map parameters, String boundary) { + MultipartBody.Builder requestBodyBuilder = + boundary != null && !boundary.isEmpty() + ? new MultipartBody.Builder(boundary) + : new MultipartBody.Builder(); + + requestBodyBuilder.setType(MultipartBody.FORM); + + if (parameters == null) return requestBodyBuilder; + + for (String key : parameters.keySet()) { + String parameter = parameters.get(key); + if (parameter != null) { + requestBodyBuilder.addFormDataPart(key, parameter); + } + } + + return requestBodyBuilder; + } +} diff --git a/android/src/main/java/com/bluechilli/flutteruploader/RawUploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/RawUploadWorker.java new file mode 100644 index 0000000..15f06d0 --- /dev/null +++ b/android/src/main/java/com/bluechilli/flutteruploader/RawUploadWorker.java @@ -0,0 +1,48 @@ +package com.bluechilli.flutteruploader; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.work.WorkerParameters; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import okhttp3.MediaType; +import okhttp3.RequestBody; + +public class RawUploadWorker extends UploadWorker { + + public RawUploadWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) { + super(appContext, workerParams); + } + + @Nullable + @Override + RequestBody buildRequestBody() { + final Gson gson = new Gson(); + + String filesJson = getInputData().getString(ARG_FILES); + + final Type listFileItemType = new TypeToken>() {}.getType(); + + List files = new ArrayList<>(); + + if (filesJson != null) { + files = gson.fromJson(filesJson, listFileItemType); + } + + final FileItem item = files.get(0); + File file = new File(item.getPath()); + + if (!file.exists()) { + return null; + } + + String mimeType = MimeTypeDetector.detect(item.getPath()); + MediaType contentType = MediaType.parse(mimeType); + return RequestBody.create(file, contentType); + } +} diff --git a/android/src/main/java/com/bluechilli/flutteruploader/UploadTask.java b/android/src/main/java/com/bluechilli/flutteruploader/UploadTask.java index 6164595..b26b6a9 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/UploadTask.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/UploadTask.java @@ -1,6 +1,5 @@ package com.bluechilli.flutteruploader; -import android.net.Uri; import java.util.List; import java.util.Map; @@ -12,7 +11,6 @@ public class UploadTask { private Map data; private List files; private int requestTimeoutInSeconds; - private boolean binaryUpload; private String tag; public UploadTask( @@ -22,7 +20,6 @@ public UploadTask( Map headers, Map data, int requestTimeoutInSeconds, - boolean binaryUpload, String tag) { this.url = url; this.method = method; @@ -30,7 +27,6 @@ public UploadTask( this.headers = headers; this.data = data; this.requestTimeoutInSeconds = requestTimeoutInSeconds; - this.binaryUpload = binaryUpload; this.tag = tag; } @@ -38,10 +34,6 @@ public String getURL() { return url; } - public Uri getUri() { - return Uri.parse(url); - } - public String getMethod() { return method; } @@ -62,10 +54,6 @@ public int getTimeout() { return requestTimeoutInSeconds; } - public boolean isBinaryUpload() { - return binaryUpload; - } - public String getTag() { return tag; } diff --git a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java index 45463f6..319ec7e 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java @@ -2,7 +2,6 @@ import android.content.Context; import android.util.Log; -import android.webkit.MimeTypeMap; import android.webkit.URLUtil; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -33,24 +32,20 @@ import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; -public class UploadWorker extends ListenableWorker implements CountProgressListener { +public abstract class UploadWorker extends ListenableWorker implements CountProgressListener { public static final String ARG_URL = "url"; public static final String ARG_METHOD = "method"; public static final String ARG_HEADERS = "headers"; public static final String ARG_DATA = "data"; public static final String ARG_FILES = "files"; public static final String ARG_REQUEST_TIMEOUT = "requestTimeout"; - public static final String ARG_BINARY_UPLOAD = "binaryUpload"; public static final String ARG_UPLOAD_REQUEST_TAG = "tag"; - public static final String ARG_ID = "primaryId"; public static final String EXTRA_STATUS_CODE = "statusCode"; public static final String EXTRA_STATUS = "status"; public static final String EXTRA_ERROR_MESSAGE = "errorMessage"; @@ -68,12 +63,8 @@ public class UploadWorker extends ListenableWorker implements CountProgressListe private Call call; private boolean isCancelled = false; - private Context context; - - public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - - this.context = context; + public UploadWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) { + super(appContext, workerParams); } @Nullable private static FlutterEngine engine; @@ -106,14 +97,11 @@ public ListenableFuture startWork() { } @NonNull - public Result doWorkInternal() { + private Result doWorkInternal() { String url = getInputData().getString(ARG_URL); String method = getInputData().getString(ARG_METHOD); int timeout = getInputData().getInt(ARG_REQUEST_TIMEOUT, 3600); - boolean isBinaryUpload = getInputData().getBoolean(ARG_BINARY_UPLOAD, false); String headersJson = getInputData().getString(ARG_HEADERS); - String parametersJson = getInputData().getString(ARG_DATA); - String filesJson = getInputData().getString(ARG_FILES); tag = getInputData().getString(ARG_UPLOAD_REQUEST_TAG); if (tag == null) { @@ -124,72 +112,23 @@ public Result doWorkInternal() { try { Map headers = null; - Map parameters = null; - List files = new ArrayList<>(); - Gson gson = new Gson(); - Type type = new TypeToken>() {}.getType(); - Type fileItemType = new TypeToken>() {}.getType(); + final Gson gson = new Gson(); + final Type mapStringStringType = new TypeToken>() {}.getType(); if (headersJson != null) { - headers = gson.fromJson(headersJson, type); + headers = gson.fromJson(headersJson, mapStringStringType); } - if (parametersJson != null) { - parameters = gson.fromJson(parametersJson, type); - } - - if (filesJson != null) { - files = gson.fromJson(filesJson, fileItemType); - } - - final RequestBody innerRequestBody; - - if (isBinaryUpload) { - final FileItem item = files.get(0); - File file = new File(item.getPath()); - - if (!file.exists()) { - return Result.failure( - createOutputErrorData( - UploadStatus.FAILED, - DEFAULT_ERROR_STATUS_CODE, - "invalid_files", - "There are no items to upload", - null)); - } - - String mimeType = GetMimeType(item.getPath()); - MediaType contentType = MediaType.parse(mimeType); - innerRequestBody = RequestBody.create(file, contentType); - } else { - MultipartBody.Builder formRequestBuilder = prepareRequest(parameters, null); - int fileExistsCount = 0; - for (FileItem item : files) { - File file = new File(item.getPath()); - Log.d(TAG, "attaching file: " + item.getPath()); - - if (file.exists() && file.isFile()) { - fileExistsCount++; - String mimeType = GetMimeType(item.getPath()); - MediaType contentType = MediaType.parse(mimeType); - RequestBody fileBody = RequestBody.create(file, contentType); - formRequestBuilder.addFormDataPart(item.getFieldname(), file.getName(), fileBody); - } else { - Log.d(TAG, "File does not exists -> file:" + item.getPath()); - } - } - - if (fileExistsCount <= 0) { - return Result.failure( - createOutputErrorData( - UploadStatus.FAILED, - DEFAULT_ERROR_STATUS_CODE, - "invalid_files", - "There are no items to upload", - null)); - } + final RequestBody innerRequestBody = buildRequestBody(); - innerRequestBody = formRequestBuilder.build(); + if (innerRequestBody == null) { + return Result.failure( + createOutputErrorData( + UploadStatus.FAILED, + DEFAULT_ERROR_STATUS_CODE, + "invalid_parameters", + "There are no items to upload", + null)); } RequestBody requestBody = new CountingRequestBody(innerRequestBody, getId().toString(), this); @@ -300,7 +239,7 @@ public Result doWorkInternal() { "IllegalStateException while building a outputData object. Replace response with on-disk reference."); builder.putString(EXTRA_RESPONSE, null); - File responseFile = writeResponseToTemporaryFile(context, responseString); + File responseFile = writeResponseToTemporaryFile(responseString); if (responseFile != null) { builder.putString(EXTRA_RESPONSE_FILE, responseFile.getAbsolutePath()); } @@ -313,24 +252,28 @@ public Result doWorkInternal() { if (isCancelled) { return Result.failure(); } - return handleException(context, ex, "protocol"); + return handleException(ex, "protocol"); } catch (JsonIOException ex) { - return handleException(context, ex, "json_error"); + return handleException(ex, "json_error"); } catch (UnknownHostException ex) { - return handleException(context, ex, "unknown_host"); + return handleException(ex, "unknown_host"); } catch (IOException ex) { - return handleException(context, ex, "io_error"); + return handleException(ex, "io_error"); } catch (Exception ex) { - return handleException(context, ex, "upload error"); + return handleException(ex, "upload error"); } finally { call = null; } } - private File writeResponseToTemporaryFile(Context context, String body) { + @Nullable + abstract RequestBody buildRequestBody(); + + private File writeResponseToTemporaryFile(String body) { + final File cacheDir = getApplicationContext().getCacheDir(); FileOutputStream fos = null; try { - File tempFile = File.createTempFile("flutter_uploader", null, context.getCacheDir()); + File tempFile = File.createTempFile("flutter_uploader", null, cacheDir); fos = new FileOutputStream(tempFile); fos.write(body.getBytes()); fos.close(); @@ -348,6 +291,7 @@ private File writeResponseToTemporaryFile(Context context, String body) { } private void startEngine() { + final Context context = getApplicationContext(); long callbackHandle = SharedPreferenceHelper.getCallbackHandle(context); Log.d(TAG, "callbackHandle: " + callbackHandle); @@ -380,7 +324,7 @@ private void stopEngine() { } } - private Result handleException(Context context, Exception ex, String code) { + private Result handleException(Exception ex, String code) { Log.e(TAG, "exception encountered", ex); int finalStatus = isCancelled ? UploadStatus.CANCELED : UploadStatus.FAILED; @@ -395,42 +339,7 @@ private Result handleException(Context context, Exception ex, String code) { getStacktraceAsStringList(ex.getStackTrace()))); } - private String GetMimeType(String url) { - String type = "application/octet-stream"; - String extension = MimeTypeMap.getFileExtensionFromUrl(url); - try { - if (extension != null && !extension.isEmpty()) { - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()); - } - } catch (Exception ex) { - Log.d(TAG, "UploadWorker - GetMimeType", ex); - } - - return type; - } - - private MultipartBody.Builder prepareRequest(Map parameters, String boundary) { - - MultipartBody.Builder requestBodyBuilder = - boundary != null && !boundary.isEmpty() - ? new MultipartBody.Builder(boundary) - : new MultipartBody.Builder(); - - requestBodyBuilder.setType(MultipartBody.FORM); - - if (parameters == null) return requestBodyBuilder; - - for (String key : parameters.keySet()) { - String parameter = parameters.get(key); - if (parameter != null) { - requestBodyBuilder.addFormDataPart(key, parameter); - } - } - - return requestBodyBuilder; - } - - private void sendUpdateProcessEvent(Context context, int status, int progress) { + private void sendUpdateProcessEvent(int status, int progress) { setProgressAsync( new Data.Builder().putInt("status", status).putInt("progress", progress).build()); } @@ -466,7 +375,7 @@ public void OnProgress(String taskId, long bytesWritten, long contentLength) { + ", progress: " + progress); - sendUpdateProcessEvent(context, UploadStatus.RUNNING, progress); + sendUpdateProcessEvent(UploadStatus.RUNNING, progress); } @Override @@ -497,7 +406,7 @@ public void OnError(String taskId, String code, String message) { + code + ", error: " + message); - sendUpdateProcessEvent(context, UploadStatus.FAILED, -1); + sendUpdateProcessEvent(UploadStatus.FAILED, -1); } private String[] getStacktraceAsStringList(StackTraceElement[] stacktrace) { From 601bf5a20d07e85738423bd84b43511b59fc0e3a Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 4 Sep 2020 15:33:50 +0100 Subject: [PATCH 09/31] Move FlutterEngine management to a separate class --- .../flutteruploader/FlutterEngineHelper.java | 43 +++++++++++++++++++ .../flutteruploader/UploadWorker.java | 42 +----------------- 2 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java new file mode 100644 index 0000000..ab21c2b --- /dev/null +++ b/android/src/main/java/com/bluechilli/flutteruploader/FlutterEngineHelper.java @@ -0,0 +1,43 @@ +package com.bluechilli.flutteruploader; + +import android.content.Context; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.view.FlutterCallbackInformation; +import io.flutter.view.FlutterMain; + +public class FlutterEngineHelper { + @Nullable private static FlutterEngine engine; + + public static void start(Context context) { + long callbackHandle = SharedPreferenceHelper.getCallbackHandle(context); + + if (callbackHandle != -1L && engine == null) { + engine = new FlutterEngine(context); + FlutterMain.ensureInitializationComplete(context, null); + + FlutterCallbackInformation callbackInfo = + FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); + String dartBundlePath = FlutterMain.findAppBundlePath(); + + engine + .getDartExecutor() + .executeDartCallback( + new DartExecutor.DartCallback(context.getAssets(), dartBundlePath, callbackInfo)); + } + } + + // private void stopEngine() { + // Log.d(TAG, "Destroying worker engine."); + // + // if (engine != null) { + // try { + // engine.destroy(); + // } catch (Throwable e) { + // Log.e(TAG, "Can not destroy engine", e); + // } + // engine = null; + // } + // } +} diff --git a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java index 319ec7e..6660f14 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/UploadWorker.java @@ -13,10 +13,6 @@ import com.google.gson.Gson; import com.google.gson.JsonIOException; import com.google.gson.reflect.TypeToken; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.view.FlutterCallbackInformation; -import io.flutter.view.FlutterMain; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -67,14 +63,12 @@ public UploadWorker(@NonNull Context appContext, @NonNull WorkerParameters worke super(appContext, workerParams); } - @Nullable private static FlutterEngine engine; - private Executor backgroundExecutor = Executors.newSingleThreadExecutor(); @NonNull @Override public ListenableFuture startWork() { - startEngine(); + FlutterEngineHelper.start(getApplicationContext()); return CallbackToFutureAdapter.getFuture( completer -> { @@ -290,40 +284,6 @@ private File writeResponseToTemporaryFile(String body) { return null; } - private void startEngine() { - final Context context = getApplicationContext(); - long callbackHandle = SharedPreferenceHelper.getCallbackHandle(context); - - Log.d(TAG, "callbackHandle: " + callbackHandle); - - if (callbackHandle != -1L && engine == null) { - engine = new FlutterEngine(context); - FlutterMain.ensureInitializationComplete(context, null); - - FlutterCallbackInformation callbackInfo = - FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); - String dartBundlePath = FlutterMain.findAppBundlePath(); - - engine - .getDartExecutor() - .executeDartCallback( - new DartExecutor.DartCallback(context.getAssets(), dartBundlePath, callbackInfo)); - } - } - - private void stopEngine() { - Log.d(TAG, "Destroying worker engine."); - - if (engine != null) { - try { - engine.destroy(); - } catch (Throwable e) { - Log.e(TAG, "Can not destroy engine", e); - } - engine = null; - } - } - private Result handleException(Exception ex, String code) { Log.e(TAG, "exception encountered", ex); From d04e34b4c8dbea221c90a15aba3685d57cd58663 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 4 Sep 2020 15:50:53 +0100 Subject: [PATCH 10/31] Move optional WorkManager config to the host application --- README.md | 44 ++++--- android/build.gradle | 2 +- android/src/main/AndroidManifest.xml | 5 +- .../FlutterUploaderInitializer.java | 120 ------------------ .../FlutterUploaderPlugin.java | 4 +- .../MethodCallHandlerImpl.java | 17 ++- example/android/app/build.gradle | 3 + .../android/app/src/main/AndroidManifest.xml | 35 ++--- .../ExampleApplication.java | 21 +++ 9 files changed, 76 insertions(+), 175 deletions(-) delete mode 100644 android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java create mode 100644 example/android/app/src/main/java/com/bluechilli/flutteruploaderexample/ExampleApplication.java diff --git a/README.md b/README.md index 7336ab4..377fb97 100644 --- a/README.md +++ b/README.md @@ -68,29 +68,33 @@ func registerPlugins(registry: FlutterPluginRegistry) { ### Optional configuration: -- **Configure maximum number of concurrent tasks:** the plugin depends on `WorkManager` library and `WorkManager` depends on the number of available processor to configure the maximum number of tasks running at a moment. You can setup a fixed number for this configuration by adding following codes to your `AndroidManifest.xml`: +#### Configure maximum number of concurrent tasks -```xml - - - - - - - - - +The plugin depends on the `WorkManager` library. The configuration can be done using the instructions at [https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration](https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration). + +The example project shows a custom configuration of up to 10 simultaneous uploads. + +Two steps are required: + +Depend on the appropriate work-runtime in your host App. +``` gradle +implementation "androidx.work:work-runtime:$work_version" ``` +Override the default `Application` and implement the `androidx.work.Configuration.Provider` interface: + +``` java +@NonNull +@Override +public Configuration getWorkManagerConfiguration() { + return new Configuration.Builder() + .setMinimumLoggingLevel(android.util.Log.INFO) + .setExecutor(Executors.newFixedThreadPool(10)) + .build(); +} +``` + + ## Usage #### Import package: diff --git a/android/build.gradle b/android/build.gradle index f952e50..07817ef 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,7 +1,7 @@ group 'com.bluechilli.flutteruploader' version '1.0-SNAPSHOT' def lifecycle_version = "2.2.0" -def work_version = "2.3.4" +def work_version = "2.4.0" def futures_version = "1.1.0-beta01" def core_version = "1.5.0-alpha01" def annotation_version = "1.1.0" diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 077a608..bba894e 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,2 +1,5 @@ - + + + diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java deleted file mode 100644 index 730edce..0000000 --- a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.bluechilli.flutteruploader; - -import android.content.ComponentName; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.work.Configuration; -import androidx.work.WorkManager; -import java.util.concurrent.Executors; - -public class FlutterUploaderInitializer extends ContentProvider { - - private static final String TAG = "UploaderInitializer"; - private static final int DEFAULT_MAX_CONCURRENT_TASKS = 3; - private static final int DEFAULT_UPLOAD_CONNECTION_TIMEOUT = 3600; - - @Override - public boolean onCreate() { - int maximumConcurrentTask = getMaxConcurrentTaskMetadata(getContext()); - WorkManager.initialize( - getContext(), - new Configuration.Builder() - .setExecutor(Executors.newFixedThreadPool(maximumConcurrentTask)) - .build()); - return true; - } - - @Nullable - @Override - public Cursor query( - @NonNull Uri uri, - @Nullable String[] strings, - @Nullable String s, - @Nullable String[] strings1, - @Nullable String s1) { - return null; - } - - @Nullable - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - @Nullable - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) { - return 0; - } - - @Override - public int update( - @NonNull Uri uri, - @Nullable ContentValues contentValues, - @Nullable String s, - @Nullable String[] strings) { - return 0; - } - - private static int getMaxConcurrentTaskMetadata(Context context) { - try { - ProviderInfo pi = - context - .getPackageManager() - .getProviderInfo( - new ComponentName( - context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"), - PackageManager.GET_META_DATA); - Bundle bundle = pi.metaData; - int max = - bundle.getInt( - "com.bluechilli.flutteruploader.MAX_CONCURRENT_TASKS", DEFAULT_MAX_CONCURRENT_TASKS); - Log.d(TAG, "MAX_CONCURRENT_TASKS = " + max); - return max; - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); - } catch (NullPointerException e) { - Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); - } - return DEFAULT_MAX_CONCURRENT_TASKS; - } - - public static int getConnectionTimeout(Context context) { - try { - ProviderInfo pi = - context - .getPackageManager() - .getProviderInfo( - new ComponentName( - context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"), - PackageManager.GET_META_DATA); - Bundle bundle = pi.metaData; - int max = - bundle.getInt( - "com.bluechilli.flutteruploader.UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS", - DEFAULT_UPLOAD_CONNECTION_TIMEOUT); - Log.d(TAG, "UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS = " + max); - return max; - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); - } catch (NullPointerException e) { - Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); - } - - return DEFAULT_UPLOAD_CONNECTION_TIMEOUT; - } -} diff --git a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java index 6e5bf31..ef28302 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java @@ -60,10 +60,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } private void startListening(Context context, BinaryMessenger messenger) { - final int timeout = FlutterUploaderInitializer.getConnectionTimeout(context); - channel = new MethodChannel(messenger, CHANNEL_NAME); - methodCallHandler = new MethodCallHandlerImpl(context, timeout, this); + methodCallHandler = new MethodCallHandlerImpl(context, this); uploadObserver = new UploadObserver(this); WorkManager.getInstance(context) diff --git a/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java b/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java index 79687de..ecb257c 100644 --- a/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java +++ b/android/src/main/java/com/bluechilli/flutteruploader/MethodCallHandlerImpl.java @@ -25,20 +25,19 @@ public class MethodCallHandlerImpl implements MethodCallHandler { + private static final int DEFAULT_CONNECTION_TIMEOUT = 3600; + /** The generic {@link WorkManager} tag which matches any upload. */ public static final String FLUTTER_UPLOAD_WORK_TAG = "flutter_upload_task"; private final Context context; - private int connectionTimeout; - @NonNull private final StatusListener statusListener; private static final List VALID_HTTP_METHODS = Arrays.asList("POST", "PUT", "PATCH"); - MethodCallHandlerImpl(Context context, int timeout, @NonNull StatusListener listener) { + MethodCallHandlerImpl(Context context, @NonNull StatusListener listener) { this.context = context; - this.connectionTimeout = timeout; this.statusListener = listener; } @@ -85,6 +84,7 @@ private void enqueue(MethodCall call, MethodChannel.Result result) { Map parameters = call.argument("data"); Map headers = call.argument("headers"); String tag = call.argument("tag"); + Integer connectionTimeout = call.argument("timeout"); if (method == null) { method = "POST"; @@ -100,6 +100,10 @@ private void enqueue(MethodCall call, MethodChannel.Result result) { return; } + if (connectionTimeout == null) { + connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + } + List items = new ArrayList<>(); for (Map file : files) { @@ -121,6 +125,7 @@ private void enqueueBinary(MethodCall call, MethodChannel.Result result) { String path = call.argument("path"); Map headers = call.argument("headers"); String tag = call.argument("tag"); + Integer connectionTimeout = call.argument("timeout"); if (method == null) { method = "POST"; @@ -136,6 +141,10 @@ private void enqueueBinary(MethodCall call, MethodChannel.Result result) { return; } + if (connectionTimeout == null) { + connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + } + WorkRequest request = buildRequest( new UploadTask( diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index f844377..1a9013d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -21,6 +21,8 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +def work_version = "2.4.0" + apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -60,6 +62,7 @@ flutter { } dependencies { + implementation "androidx.work:work-runtime:$work_version" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index bd91504..ef2762b 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.bluechilli.flutteruploaderexample"> - @@ -12,17 +11,17 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. -->