first poc for #197

This commit is contained in:
otsmr 2025-06-12 15:49:08 +02:00
parent d871e04f0e
commit 6677f89a18
7 changed files with 172 additions and 123 deletions

View file

@ -4,4 +4,6 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<application android:usesCleartextTraffic="true" >
</application>
</manifest>

View file

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>15.6</string>
</dict>
</plist>

View file

@ -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.

View file

@ -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

View file

@ -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",

View file

@ -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: [

View file

@ -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<ErrorCode?> 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<Uint8List> 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<Uint8List> 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(
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<bool> 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<bool> 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),
try {
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))
},
);
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 result = await FileDownloader().enqueue(task);
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
Log.info("Upload was success!");
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,7 +575,6 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
return false;
}
return await Isolate.run(() async {
MediaInfo? mediaInfo;
try {
mediaInfo = await VideoCompress.compressVideo(
@ -568,12 +608,11 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
await mediaInfo.file!.delete();
}
return true;
});
}
/// --- helper functions ---
Future<Uint8List> readMediaFile(int mediaUploadId, String type) async {
Future<Uint8List> 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<Uint8List> readMediaFile(int mediaUploadId, String type) async {
return await file.readAsBytes();
}
Future<void> writeMediaFile(
Future<File> 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<void> deleteMediaFile(int mediaUploadId, String type) async {
Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File file = File("$basePath.$type");
if (await file.exists()) {