Merge pull request 'download resumption, hash verification' (#231) from filefix2 into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #231
This commit is contained in:
Sarah Jamie Lewis 2021-11-05 00:08:56 +00:00
commit 0dc4849a5d
16 changed files with 134 additions and 35 deletions

View File

@ -1 +1 @@
v1.3.1-25-g4adb501-2021-11-04-04-20
v1.3.1-31-g706be7f-2021-11-04-22-49

View File

@ -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) ?: ""

View File

@ -52,6 +52,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);

View File

@ -366,7 +366,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"]);

View File

@ -413,6 +413,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<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
// ignore: non_constant_identifier_names
void ResetTor() {

View File

@ -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() {

View File

@ -1,6 +1,7 @@
{
"@@locale": "de",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "en",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "es",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "fr",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "it",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "pl",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -1,6 +1,7 @@
{
"@@locale": "pt",
"@@last_modified": "2021-11-04T22:56:21+01:00",
"@@last_modified": "2021-11-04T23:42:54+01:00",
"verfiyResumeButton": "Verify\/resume",
"fileCheckingStatus": "Checking download status",
"fileInterrupted": "Interrupted",
"fileSavedTo": "Saved to",

View File

@ -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;

View File

@ -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<ContactInfoState>(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);
}
}

View File

@ -33,6 +33,11 @@ class FileBubble extends StatefulWidget {
}
class FileBubbleState extends State<FileBubble> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
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())) {
// in this case, whatever marked download.complete would have also set the path
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())) {
if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) {
wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F');
@ -84,12 +89,15 @@ class FileBubbleState extends State<FileBubble> {
} 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<ProfileInfoState>(context).downloadFinalPath(widget.fileKey());
if (path == null) {
wdgDecorations = Text('Checking download status...' + '\u202F');
if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) {
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
} 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(AppLocalizations.of(context)!.verfiyResumeButton))]
);
}
} else {
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
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
return ListTile(