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()) {