mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
first poc for #197
This commit is contained in:
parent
d871e04f0e
commit
6677f89a18
7 changed files with 172 additions and 123 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>15.6</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
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<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),
|
||||
);
|
||||
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<bool> 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<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()) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue