diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index acb83e5f..b5727649 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -194,6 +194,11 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : Log.i("FlwtchWorker::DownloadFile", "DownloadFile("+filepath+", "+manifestpath+")") Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey) } + "CheckDownloadStatus" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val fileKey = (a.get("fileKey") as? String) ?: "" + Cwtch.checkDownloadStatus(profile, fileKey) + } "SendProfileEvent" -> { val onion = (a.get("onion") as? String) ?: "" val jsonEvent = (a.get("jsonEvent") as? String) ?: "" diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index faa4fcec..0df9c0b0 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -46,6 +46,8 @@ abstract class Cwtch { void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey); // ignore: non_constant_identifier_names void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profile, String fileKey); // ignore: non_constant_identifier_names void ArchiveConversation(String profile, String handle); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 818d694b..0a3644ae 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -330,7 +330,7 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], int.parse(data["Progress"])); break; case "FileDownloaded": - profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"]); + profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); break; default: EnvironmentConfig.debugLog("unhandled event: $type"); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 9dfc7328..6b7cb552 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -397,6 +397,19 @@ class CwtchFfi implements Cwtch { // android only - do nothing } + @override + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profileOnion, String fileKey) { + var checkDownloadStatus = library.lookup>("c_CheckDownloadStatus"); + // ignore: non_constant_identifier_names + final CheckDownloadStatus = checkDownloadStatus.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = fileKey.toNativeUtf8(); + CheckDownloadStatus(u1, u1.length, u2, u2.length); + malloc.free(u1); + malloc.free(u2); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 40437cf7..7bab3150 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -148,6 +148,12 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey}); } + @override + // ignore: non_constant_identifier_names + void CheckDownloadStatus(String profileOnion, String fileKey) { + cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey}); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/model.dart b/lib/model.dart index 9dcac693..ae1d9c1f 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -381,14 +381,17 @@ class ProfileInfoState extends ChangeNotifier { } } - void downloadMarkFinished(String fileKey) { + void downloadMarkFinished(String fileKey, String finalPath) { if (!downloadActive(fileKey)) { - print("error: received download completion notice for unknown download "+fileKey); - } else { - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.complete = true; - notifyListeners(); + // happens as a result of a CheckDownloadStatus call, + // invoked from a historical (timeline) download message + // so setting numChunks correctly shouldn't matter + this.downloadInit(fileKey, 1); } + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.downloadedTo = finalPath; + this._downloads[fileKey]!.complete = true; + notifyListeners(); } bool downloadActive(String fileKey) { @@ -407,8 +410,12 @@ class ProfileInfoState extends ChangeNotifier { return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; } + String? downloadFinalPath(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; + } + String downloadSpeed(String fileKey) { - if (!downloadActive(fileKey)) { + if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { return "0 B/s"; } var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; @@ -425,6 +432,7 @@ class FileDownloadProgress { int chunksTotal = 1; bool complete = false; bool gotManifest = false; + String? downloadedTo; DateTime? timeStart; DateTime? timeEnd; diff --git a/lib/models/message.dart b/lib/models/message.dart index a6c28282..2156b8d6 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -35,38 +35,39 @@ Future messageHandler(BuildContext context, String profileOnion, String try { var rawMessageEnvelopeFuture = Provider.of(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index); return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { - dynamic messageWrapper = jsonDecode(rawMessageEnvelope); - // There are 2 conditions in which this error condition can be met: - // 1. The application == nil, in which case this instance of the UI is already - // broken beyond repair, and will either be replaced by a new version, or requires a complete - // restart. - // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. - // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the - // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one - // will find itself delayed. - // The second case is recoverable by tail-recursing this future. - if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { - return Future.delayed(Duration(seconds: 2), () { - print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); - return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); - }); - } - - // Construct the initial metadata - var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; - var senderHandle = messageWrapper['PeerID']; - var senderImage = messageWrapper['ContactImage']; - var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2); - var ackd = messageWrapper['Acknowledged']; - var error = messageWrapper['Error'] != null; - String? signature; - // If this is a group, store the signature - if (contactHandle.length == GroupConversationHandleLength) { - signature = messageWrapper['Signature']; - } - var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); - + var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true); try { + dynamic messageWrapper = jsonDecode(rawMessageEnvelope); + // There are 2 conditions in which this error condition can be met: + // 1. The application == nil, in which case this instance of the UI is already + // broken beyond repair, and will either be replaced by a new version, or requires a complete + // restart. + // 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion. + // This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the + // calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one + // will find itself delayed. + // The second case is recoverable by tail-recursing this future. + if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { + return Future.delayed(Duration(seconds: 2), () { + print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug."); + return messageHandler(context, profileOnion, contactHandle, index).then((value) => value); + }); + } + + // Construct the initial metadata + var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; + var senderHandle = messageWrapper['PeerID']; + var senderImage = messageWrapper['ContactImage']; + var flags = int.parse(messageWrapper['Flags'].toString()); + var ackd = messageWrapper['Acknowledged']; + var error = messageWrapper['Error'] != null; + String? signature; + // If this is a group, store the signature + if (contactHandle.length == GroupConversationHandleLength) { + signature = messageWrapper['Signature']; + } + metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error); + dynamic message = jsonDecode(messageWrapper['Message']); var content = message['d'] as dynamic; var overlay = int.parse(message['o'].toString()); @@ -86,6 +87,7 @@ Future messageHandler(BuildContext context, String profileOnion, String return MalformedMessage(metadata); } } catch (e) { + print("an error! " + e.toString()); return MalformedMessage(metadata); } }); diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 7286f7cf..49aef579 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -39,7 +39,7 @@ class FileBubbleState extends State { @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; - //isAccepted = Provider.of(context).contactList.getContact(widget.inviteTarget) != null; + var flagStarted = Provider.of(context).flags & 0x02 > 0; var borderRadiousEh = 15.0; var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment); var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of(context).timestamp); @@ -66,27 +66,40 @@ class FileBubbleState extends State { ? senderFileChrome( AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize) : (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, Provider.of(context).downloadSpeed(widget.fileKey()))); - Widget wdgDecorations; if (!showFileSharing) { wdgDecorations = Text('\u202F'); } else if (fromMe) { wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate); } else if (Provider.of(context).downloadComplete(widget.fileKey())) { - wdgDecorations = Center( - widthFactor: 1, - child: Wrap(children: [ - Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.openFolderButton + '\u202F'), onPressed: _btnAccept)), - ])); + // in this case, whatever marked download.complete would have also set the path + var path = Provider.of(context).downloadFinalPath(widget.fileKey())!; + wdgDecorations = Text('Saved to: ' + path + '\u202F'); } else if (Provider.of(context).downloadActive(widget.fileKey())) { - if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { - wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); + if (!Provider.of(context).downloadGotManifest( + widget.fileKey())) { + wdgDecorations = Text( + AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); } else { wdgDecorations = LinearProgressIndicator( - value: Provider.of(context).downloadProgress(widget.fileKey()), - color: Provider.of(context).theme.defaultButtonActiveColor(), + value: Provider.of(context).downloadProgress( + widget.fileKey()), + color: Provider + .of(context) + .theme + .defaultButtonActiveColor(), ); } + } else if (flagStarted) { + // in this case, the download was done in a previous application launch, + // so we probably have to request an info lookup + var path = Provider.of(context).downloadFinalPath(widget.fileKey()); + if (path == null) { + wdgDecorations = Text('Checking download status...' + '\u202F'); + Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); + } else { + wdgDecorations = Text('Saved to: ' + (path??"null") + '\u202F'); + } } else { wdgDecorations = Center( widthFactor: 1, @@ -135,10 +148,13 @@ class FileBubbleState extends State { File? file; var profileOnion = Provider.of(context, listen: false).onion; var handle = Provider.of(context, listen: false).senderHandle; + var contact = Provider.of(context, listen: false).onion; + var idx = Provider.of(context, listen: false).messageIndex; if (Platform.isAndroid) { - //todo: would be better to only call downloadInit if CreateDownloadableFile results in a user-pick (they might cancel) Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + Provider.of(context, listen: false).flags |= 0x02; Provider.of(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey()); } else { try { @@ -148,6 +164,8 @@ class FileBubbleState extends State { print("saving to " + file.path); var manifestPath = file.path + ".manifest"; Provider.of(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil()); + Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x02); + Provider.of(context, listen: false).flags |= 0x02; Provider.of(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey()); } } catch (e) { diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 3a9c2585..0e6b9219 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -131,7 +131,7 @@ class InvitationBubbleState extends State { var contact = Provider.of(context, listen: false).onion; var idx = Provider.of(context, listen: false).messageIndex; Provider.of(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of(context, listen: false).flags | 0x01); - Provider.of(context).flags |= 0x01; + Provider.of(context, listen: false).flags |= 0x01; }); } diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index a6a8353d..ac42a8a7 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -87,7 +87,7 @@ class _MessageListState extends State { // Already includes MessageRow,, return message.getWidget(context); } else { - return MessageLoadingBubble(); + return Text('');//MessageLoadingBubble(); } }, );