diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..02e134f 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,6 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..a558ccc 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 15.6 diff --git a/ios/Podfile b/ios/Podfile index 5c0885d..d188a75 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' use_frameworks! # CocoaPods analytics sends network stats synchronously affecting flutter build latency. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ec018ee..052f8a2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - background_downloader (0.0.1): + - Flutter - camera_avfoundation (0.0.1): - Flutter - connectivity_plus (0.0.1): @@ -223,6 +225,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - Firebase @@ -275,6 +278,8 @@ SPEC REPOS: - sqlite3 EXTERNAL SOURCES: + background_downloader: + :path: ".symlinks/plugins/background_downloader/ios" camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" connectivity_plus: @@ -327,6 +332,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: + background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 @@ -370,6 +376,6 @@ SPEC CHECKSUMS: video_compress: f2133a07762889d67f0711ac831faa26f956980e video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b -PODFILE CHECKSUM: a6fb5a4d094eb37ff57a33aa854ee613ab378080 +PODFILE CHECKSUM: 3e94c12f4f6904137d1449e3b100fda499ccd32d COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 54bbd8d..699543d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -632,7 +632,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = twonly; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -830,7 +830,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = twonly; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -864,7 +864,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = twonly; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 13; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/lib/main.dart b/lib/main.dart index 7d7d3f7..3d0075c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:isolate'; - import 'package:camera/camera.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -52,6 +51,8 @@ void main() async { purgeSendMediaFiles(); }); + await initMediaUploader(); + runApp( MultiProvider( providers: [ diff --git a/lib/src/services/api/media_send.dart b/lib/src/services/api/media_send.dart index 35f755f..d013a2e 100644 --- a/lib/src/services/api/media_send.dart +++ b/lib/src/services/api/media_send.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:isolate'; import 'dart:math'; +import 'package:background_downloader/background_downloader.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:http/http.dart' as http; import 'dart:io'; -import 'dart:typed_data'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; @@ -54,6 +52,81 @@ Future isAllowedToSend() async { return null; } +Future initMediaUploader() async { + FileDownloader().updates.listen((update) async { + switch (update) { + case TaskStatusUpdate(): + if (update.status == TaskStatus.complete) { + int mediaUploadId = int.parse(update.task.taskId); + MediaUpload? media = await twonlyDB.mediaUploadsDao + .getMediaUploadById(mediaUploadId) + .getSingleOrNull(); + if (media == null) { + Log.error( + "Got an upload task but no upload media in the mediaupload atabase"); + return; + } + if (update.responseStatusCode == 200) { + Log.info("Upload was success!"); + + await twonlyDB.mediaUploadsDao.updateMediaUpload( + mediaUploadId, + MediaUploadsCompanion( + state: Value(UploadState.receiverNotified), + ), + ); + + for (final messageId in media.messageIds!) { + await twonlyDB.messagesDao.updateMessageByMessageId( + messageId, + MessagesCompanion( + acknowledgeByServer: Value(true), + errorWhileSending: Value(false), + ), + ); + } + return; + } else if (update.responseStatusCode != null) { + if (update.responseStatusCode! >= 400 && + update.responseStatusCode! < 500) { + for (final messageId in media.messageIds!) { + await twonlyDB.messagesDao.updateMessageByMessageId( + messageId, + MessagesCompanion( + acknowledgeByServer: Value(true), + errorWhileSending: Value(true), + ), + ); + } + } + Log.error( + "Got error while uploading: ${update.responseStatusCode}"); + } + } + + print('Status update for ${update.task} with status ${update.status}'); + case TaskProgressUpdate(): + print( + 'Progress update for ${update.task} with progress ${update.progress}'); + } + }); + + await FileDownloader().start(); + + FileDownloader().configure(androidConfig: [ + (Config.bypassTLSCertificateValidation, kDebugMode), + ]); + + FileDownloader().configureNotification( + running: TaskNotification( + 'Uploading', + 'Uploading your {filename} ({progress}).', + ), + complete: null, + progressBar: true, + ); +} + /// States: /// when user recorded an video /// 1. Compress video @@ -172,11 +245,11 @@ Future addOrModifyImageToUpload( quality: 60, ); } - await writeMediaFile(mediaUploadId, "png", imageBytesCompressed); + await writeSendMediaFile(mediaUploadId, "png", imageBytesCompressed); } catch (e) { Log.error("$e"); // as a fall back use the original image - await writeMediaFile(mediaUploadId, "png", imageBytes); + await writeSendMediaFile(mediaUploadId, "png", imageBytes); imageBytesCompressed = imageBytes; } @@ -193,7 +266,7 @@ Future addOrModifyImageToUpload( Future handlePreProcessingState(MediaUpload media) async { try { - final imageHandler = readMediaFile(media.mediaUploadId, "png"); + final imageHandler = readSendMediaFile(media.mediaUploadId, "png"); final videoHandler = compressVideoIfExists(media.mediaUploadId); await encryptMediaFiles( media.mediaUploadId, @@ -217,7 +290,7 @@ Future encryptMediaFiles( /// if there is a video wait until it is finished with compression if (videoHandler != null) { if (await videoHandler) { - Uint8List compressedVideo = await readMediaFile(mediaUploadId, "mp4"); + Uint8List compressedVideo = await readSendMediaFile(mediaUploadId, "mp4"); dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo); } } @@ -230,12 +303,10 @@ Future encryptMediaFiles( state.encryptionKey = secretKey.bytes; state.encryptionNonce = xchacha20.newNonce(); - final secretBox = await Isolate.run( - () => xchacha20.encrypt( - dataToEncrypt, - secretKey: secretKey, - nonce: state.encryptionNonce, - ), + final secretBox = await xchacha20.encrypt( + dataToEncrypt, + secretKey: secretKey, + nonce: state.encryptionNonce, ); state.encryptionMac = secretBox.mac.bytes; @@ -244,7 +315,7 @@ Future encryptMediaFiles( state.sha2Hash = (await algorithm.hash(secretBox.cipherText)).bytes; final encryptedBytes = Uint8List.fromList(secretBox.cipherText); - await writeMediaFile( + await writeSendMediaFile( mediaUploadId, "encrypted", encryptedBytes, @@ -376,7 +447,7 @@ Future handleUploadError(MediaUpload mediaUpload) async { Future handleMediaUpload(MediaUpload media) async { Uint8List bytesToUpload = - await readMediaFile(media.mediaUploadId, "encrypted"); + await readSendMediaFile(media.mediaUploadId, "encrypted"); if (media.messageIds == null) return false; @@ -456,63 +527,33 @@ Future handleMediaUpload(MediaUpload media) async { return false; } + File uploadRequestFile = await writeSendMediaFile( + media.mediaUploadId, + "upload", + uploadRequestBytes, + ); + String apiUrl = "http${apiService.apiSecure}://${apiService.apiHost}/api/upload"; - var requestMultipart = http.MultipartRequest( - "POST", - Uri.parse(apiUrl), - ); - requestMultipart.headers['x-twonly-auth-token'] = - uint8ListToHex(base64Decode(apiAuthToken)); - - requestMultipart.files.add(http.MultipartFile.fromBytes( - "file", - uploadRequestBytes, - filename: "upload", - )); - - Log.info("Starting upload from ${media.mediaUploadId}"); - try { - var streamedResponse = await requestMultipart.send(); + final task = UploadTask.fromFile( + taskId: "${media.mediaUploadId}", + displayName: (media.metadata?.isVideo ?? false) ? "image" : "video", + file: uploadRequestFile, + url: apiUrl, + priority: 0, + retries: 10, + headers: { + 'x-twonly-auth-token': uint8ListToHex(base64Decode(apiAuthToken)) + }, + ); - final response = await http.Response.fromStream(streamedResponse); + Log.info("Starting upload from ${media.mediaUploadId}"); - if (response.statusCode == 200) { - Log.info("Upload was success!"); + final result = await FileDownloader().enqueue(task); - await twonlyDB.mediaUploadsDao.updateMediaUpload( - media.mediaUploadId, - MediaUploadsCompanion( - state: Value(UploadState.receiverNotified), - ), - ); - - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(false), - ), - ); - } - return true; - } else { - if (response.statusCode >= 400 && response.statusCode < 500) { - for (final messageId in media.messageIds!) { - await twonlyDB.messagesDao.updateMessageByMessageId( - messageId, - MessagesCompanion( - acknowledgeByServer: Value(true), - errorWhileSending: Value(true), - ), - ); - } - } - Log.error("Got error while uploading: ${response.statusCode}"); - } + return result; } catch (e) { Log.error("Exception during upload: $e"); } @@ -534,46 +575,44 @@ Future compressVideoIfExists(int mediaUploadId) async { return false; } - return await Isolate.run(() async { - MediaInfo? mediaInfo; - try { + MediaInfo? mediaInfo; + try { + mediaInfo = await VideoCompress.compressVideo( + videoOriginalFile.path, + quality: VideoQuality.Res1280x720Quality, + deleteOrigin: false, + includeAudio: + true, // https://github.com/jonataslaw/VideoCompress/issues/184 + ); + + if (mediaInfo!.filesize! >= 30 * 1000 * 1000) { + // if the media file is over 20MB compress it with low quality mediaInfo = await VideoCompress.compressVideo( videoOriginalFile.path, - quality: VideoQuality.Res1280x720Quality, + quality: VideoQuality.Res960x540Quality, deleteOrigin: false, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 + includeAudio: true, ); - - if (mediaInfo!.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res960x540Quality, - deleteOrigin: false, - includeAudio: true, - ); - } - } catch (e) { - Log.error("during video compression: $e"); } + } catch (e) { + Log.error("during video compression: $e"); + } - if (mediaInfo == null) { - Log.error("could not compress video."); - // as a fall back use the non compressed version - await videoOriginalFile.copy(videoCompressedFile.path); - await videoOriginalFile.delete(); - } else { - await mediaInfo.file!.copy(videoCompressedFile.path); - await mediaInfo.file!.delete(); - } - return true; - }); + if (mediaInfo == null) { + Log.error("could not compress video."); + // as a fall back use the non compressed version + await videoOriginalFile.copy(videoCompressedFile.path); + await videoOriginalFile.delete(); + } else { + await mediaInfo.file!.copy(videoCompressedFile.path); + await mediaInfo.file!.delete(); + } + return true; } /// --- helper functions --- -Future readMediaFile(int mediaUploadId, String type) async { +Future readSendMediaFile(int mediaUploadId, String type) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); File file = File("$basePath.$type"); if (!await file.exists()) { @@ -582,14 +621,15 @@ Future readMediaFile(int mediaUploadId, String type) async { return await file.readAsBytes(); } -Future writeMediaFile( +Future writeSendMediaFile( int mediaUploadId, String type, Uint8List data) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); File file = File("$basePath.$type"); await file.writeAsBytes(data); + return file; } -Future deleteMediaFile(int mediaUploadId, String type) async { +Future deleteSendMediaFile(int mediaUploadId, String type) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); File file = File("$basePath.$type"); if (await file.exists()) {