mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
fix #185
This commit is contained in:
parent
87bc1ed824
commit
8cb5e77686
5 changed files with 136 additions and 120 deletions
|
|
@ -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<int, DateTime> 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<List<int>> chunks = [];
|
||||
int downloaded = 0;
|
||||
|
||||
response.asStream().listen((http.StreamedResponse r) {
|
||||
r.stream.listen((List<int> 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<int> 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<client.Response> 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<client.Response> 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<client.Response> 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<client.Response> 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<Uint8List?> getImageBytes(int mediaId) async {
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ Future<int?> initMediaUpload() async {
|
|||
|
||||
Future<bool> 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<bool> handleNotifyReceiver(MediaUpload media) async {
|
|||
|
||||
Future<bool> 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()) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<server.ServerToClient?> _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<Result> 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<Map<String, dynamic>> getRetransmission() async {
|
||||
final box = await getMediaStorage();
|
||||
Map<String, dynamic>? retransmit = box.get("rawbytes-to-retransmit");
|
||||
// Map<String, dynamic> 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<Result> 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<Result> downloadDone(List<int> token) async {
|
||||
var get = ApplicationData_DownloadDone()..downloadToken = token;
|
||||
var appData = ApplicationData()..downloaddone = get;
|
||||
var req = createClientToServerFromApplicationData(appData);
|
||||
return await sendRequestSync(req, ensureRetransmission: true);
|
||||
}
|
||||
|
||||
Future<Result> getCurrentLocation() async {
|
||||
var get = ApplicationData_GetLocation();
|
||||
var appData = ApplicationData()..getlocation = get;
|
||||
|
|
@ -379,15 +437,6 @@ class ApiProvider {
|
|||
return await sendRequestSync(req);
|
||||
}
|
||||
|
||||
Future<Result> triggerDownload(List<int> 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<Result> uploadData(List<int> uploadToken, Uint8List data, int offset,
|
||||
List<int>? checksum) async {
|
||||
var get = ApplicationData_UploadData()
|
||||
|
|
|
|||
|
|
@ -290,12 +290,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue