forked from cwtch.im/cwtch-ui
file resumption support
This commit is contained in:
parent
2b8f8e825f
commit
e55d9301e4
|
@ -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) ?: ""
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"]);
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue