diff --git a/lib/src/providers/api/media_received.dart b/lib/src/providers/api/media_received.dart index cfdb1f9..9c18c54 100644 --- a/lib/src/providers/api/media_received.dart +++ b/lib/src/providers/api/media_received.dart @@ -8,16 +8,13 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:http/http.dart' as http; +// import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/media_send.dart'; -import 'dart:typed_data'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:logging/logging.dart'; -import 'package:twonly/app.dart'; import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' as client; -import 'package:twonly/src/model/protobuf/api/error.pb.dart'; -import 'package:twonly/src/model/protobuf/api/server_to_client.pbserver.dart'; import 'package:twonly/src/utils/storage.dart'; Map downloadStartedForMediaReceived = {}; @@ -91,7 +88,6 @@ Future startDownloadMedia(Message message, bool force) async { final content = MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); - if (content is! MediaMessageContent) return; if (content.downloadToken == null) return; @@ -123,100 +119,70 @@ Future startDownloadMedia(Message message, bool force) async { downloadState: Value(DownloadState.downloading), ), ); + } - int offset = 0; - Uint8List? bytes = await readMediaFile(media.messageId, "encrypted"); - if (bytes != null && bytes.isNotEmpty) { - offset = bytes.length; - } + // int offset = 0; + // Uint8List? bytes = await readMediaFile(media.messageId, "encrypted"); + // if (bytes != null && bytes.isNotEmpty) { + // offset = bytes.length; - downloadStartedForMediaReceived[message.messageId] = DateTime.now(); - Result res = - await apiProvider.triggerDownload(content.downloadToken!, offset); - if (res.isError) { - if (res.error == ErrorCode.InvalidDownloadToken) { - // TODO: notfy the sender about this issue + downloadStartedForMediaReceived[message.messageId] = DateTime.now(); + + String downloadToken = uint8ListToHex(content.downloadToken!); + + String apiUrl = + "http${apiProvider.apiSecure}://${apiProvider.apiHost}/api/download/$downloadToken"; + + var httpClient = http.Client(); + var request = http.Request('GET', Uri.parse(apiUrl)); + var response = httpClient.send(request); + + List> chunks = []; + int downloaded = 0; + + response.asStream().listen((http.StreamedResponse r) { + r.stream.listen((List chunk) { + // Display percentage of completion + print('downloadPercentage: ${downloaded / (r.contentLength ?? 0) * 100}'); + + chunks.add(chunk); + downloaded += chunk.length; + }, onDone: () async { + if (r.statusCode != 200) { + Logger("media_received.dart").shout("Download error: $r"); await twonlyDatabase.messagesDao.updateMessageByMessageId( - media.messageId, + message.messageId, MessagesCompanion( errorWhileSending: Value(true), ), ); + return; } - } - } + + // Display percentage of completion + print('downloadPercentage: ${downloaded / (r.contentLength ?? 0) * 100}'); + + // Save the file + final Uint8List bytes = Uint8List(r.contentLength ?? 0); + int offset = 0; + for (List chunk in chunks) { + bytes.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + await writeMediaFile(message.messageId, "encrypted", bytes); + handleEncryptedFile(message, encryptedBytesTmp: bytes); + return; + }); + }); } -Future handleDownloadData(DownloadData data) async { - if (globalIsAppInBackground) { - // download should only be done when the app is open - return client.Response()..error = ErrorCode.InternalError; - } +Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async { + Uint8List? encryptedBytes = + encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted"); - Logger("server_messages") - .info("downloading: ${data.downloadToken} ${data.fin}"); - - final media = await twonlyDatabase.mediaDownloadsDao - .getMediaDownloadByDownloadToken(data.downloadToken) - .getSingleOrNull(); - - if (media == null) { - Logger("server_messages") - .shout("download data received, but unknown messageID"); - // answers with ok, so the server will delete the message - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - - if (data.fin && data.offset == 3_980_938_213 && data.data.isEmpty) { - Logger("media_received.dart").shout("Image already deleted by the server!"); - // media file was deleted by the server. remove the media from device - await twonlyDatabase.messagesDao.updateMessageByMessageId( - media.messageId, - MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - await deleteMediaFile(media.messageId, "encrypted"); - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - - Uint8List? buffered = await readMediaFile(media.messageId, "encrypted"); - Uint8List downloadedBytes; - if (buffered != null) { - if (data.offset != buffered.length) { - Logger("media_received.dart") - .shout("server send wrong offset: ${data.offset} ${buffered.length}"); - return client.Response()..error = ErrorCode.InvalidOffset; - } - var b = BytesBuilder(); - b.add(buffered); - b.add(data.data); - downloadedBytes = b.takeBytes(); - } else { - downloadedBytes = Uint8List.fromList(data.data); - } - - await writeMediaFile(media.messageId, "encrypted", downloadedBytes); - - if (!data.fin) { - // download not finished, so waiting for more data... - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - - Message? msg = await twonlyDatabase.messagesDao - .getMessageByMessageId(media.messageId) - .getSingleOrNull(); - - if (msg == null) { - await deleteMediaFile(media.messageId, "encrypted"); + if (encryptedBytes == null) { Logger("media_received.dart") - .info("messageId not found in database. Ignoring download"); - // answers with ok, so the server will delete the message - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; + .shout("encrypted bytes are not found for ${msg.messageId}"); } MediaMessageContent content = @@ -226,7 +192,7 @@ Future handleDownloadData(DownloadData data) async { SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!); SecretBox secretBox = SecretBox( - downloadedBytes, + encryptedBytes!, nonce: content.encryptionNonce!, mac: Mac(content.encryptionMac!), ); @@ -239,14 +205,14 @@ Future handleDownloadData(DownloadData data) async { if (content.isVideo) { final splited = extractUint8Lists(imageBytes); imageBytes = splited[0]; - await writeMediaFile(media.messageId, "mp4", splited[1]); + await writeMediaFile(msg.messageId, "mp4", splited[1]); } - await writeMediaFile(media.messageId, "png", imageBytes); + await writeMediaFile(msg.messageId, "png", imageBytes); } catch (e) { Logger("media_received.dart").info("Decryption error: $e"); await twonlyDatabase.messagesDao.updateMessageByMessageId( - media.messageId, + msg.messageId, MessagesCompanion( errorWhileSending: Value(true), ), @@ -257,14 +223,13 @@ Future handleDownloadData(DownloadData data) async { } await twonlyDatabase.messagesDao.updateMessageByMessageId( - media.messageId, + msg.messageId, MessagesCompanion(downloadState: Value(DownloadState.downloaded)), ); - await deleteMediaFile(media.messageId, "encrypted"); + await deleteMediaFile(msg.messageId, "encrypted"); - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; + apiProvider.downloadDone(content.downloadToken!); } Future getImageBytes(int mediaId) async { diff --git a/lib/src/providers/api/media_send.dart b/lib/src/providers/api/media_send.dart index b590cca..815b8e8 100644 --- a/lib/src/providers/api/media_send.dart +++ b/lib/src/providers/api/media_send.dart @@ -102,7 +102,7 @@ Future initMediaUpload() async { Future addVideoToUpload(int mediaUploadId, File videoFilePath) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); - await videoFilePath.rename("$basePath.original.mp4"); + await videoFilePath.copy("$basePath.original.mp4"); return await compressVideoIfExists(mediaUploadId); } @@ -499,7 +499,7 @@ Future handleNotifyReceiver(MediaUpload media) async { Future compressVideoIfExists(int mediaUploadId) async { String basePath = await getMediaFilePath(mediaUploadId, "send"); - File videoOriginalFile = File("$basePath.orginal.mp4"); + File videoOriginalFile = File("$basePath.original.mp4"); File videoCompressedFile = File("$basePath.mp4"); if (videoCompressedFile.existsSync()) { diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index 8383cd7..6835ea0 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -35,8 +35,6 @@ Future handleServerMessage(server.ServerToClient msg) async { Uint8List body = Uint8List.fromList(msg.v0.newMessage.body); int fromUserId = msg.v0.newMessage.fromUserId.toInt(); response = await handleNewMessage(fromUserId, body); - } else if (msg.v0.hasDownloaddata()) { - response = await handleDownloadData(msg.v0.downloaddata); } else { Logger("handleServerMessage") .shout("Got a new message from the server: $msg"); diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index a19d3f1..a60a4fc 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -22,6 +22,7 @@ import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/media_received.dart'; import 'package:twonly/src/providers/api/media_send.dart'; import 'package:twonly/src/providers/api/server_messages.dart'; +import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/services/flame_service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -77,6 +78,7 @@ class ApiProvider { globalCallbackConnectionState(true); if (!globalIsAppInBackground) { + retransmitRawBytes(); tryTransmitMessages(); retryMediaUpload(); tryDownloadAllMediaFiles(); @@ -148,6 +150,7 @@ class ApiProvider { try { final msg = server.ServerToClient.fromBuffer(msgBuffer); if (msg.v0.hasResponse()) { + removeFromRetransmissionBuffer(msg.v0.seq); messagesV0[msg.v0.seq] = msg; } else { await handleServerMessage(msg); @@ -160,7 +163,7 @@ class ApiProvider { Future _waitForResponse(Int64 seq) async { final startTime = DateTime.now(); - final timeout = Duration(seconds: 10); + final timeout = Duration(seconds: 20); while (true) { if (messagesV0[seq] != null) { @@ -182,18 +185,51 @@ class ApiProvider { } } - Future sendRequestSync(ClientToServer request, - {bool authenticated = true}) async { - if (_channel == null) { - log.shout("sending request, but api is not connected."); - if (!await connect()) { - return Result.error(ErrorCode.InternalError); + Future> getRetransmission() async { + final box = await getMediaStorage(); + Map? retransmit = box.get("rawbytes-to-retransmit"); + // Map retransmit = {}; + // if (retransmitJson != null) { + // try { + // retransmit = jsonDecode(retransmitJson); + // } catch (e) { + // Logger("api.dart").shout("Could not decode the rawbytes messages: $e"); + // await box.delete("rawbytes-to-retransmit"); + // } + // } + return retransmit ?? {}; + } + + Future retransmitRawBytes() async { + var retransmit = await getRetransmission(); + Logger("api_provider.dart") + .info("Retransmit: ${retransmit.keys.length} messages"); + for (final seq in retransmit.keys) { + try { + _channel!.sink.add(base64Decode(retransmit[seq])); + } catch (e) { + Logger("api_provider.dart").shout("$e"); } } - if (_channel == null) { - return Result.error(ErrorCode.InternalError); - } + } + Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async { + var retransmit = await getRetransmission(); + retransmit[seq.toString()] = base64Encode(bytes); + final box = await getMediaStorage(); + box.put("rawbytes-to-retransmit", retransmit); + } + + Future removeFromRetransmissionBuffer(Int64 seq) async { + var retransmit = await getRetransmission(); + if (retransmit.isEmpty) return; + retransmit.remove(seq.toString()); + final box = await getMediaStorage(); + box.put("rawbytes-to-retransmit", retransmit); + } + + Future sendRequestSync(ClientToServer request, + {bool authenticated = true, bool ensureRetransmission = false}) async { var seq = Int64(Random().nextInt(4294967296)); while (messagesV0.containsKey(seq)) { seq = Int64(Random().nextInt(4294967296)); @@ -201,6 +237,21 @@ class ApiProvider { request.v0.seq = seq; final requestBytes = request.writeToBuffer(); + + if (ensureRetransmission) { + addToRetransmissionBuffer(seq, requestBytes); + } + + if (_channel == null) { + log.shout("sending request, but api is not connected."); + if (!await connect()) { + return Result.error(ErrorCode.InternalError); + } + if (_channel == null) { + return Result.error(ErrorCode.InternalError); + } + } + _channel!.sink.add(requestBytes); Result res = asResult(await _waitForResponse(seq)); @@ -372,6 +423,13 @@ class ApiProvider { return await sendRequestSync(req); } + Future downloadDone(List token) async { + var get = ApplicationData_DownloadDone()..downloadToken = token; + var appData = ApplicationData()..downloaddone = get; + var req = createClientToServerFromApplicationData(appData); + return await sendRequestSync(req, ensureRetransmission: true); + } + Future getCurrentLocation() async { var get = ApplicationData_GetLocation(); var appData = ApplicationData()..getlocation = get; @@ -379,15 +437,6 @@ class ApiProvider { return await sendRequestSync(req); } - Future triggerDownload(List token, int offset) async { - var get = ApplicationData_DownloadData() - ..downloadToken = token - ..offset = offset; - var appData = ApplicationData()..downloaddata = get; - var req = createClientToServerFromApplicationData(appData); - return await sendRequestSync(req); - } - Future uploadData(List uploadToken, Uint8List data, int offset, List? checksum) async { var get = ApplicationData_UploadData() diff --git a/lib/src/views/camera/camera_preview_view.dart b/lib/src/views/camera/camera_preview_view.dart index b37fad4..aadc815 100644 --- a/lib/src/views/camera/camera_preview_view.dart +++ b/lib/src/views/camera/camera_preview_view.dart @@ -290,12 +290,16 @@ class _CameraPreviewViewState extends State { if (isFront) { return; } + if (controller == null) return; + if (!controller!.value.isInitialized) return; scaleFactor = (baseScaleFactor + (basePanY - details.localPosition.dy) / 30) .clamp(1, _maxAvailableZoom); await controller!.setZoomLevel(scaleFactor); - setState(() {}); + if (mounted) { + setState(() {}); + } } Future pickImageFromGallery() async {