forked from cwtch.im/cwtch-ui
Merge pull request 'Import and Export Profile' (#397) from import_export into trunk
Reviewed-on: cwtch.im/cwtch-ui#397
This commit is contained in:
commit
d8e19de5b1
|
@ -1 +1 @@
|
|||
2022-03-10-17-32-v1.6.0-6-gc2874db
|
||||
2022-03-10-19-05-v1.6.0-9-g5b34715
|
|
@ -1 +1 @@
|
|||
2022-03-10-22-49-v1.6.0-6-gc2874db
|
||||
2022-03-11-00-06-v1.6.0-9-g5b34715
|
|
@ -46,7 +46,7 @@
|
|||
<!--Needed to run in background (lol)-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<!--Needed to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
|
||||
<queries>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
|
@ -28,3 +28,17 @@ void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Fu
|
|||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> 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<AppState>(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;
|
||||
}
|
||||
|
|
|
@ -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<dynamic> ImportProfile(String file, String pass);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ResetTor();
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
|
|||
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
|
||||
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef string_string_to_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
|
||||
typedef StringFromStringStringFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef string_int_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Int32 handle);
|
||||
typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, 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<NativeFunction<void_from_string_string_function>>("c_ExportProfile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ExportProfileFn = exportProfileC.asFunction<VoidFromStringStringFn>();
|
||||
ExportProfileFn(utf8profile, utf8profile.length, utf8file, utf8file.length);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(utf8file);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<String> ImportProfile(String file, String pass) async {
|
||||
final utf8pass = pass.toNativeUtf8();
|
||||
final utf8file = file.toNativeUtf8();
|
||||
var exportProfileC = library.lookup<NativeFunction<string_string_to_string_function>>("c_ImportProfile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ExportProfileFn = exportProfileC.asFunction<StringFromStringStringFn>();
|
||||
Pointer<Utf8> result = ExportProfileFn(utf8file, utf8file.length, utf8pass, utf8pass.length);
|
||||
String importResult = result.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(result);
|
||||
malloc.free(utf8pass);
|
||||
malloc.free(utf8file);
|
||||
return importResult;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<dynamic> ImportProfile(String file, String pass) {
|
||||
return cwtchPlatform.invokeMethod("ImportProfile", {"file": file, "pass": pass});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AddEditProfileView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: Provider.of<ProfileInfoState>(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<FlwtchState>(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<FlwtchState>(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<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
|
|
|
@ -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<ProfileMgrView> {
|
|||
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<ProfileMgrView> {
|
|||
));
|
||||
}
|
||||
|
||||
void _pushAddProfile({onion: ""}) {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
void _pushAddProfile(bcontext, {onion: ""}) {
|
||||
Navigator.popUntil(bcontext, (route) => route.isFirst);
|
||||
Navigator.of(bcontext).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
|
@ -174,6 +178,70 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
));
|
||||
}
|
||||
|
||||
void _modalAddImportProfiles() {
|
||||
showModalBottomSheet<void>(
|
||||
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: <Widget>[
|
||||
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<FlwtchState>(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<void>(
|
||||
context: context,
|
||||
|
|
|
@ -46,10 +46,10 @@ class FileBubbleState extends State<FileBubble> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var fromMe = Provider.of<MessageMetadata>(context, listen: false).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
|
||||
var borderRadiousEh = 15.0;
|
||||
var showFileSharing = Provider.of<Settings>(context, listen: false).isExperimentEnabled(FileSharingExperiment);
|
||||
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
|
||||
|
||||
var metadata = Provider.of<MessageMetadata>(context);
|
||||
|
|
Loading…
Reference in New Issue