file resumption support

This commit is contained in:
erinn 2021-11-04 15:31:50 -07:00
parent 2b8f8e825f
commit e55d9301e4
8 changed files with 119 additions and 27 deletions

View File

@ -112,24 +112,26 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
if (dlID == null) { if (dlID == null) {
dlID = 0; dlID = 0;
} }
val channelId = if (progress >= 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channelId =
createDownloadNotificationChannel(fileKey, fileKey) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
} else { createDownloadNotificationChannel(fileKey, fileKey)
// If earlier version channel ID is not used } else {
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) // 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) val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle("Downloading")//todo: translate .setOngoing(true)
.setContentText(title) .setContentTitle("Downloading")//todo: translate
.setSmallIcon(android.R.drawable.stat_sys_download) .setContentText(title)
.setProgress(progressMax, progress, false) .setSmallIcon(android.R.drawable.stat_sys_download)
.setSound(null) .setProgress(progressMax, progress, false)
//.setSilent(true) .setSound(null)
.build(); //.setSilent(true)
notificationManager.notify(dlID, newNotification); .build();
notificationManager.notify(dlID, newNotification);
}
} catch (e: Exception) { } catch (e: Exception) {
Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace()); 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) ?: "" val fileKey = (a.get("fileKey") as? String) ?: ""
Cwtch.checkDownloadStatus(profile, fileKey) 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" -> { "SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: "" val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: "" val jsonEvent = (a.get("jsonEvent") as? String) ?: ""

View File

@ -48,6 +48,8 @@ abstract class Cwtch {
void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey); void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void CheckDownloadStatus(String profile, String fileKey); void CheckDownloadStatus(String profile, String fileKey);
// ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profile, String handle, String filekey);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ArchiveConversation(String profile, String handle); void ArchiveConversation(String profile, String handle);

View File

@ -340,7 +340,12 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]); profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]);
break; break;
case "FileDownloadProgressUpdate": 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; break;
case "FileDownloaded": case "FileDownloaded":
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]); profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);

View File

@ -410,6 +410,22 @@ class CwtchFfi implements Cwtch {
malloc.free(u2); malloc.free(u2);
} }
@override
// ignore: non_constant_identifier_names
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) {
var fn = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_VerifyOrResumeDownload");
// ignore: non_constant_identifier_names
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringStringStringFn>();
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 @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor() { void ResetTor() {

View File

@ -154,6 +154,12 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey}); 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 @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor() { void ResetTor() {

View File

@ -362,11 +362,21 @@ class ProfileInfoState extends ChangeNotifier {
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
} }
void downloadUpdate(String fileKey, int progress) { void downloadUpdate(String fileKey, int progress, int numChunks) {
if (!downloadActive(fileKey)) { 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 { } else {
if (this._downloads[fileKey]!.interrupted) {
this._downloads[fileKey]!.interrupted = false;
}
this._downloads[fileKey]!.chunksDownloaded = progress; this._downloads[fileKey]!.chunksDownloaded = progress;
this._downloads[fileKey]!.chunksTotal = numChunks;
notifyListeners(); notifyListeners();
} }
} }
@ -394,7 +404,7 @@ class ProfileInfoState extends ChangeNotifier {
} }
bool downloadActive(String fileKey) { bool downloadActive(String fileKey) {
return this._downloads.containsKey(fileKey); return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted;
} }
bool downloadGotManifest(String fileKey) { bool downloadGotManifest(String fileKey) {
@ -405,10 +415,27 @@ class ProfileInfoState extends ChangeNotifier {
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; 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) { double downloadProgress(String fileKey) {
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; 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) { String? downloadFinalPath(String fileKey) {
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null;
} }
@ -431,6 +458,7 @@ class FileDownloadProgress {
int chunksTotal = 1; int chunksTotal = 1;
bool complete = false; bool complete = false;
bool gotManifest = false; bool gotManifest = false;
bool interrupted = false;
String? downloadedTo; String? downloadedTo;
DateTime? timeStart; DateTime? timeStart;
DateTime? timeEnd; DateTime? timeEnd;

View File

@ -12,6 +12,7 @@ import '../../model.dart';
class FileMessage extends Message { class FileMessage extends Message {
final MessageMetadata metadata; final MessageMetadata metadata;
final String content; final String content;
final RegExp nonHex = RegExp(r'[^a-f0-9]');
FileMessage(this.metadata, this.content); FileMessage(this.metadata, this.content);
@ -30,6 +31,10 @@ class FileMessage extends Message {
String nonce = shareObj['n'] as String; String nonce = shareObj['n'] as String;
int fileSize = shareObj['s'] as int; int fileSize = shareObj['s'] as int;
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
}
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)); return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
}); });
} }
@ -47,6 +52,9 @@ class FileMessage extends Message {
String rootHash = shareObj['h'] as String; String rootHash = shareObj['h'] as String;
String nonce = shareObj['n'] as String; String nonce = shareObj['n'] as String;
int fileSize = shareObj['s'] as int; int fileSize = shareObj['s'] as int;
if (!validHash(rootHash, nonce)) {
return MessageRow(MalformedBubble());
}
return FileBubble( return FileBubble(
nameSuggestion, nameSuggestion,
rootHash, rootHash,
@ -61,4 +69,8 @@ class FileMessage extends Message {
MessageMetadata getMetadata() { MessageMetadata getMetadata() {
return this.metadata; return this.metadata;
} }
bool validHash(String hash, String nonce) {
return hash.length == 128 && nonce.length == 48 && !hash.contains(nonHex) && !nonce.contains(nonHex);
}
} }

View File

@ -33,6 +33,11 @@ class FileBubble extends StatefulWidget {
} }
class FileBubbleState extends State<FileBubble> { class FileBubbleState extends State<FileBubble> {
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
@ -71,7 +76,7 @@ class FileBubbleState extends State<FileBubble> {
} else if (Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey())) { } else if (Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey())) {
// in this case, whatever marked download.complete would have also set the path // in this case, whatever marked download.complete would have also set the path
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey())!; var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey())!;
wdgDecorations = Text('Saved to: ' + path + '\u202F'); wdgDecorations = Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F');
} else if (Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey())) { } else if (Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey())) {
if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) { if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) {
wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F'); wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F');
@ -84,12 +89,15 @@ class FileBubbleState extends State<FileBubble> {
} else if (flagStarted) { } else if (flagStarted) {
// in this case, the download was done in a previous application launch, // in this case, the download was done in a previous application launch,
// so we probably have to request an info lookup // so we probably have to request an info lookup
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()); if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) {
if (path == null) { wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
wdgDecorations = Text('Checking download status...' + '\u202F');
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey()); Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
} else { } else {
wdgDecorations = Text('Saved to: ' + path + '\u202F'); var path = Provider.of<ProfileInfoState>(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 { } else {
wdgDecorations = Center( wdgDecorations = Center(
@ -167,6 +175,13 @@ class FileBubbleState extends State<FileBubble> {
} }
} }
void _btnResume() async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
}
// Construct an file chrome for the sender // Construct an file chrome for the sender
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) { Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
return ListTile( return ListTile(