diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version
index 368ca44f..6d5fa28d 100644
--- a/LIBCWTCH-GO-MACOS.version
+++ b/LIBCWTCH-GO-MACOS.version
@@ -1 +1 @@
-2022-03-10-17-32-v1.6.0-6-gc2874db
\ No newline at end of file
+2022-03-10-19-05-v1.6.0-9-g5b34715
\ No newline at end of file
diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version
index 79a33b2e..f4acd5e9 100644
--- a/LIBCWTCH-GO.version
+++ b/LIBCWTCH-GO.version
@@ -1 +1 @@
-2022-03-10-22-49-v1.6.0-6-gc2874db
\ No newline at end of file
+2022-03-11-00-06-v1.6.0-9-g5b34715
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d95c4e96..ff06f437 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -46,7 +46,7 @@
-
+
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 52037442..35980b5e 100644
--- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt
+++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt
@@ -1,6 +1,7 @@
package im.cwtch.flwtch
import android.app.*
+import android.os.Environment
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
@@ -428,6 +429,17 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
notificationConversationInfo = (a.get("notificationConversationInfo") as? String)
?: "New Message From "
}
+ "ExportProfile" -> {
+ val profileOnion = (a.get("ProfileOnion") as? String) ?: ""
+ val file = StringBuilder().append(this.applicationContext.cacheDir).append("/").append((a.get("file") as? String) ?: "").toString()
+ Log.i("FlwtchWorker", "constructing exported file " + file);
+ Cwtch.exportProfile(profileOnion,file)
+ }
+ "ImportProfile" -> {
+ val file = (a.get("file") as? String) ?: ""
+ val pass = (a.get("pass") as? String) ?: ""
+ return Result.success(Data.Builder().putString("result", Cwtch.importProfile(file, pass)).build());
+ }
else -> {
Log.i(TAG, "unknown command: " + method);
return Result.failure()
diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
index 499c01ce..383887bb 100644
--- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
+++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
@@ -62,6 +62,7 @@ class MainActivity: FlutterActivity() {
// "Download to..." prompt extra arguments
private val FILEPICKER_REQUEST_CODE = 234
private val PREVIEW_EXPORT_REQUEST_CODE = 235
+ private val PROFILE_EXPORT_REQUEST_CODE = 236
private var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
@@ -110,8 +111,6 @@ class MainActivity: FlutterActivity() {
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
} else if (requestCode == PREVIEW_EXPORT_REQUEST_CODE) {
val targetPath = intent!!.getData().toString()
- var srcFile = File(this.exportFromPath)
- Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file")
val sourcePath = Paths.get(this.exportFromPath);
val targetUri = Uri.parse(targetPath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
@@ -122,6 +121,20 @@ class MainActivity: FlutterActivity() {
os?.close();
//Files.delete(sourcePath);
}
+ } else if (requestCode == PROFILE_EXPORT_REQUEST_CODE ) {
+ val targetPath = intent!!.getData().toString()
+ val srcFile = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.exportFromPath).toString();
+ Log.i("MainActivity:PREVIEW_EXPORT", "exporting previewed file " + srcFile);
+ val sourcePath = Paths.get(srcFile);
+ val targetUri = Uri.parse(targetPath);
+ val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
+ val bytesWritten = Files.copy(sourcePath, os);
+ Log.d("MainActivity:PREVIEW_EXPORT", "copied " + bytesWritten.toString() + " bytes");
+ if (bytesWritten != 0L) {
+ os?.flush();
+ os?.close();
+ //Files.delete(sourcePath);
+ }
}
}
@@ -211,6 +224,14 @@ class MainActivity: FlutterActivity() {
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
return
+ } else if (call.method == "ExportProfile") {
+ this.exportFromPath = argmap["file"] ?: ""
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/gzip"
+ putExtra(Intent.EXTRA_TITLE, argmap["file"])
+ }
+ startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
diff --git a/lib/controllers/enter_password.dart b/lib/controllers/enter_password.dart
new file mode 100644
index 00000000..92e5af7a
--- /dev/null
+++ b/lib/controllers/enter_password.dart
@@ -0,0 +1,43 @@
+import 'package:cwtch/widgets/passwordfield.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+void showPasswordDialog(BuildContext context, String title, String action, Function(String) onEntered) {
+ TextEditingController passwordController = TextEditingController();
+ CwtchPasswordField passwordField = CwtchPasswordField(
+ controller: passwordController,
+ validator: (passsword) {
+ return null;
+ });
+
+ // set up the buttons
+ Widget cancelButton = ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.cancel),
+ onPressed: () {
+ Navigator.of(context).pop(); // dismiss dialog
+ },
+ );
+ Widget continueButton = ElevatedButton(
+ child: Text(action),
+ onPressed: () {
+ onEntered(passwordController.value.text);
+ });
+
+ // set up the AlertDialog
+ AlertDialog alert = AlertDialog(
+ title: Text(title),
+ content: passwordField,
+ actions: [
+ cancelButton,
+ continueButton,
+ ],
+ );
+
+ // show the dialog
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return alert;
+ },
+ );
+}
diff --git a/lib/controllers/filesharing.dart b/lib/controllers/filesharing.dart
index cbfdaa20..49da9bdf 100644
--- a/lib/controllers/filesharing.dart
+++ b/lib/controllers/filesharing.dart
@@ -28,3 +28,17 @@ void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Fu
onCancel();
}
}
+
+Future showCreateFilePicker(BuildContext ctx) async {
+ // only allow one file picker at a time
+ // note: ideally we would destroy file picker when leaving a conversation
+ // but we don't currently have that option.
+ // we need to store AppState in a variable because ctx might be destroyed
+ // while awaiting for pickFiles.
+ var appstate = Provider.of(ctx, listen: false);
+ appstate.disableFilePicker = true;
+ // currently lockParentWindow only works on Windows...
+ String? result = await FilePicker.platform.saveFile(lockParentWindow: true);
+ appstate.disableFilePicker = false;
+ return result;
+}
diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart
index b06040f6..e4a65896 100644
--- a/lib/cwtch/cwtch.dart
+++ b/lib/cwtch/cwtch.dart
@@ -1,5 +1,3 @@
-import 'package:flutter/src/services/text_input.dart';
-
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
const DefaultPassword = "be gay do crime";
@@ -19,6 +17,11 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names
void ChangePassword(String profile, String pass, String newpass, String newpassAgain);
+ // ignore: non_constant_identifier_names
+ void ExportProfile(String profile, String file);
+ // ignore: non_constant_identifier_names
+ Future ImportProfile(String file, String pass);
+
// ignore: non_constant_identifier_names
void ResetTor();
diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart
index c5e56206..a1c8fe2e 100644
--- a/lib/cwtch/ffi.dart
+++ b/lib/cwtch/ffi.dart
@@ -52,6 +52,9 @@ typedef StringFn = void Function(Pointer dir, int);
typedef string_string_to_void_function = Void Function(Pointer str, Int32 length, Pointer str2, Int32 length2);
typedef StringStringFn = void Function(Pointer, int, Pointer, int);
+typedef string_string_to_string_function = Pointer Function(Pointer str, Int32 length, Pointer str2, Int32 length2);
+typedef StringFromStringStringFn = Pointer Function(Pointer, int, Pointer, int);
+
typedef string_int_to_void_function = Void Function(Pointer str, Int32 length, Int32 handle);
typedef VoidFromStringIntFn = void Function(Pointer, int, int);
@@ -756,4 +759,33 @@ class CwtchFfi implements Cwtch {
cwtchNotifier.l10nInit(notificationSimple, notificationConversationInfo);
_isL10nInit = true;
}
+
+ @override
+ // ignore: non_constant_identifier_names
+ void ExportProfile(String profile, String file) {
+ final utf8profile = profile.toNativeUtf8();
+ final utf8file = file.toNativeUtf8();
+ var exportProfileC = library.lookup>("c_ExportProfile");
+ // ignore: non_constant_identifier_names
+ final ExportProfileFn = exportProfileC.asFunction();
+ ExportProfileFn(utf8profile, utf8profile.length, utf8file, utf8file.length);
+ malloc.free(utf8profile);
+ malloc.free(utf8file);
+ }
+
+ @override
+ // ignore: non_constant_identifier_names
+ Future ImportProfile(String file, String pass) async {
+ final utf8pass = pass.toNativeUtf8();
+ final utf8file = file.toNativeUtf8();
+ var exportProfileC = library.lookup>("c_ImportProfile");
+ // ignore: non_constant_identifier_names
+ final ExportProfileFn = exportProfileC.asFunction();
+ Pointer result = ExportProfileFn(utf8file, utf8file.length, utf8pass, utf8pass.length);
+ String importResult = result.toDartString();
+ _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
+ malloc.free(utf8pass);
+ malloc.free(utf8file);
+ return importResult;
+ }
}
diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart
index 91a08cec..fc3db6f7 100644
--- a/lib/cwtch/gomobile.dart
+++ b/lib/cwtch/gomobile.dart
@@ -308,4 +308,16 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("L10nInit", {"notificationSimple": notificationSimple, "notificationConversationInfo": notificationConversationInfo});
_isL10nInit = true;
}
+
+ @override
+ // ignore: non_constant_identifier_names
+ void ExportProfile(String profile, String file) {
+ cwtchPlatform.invokeMethod("ExportProfile", {"ProfileOnion": profile, "file": file});
+ }
+
+ @override
+ // ignore: non_constant_identifier_names
+ Future ImportProfile(String file, String pass) {
+ return cwtchPlatform.invokeMethod("ImportProfile", {"file": file, "pass": pass});
+ }
}
diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart
index 68a1f530..7caa717f 100644
--- a/lib/views/addeditprofileview.dart
+++ b/lib/views/addeditprofileview.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cwtch/config.dart';
+import 'package:cwtch/controllers/filesharing.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart';
@@ -286,6 +287,34 @@ class _AddEditProfileViewState extends State {
),
],
),
+ Visibility(
+ visible: Provider.of(context, listen: false).onion.isNotEmpty,
+ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
+ SizedBox(
+ height: 20,
+ ),
+ Tooltip(
+ message: AppLocalizations.of(context)!.exportProfileTooltip,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ if (Platform.isAndroid) {
+ Provider.of(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
+ final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ } else {
+ showCreateFilePicker(context).then((name) {
+ if (name != null) {
+ Provider.of(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
+ final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ }
+ });
+ }
+ },
+ icon: Icon(Icons.import_export),
+ label: Text(AppLocalizations.of(context)!.exportProfile),
+ ))
+ ])),
Visibility(
visible: Provider.of(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart
index f22de587..829ba74e 100644
--- a/lib/views/profilemgrview.dart
+++ b/lib/views/profilemgrview.dart
@@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:io';
+import 'package:cwtch/constants.dart';
+import 'package:cwtch/controllers/enter_password.dart';
+import 'package:cwtch/controllers/filesharing.dart';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart';
@@ -67,7 +70,7 @@ class _ProfileMgrViewState extends State {
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
- onPressed: _pushAddProfile,
+ onPressed: _modalAddImportProfiles,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
@@ -159,8 +162,9 @@ class _ProfileMgrViewState extends State {
));
}
- void _pushAddProfile({onion: ""}) {
- Navigator.of(context).push(MaterialPageRoute(
+ void _pushAddProfile(bcontext, {onion: ""}) {
+ Navigator.popUntil(bcontext, (route) => route.isFirst);
+ Navigator.of(bcontext).push(MaterialPageRoute(
builder: (BuildContext context) {
return MultiProvider(
providers: [
@@ -174,6 +178,70 @@ class _ProfileMgrViewState extends State {
));
}
+ void _modalAddImportProfiles() {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ builder: (BuildContext context) {
+ return Padding(
+ padding: MediaQuery.of(context).viewInsets,
+ child: RepaintBoundary(
+ child: Container(
+ height: 200, // bespoke value courtesy of the [TextField] docs
+ child: Center(
+ child: Padding(
+ padding: EdgeInsets.all(10.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
+ Spacer(),
+ Expanded(
+ child: ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.addProfileTitle, semanticsLabel: AppLocalizations.of(context)!.addProfileTitle),
+ onPressed: () {
+ _pushAddProfile(context);
+ },
+ )),
+ Spacer()
+ ]),
+ SizedBox(
+ height: 20,
+ ),
+ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
+ Spacer(),
+ Expanded(
+ child: Tooltip(
+ message: AppLocalizations.of(context)!.importProfileTooltip,
+ child: ElevatedButton(
+ child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
+ onPressed: () {
+ // 10GB profiles should be enough for anyone?
+ showFilePicker(context, MaxGeneralFileSharingSize, (file) {
+ showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
+ Navigator.popUntil(context, (route) => route.isFirst);
+ Provider.of(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
+ if (value == "") {
+ final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ } else {
+ final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ }
+ });
+ });
+ }, () {}, () {});
+ },
+ ))),
+ Spacer()
+ ]),
+ ],
+ ))),
+ )));
+ });
+ }
+
void _modalUnlockProfiles() {
showModalBottomSheet(
context: context,
diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart
index 0c944c13..44d09d75 100644
--- a/lib/widgets/filebubble.dart
+++ b/lib/widgets/filebubble.dart
@@ -46,10 +46,10 @@ class FileBubbleState extends State {
@override
Widget build(BuildContext context) {
- var fromMe = Provider.of(context, listen: false).senderHandle == Provider.of(context).onion;
+ var fromMe = Provider.of(context).senderHandle == Provider.of(context).onion;
var flagStarted = Provider.of(context).attributes["file-downloaded"] == "true";
var borderRadiousEh = 15.0;
- var showFileSharing = Provider.of(context, listen: false).isExperimentEnabled(FileSharingExperiment);
+ var showFileSharing = Provider.of(context).isExperimentEnabled(FileSharingExperiment);
DateTime messageDate = Provider.of(context).timestamp;
var metadata = Provider.of(context);