From e55d9301e407106c2b590a451044ee9099d67d67 Mon Sep 17 00:00:00 2001 From: erinn Date: Thu, 4 Nov 2021 15:31:50 -0700 Subject: [PATCH] file resumption support --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 44 +++++++++++-------- lib/cwtch/cwtch.dart | 2 + lib/cwtch/cwtchNotifier.dart | 7 ++- lib/cwtch/ffi.dart | 16 +++++++ lib/cwtch/gomobile.dart | 6 +++ lib/model.dart | 34 ++++++++++++-- lib/models/messages/filemessage.dart | 12 +++++ lib/widgets/filebubble.dart | 25 ++++++++--- 8 files changed, 119 insertions(+), 27 deletions(-) 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 f4ef3152..471a2ede 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -112,24 +112,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : if (dlID == null) { dlID = 0; } - val channelId = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createDownloadNotificationChannel(fileKey, fileKey) - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - "" - }; - val newNotification = NotificationCompat.Builder(applicationContext, channelId) - .setOngoing(true) - .setContentTitle("Downloading")//todo: translate - .setContentText(title) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setProgress(progressMax, progress, false) - .setSound(null) - //.setSilent(true) - .build(); - notificationManager.notify(dlID, newNotification); + if (progress >= 0) { + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createDownloadNotificationChannel(fileKey, fileKey) + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + }; + val newNotification = NotificationCompat.Builder(applicationContext, channelId) + .setOngoing(true) + .setContentTitle("Downloading")//todo: translate + .setContentText(title) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setProgress(progressMax, progress, false) + .setSound(null) + //.setSilent(true) + .build(); + notificationManager.notify(dlID, newNotification); + } } catch (e: Exception) { Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); } @@ -241,6 +243,12 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val fileKey = (a.get("fileKey") as? String) ?: "" Cwtch.checkDownloadStatus(profile, fileKey) } + "VerifyOrResumeDownload" -> { + val profile = (a.get("ProfileOnion") as? String) ?: "" + val handle = (a.get("handle") as? String) ?: "" + val fileKey = (a.get("fileKey") as? String) ?: "" + Cwtch.verifyOrResumeDownload(profile, handle, 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 0df9c0b0..2a8b03e7 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -48,6 +48,8 @@ abstract class Cwtch { 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 VerifyOrResumeDownload(String profile, String handle, 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 2f952dd0..6012385a 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -340,7 +340,12 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]); break; case "FileDownloadProgressUpdate": - profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], int.parse(data["Progress"])); + var progress = int.parse(data["Progress"]); + profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], progress, int.parse(data["FileSizeInChunks"])); + // progress == -1 is a "download was interrupted" message and should contain a path + if (progress < 0) { + profileCN.getProfile(data["ProfileOnion"])?.downloadSetPath(data["FileKey"], data["FilePath"]); + } break; case "FileDownloaded": profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index 6b7cb552..1ae11669 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -410,6 +410,22 @@ class CwtchFfi implements Cwtch { malloc.free(u2); } + @override + // ignore: non_constant_identifier_names + void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { + var fn = library.lookup>("c_VerifyOrResumeDownload"); + // ignore: non_constant_identifier_names + final VerifyOrResumeDownload = fn.asFunction(); + final u1 = profileOnion.toNativeUtf8(); + final u2 = contactHandle.toNativeUtf8(); + final u3 = filekey.toNativeUtf8(); + VerifyOrResumeDownload(u1, u1.length, u2, u2.length, u3, u3.length); + malloc.free(u1); + malloc.free(u2); + malloc.free(u3); + } + + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index bf73813e..c57d1cdb 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -154,6 +154,12 @@ class CwtchGomobile implements Cwtch { cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey}); } + @override + // ignore: non_constant_identifier_names + void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) { + cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey}); + } + @override // ignore: non_constant_identifier_names void ResetTor() { diff --git a/lib/model.dart b/lib/model.dart index f036be0f..94be845d 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -362,11 +362,21 @@ class ProfileInfoState extends ChangeNotifier { this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); } - void downloadUpdate(String fileKey, int progress) { + void downloadUpdate(String fileKey, int progress, int numChunks) { if (!downloadActive(fileKey)) { - print("error: received progress for unknown download " + fileKey); + if (progress < 0) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + this._downloads[fileKey]!.interrupted = true; + notifyListeners(); + } else { + print("error: received progress for unknown download " + fileKey); + } } else { + if (this._downloads[fileKey]!.interrupted) { + this._downloads[fileKey]!.interrupted = false; + } this._downloads[fileKey]!.chunksDownloaded = progress; + this._downloads[fileKey]!.chunksTotal = numChunks; notifyListeners(); } } @@ -394,7 +404,7 @@ class ProfileInfoState extends ChangeNotifier { } bool downloadActive(String fileKey) { - return this._downloads.containsKey(fileKey); + return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted; } bool downloadGotManifest(String fileKey) { @@ -405,10 +415,27 @@ class ProfileInfoState extends ChangeNotifier { return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; } + bool downloadInterrupted(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted; + } + + void downloadMarkResumed(String fileKey) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.interrupted = false; + } + } + double downloadProgress(String fileKey) { return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; } + // used for loading interrupted download info; use downloadMarkFinished for successful downloads + void downloadSetPath(String fileKey, String path) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.downloadedTo = path; + } + } + String? downloadFinalPath(String fileKey) { return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; } @@ -431,6 +458,7 @@ class FileDownloadProgress { int chunksTotal = 1; bool complete = false; bool gotManifest = false; + bool interrupted = false; String? downloadedTo; DateTime? timeStart; DateTime? timeEnd; diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index fa178dd4..43da9899 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -12,6 +12,7 @@ import '../../model.dart'; class FileMessage extends Message { final MessageMetadata metadata; final String content; + final RegExp nonHex = RegExp(r'[^a-f0-9]'); FileMessage(this.metadata, this.content); @@ -30,6 +31,10 @@ class FileMessage extends Message { String nonce = shareObj['n'] as String; int fileSize = shareObj['s'] as int; + if (!validHash(rootHash, nonce)) { + return MessageRow(MalformedBubble()); + } + return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of(bcontext).getMessageKey(idx)); }); } @@ -47,6 +52,9 @@ class FileMessage extends Message { String rootHash = shareObj['h'] as String; String nonce = shareObj['n'] as String; int fileSize = shareObj['s'] as int; + if (!validHash(rootHash, nonce)) { + return MessageRow(MalformedBubble()); + } return FileBubble( nameSuggestion, rootHash, @@ -61,4 +69,8 @@ class FileMessage extends Message { MessageMetadata getMetadata() { return this.metadata; } + + bool validHash(String hash, String nonce) { + return hash.length == 128 && nonce.length == 48 && !hash.contains(nonHex) && !nonce.contains(nonHex); + } } diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 496e0924..a0052919 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -33,6 +33,11 @@ class FileBubble extends StatefulWidget { } class FileBubbleState extends State { + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion; @@ -71,7 +76,7 @@ class FileBubbleState extends State { } else if (Provider.of(context).downloadComplete(widget.fileKey())) { // 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'); + wdgDecorations = Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F'); } else if (Provider.of(context).downloadActive(widget.fileKey())) { if (!Provider.of(context).downloadGotManifest(widget.fileKey())) { wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); @@ -84,12 +89,15 @@ class FileBubbleState extends State { } 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'); + if (!Provider.of(context).downloadInterrupted(widget.fileKey()) ) { + wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F'); Provider.of(context, listen: false).cwtch.CheckDownloadStatus(Provider.of(context, listen: false).onion, widget.fileKey()); } else { - wdgDecorations = Text('Saved to: ' + path + '\u202F'); + var path = Provider.of(context).downloadFinalPath(widget.fileKey()) ?? ""; + wdgDecorations = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text('Verify/resume'))] + ); } } else { wdgDecorations = Center( @@ -167,6 +175,13 @@ class FileBubbleState extends State { } } + void _btnResume() async { + var profileOnion = Provider.of(context, listen: false).onion; + var handle = Provider.of(context, listen: false).senderHandle; + Provider.of(context, listen: false).downloadMarkResumed(widget.fileKey()); + Provider.of(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey()); + } + // Construct an file chrome for the sender Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { return ListTile(