Browse Source

wip: filesharing ui dev

filesharing
erinn 4 weeks ago
parent
commit
cb3c161277
  1. 4
      android/app/build.gradle
  2. 8
      android/app/local.properties
  3. 9
      android/app/src/main/AndroidManifest.xml
  4. 37
      android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt
  5. 60
      android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt
  6. 2
      android/build.gradle
  7. 31
      lib/model.dart
  8. 2
      lib/views/globalsettingsview.dart
  9. 3
      lib/views/messageview.dart
  10. 28
      lib/widgets/filebubble.dart
  11. 2
      lib/widgets/messagerow.dart
  12. 29
      pubspec.lock
  13. 2
      pubspec.yaml

4
android/app/build.gradle

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 29
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -48,7 +48,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "im.cwtch.flwtch"
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

8
android/app/local.properties

@ -0,0 +1,8 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Fri Jul 02 15:08:54 PDT 2021
sdk.dir=/home/erinn/Android/Sdk

9
android/app/src/main/AndroidManifest.xml

@ -9,7 +9,8 @@
android:name="io.flutter.app.FlutterApplication"
android:label="Cwtch"
android:extractNativeLibs="true"
android:icon="@mipmap/knott">
android:icon="@mipmap/knott"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
@ -48,4 +49,10 @@
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
<!--Needed to download files-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.PERMISSIONS_STORAGE" />
</manifest>

37
android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt

@ -15,6 +15,10 @@ import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import android.net.Uri
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
@ -93,6 +97,24 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
}
} else if (evt.EventType == "FileDownloaded") {
Log.i("FlwtchWorker", "file downloaded!");
val data = JSONObject(evt.Data);
val tempFile = data.getString("TempFile");
if (tempFile != "") {
val filePath = data.getString("FilePath");
Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath);
val sourcePath = Paths.get(tempFile);
val targetUri = Uri.parse(filePath);
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
val bytesWritten = Files.copy(sourcePath, os);
Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
if (bytesWritten != 0L) {
os?.flush();
os?.close();
Files.delete(sourcePath);
}
}
}
Intent().also { intent ->
@ -157,6 +179,21 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val target = (a.get("target") as? String) ?: ""
Cwtch.sendInvitation(profile, handle, target)
}
"ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, handle, filepath)
}
"DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val filepath = (a.get("filepath") as? String) ?: ""
val manifestpath = (a.get("manifestpath") as? String) ?: ""
val filekey = (a.get("filekey") as? String) ?: ""
Log.i("FlwtchWorker::DownloadFile", "DownloadFile("+filepath+", "+manifestpath+")")
Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""

60
android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt

@ -12,17 +12,25 @@ import android.view.Window
import androidx.lifecycle.Observer
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.ErrorLogResult
import org.json.JSONObject
import java.util.concurrent.TimeUnit
import android.net.Uri
import android.provider.DocumentsContract
import android.content.ContentUris
import android.os.Build
import android.os.Environment
import android.database.Cursor
import android.provider.MediaStore
class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView()
@ -47,6 +55,11 @@ class MainActivity: FlutterActivity() {
private var notificationClickChannel: MethodChannel? = null
private var shutdownClickChannel: MethodChannel? = null
// "Download to..." prompt extra arguments
private var dlToProfile = ""
private var dlToHandle = ""
private var dlToFileKey = ""
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (notificationClickChannel == null || intent.extras == null) return
@ -68,6 +81,25 @@ class MainActivity: FlutterActivity() {
}
}
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
if (intent == null || intent!!.getData() == null) {
Log.i("MainActivity:onActivityResult", "user canceled activity");
return;
}
val filePath = intent!!.getData().toString();
val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString();
Log.i("onActivityResult", "got download path: " + filePath);
Log.i("onActivityResult", "got manifest path: " + manifestPath);
handleCwtch(MethodCall("DownloadFile", mapOf(
"ProfileOnion" to this.dlToProfile,
"handle" to this.dlToHandle,
"filepath" to filePath,
"manifestpath" to manifestPath,
"filekey" to this.dlToFileKey
)), ErrorLogResult(""));//placeholder; result is never actually invoked
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Note: this methods are invoked on the main thread.
@ -125,6 +157,18 @@ class MainActivity: FlutterActivity() {
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
return
} else if (call.method == "CreateDownloadableFile") {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, 1)
return
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
@ -178,20 +222,6 @@ class MainActivity: FlutterActivity() {
WorkManager.getInstance(this).pruneWork()
}
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
// for reference:
//
// class Response(json: String) : JSONObject(json) {
// val type: String? = this.optString("type")
// val data = this.optJSONArray("data")
// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
// }
//
// class Foo(json: String) : JSONObject(json) {
// val id = this.optInt("id")
// val title: String? = this.optString("title")
// }
class AppbusEvent(json: String) : JSONObject(json) {
val EventType = this.optString("EventType")
val EventID = this.optString("EventID")

2
android/build.gradle

@ -6,7 +6,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.5.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

31
lib/model.dart

@ -360,7 +360,7 @@ class ProfileInfoState extends ChangeNotifier {
}
void downloadInit(String fileKey, int numChunks) {
this._downloads[fileKey] = FileDownloadProgress(numChunks);
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
}
void downloadUpdate(String fileKey, int progress) {
@ -385,6 +385,7 @@ class ProfileInfoState extends ChangeNotifier {
if (!downloadActive(fileKey)) {
print("error: received download completion notice for unknown download "+fileKey);
} else {
this._downloads[fileKey]!.timeEnd = DateTime.now();
this._downloads[fileKey]!.complete = true;
notifyListeners();
}
@ -405,6 +406,18 @@ class ProfileInfoState extends ChangeNotifier {
double downloadProgress(String fileKey) {
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0;
}
String downloadSpeed(String fileKey) {
if (!downloadActive(fileKey)) {
return "0 B/s";
}
var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096;
var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds;
if (seconds == 0) {
return "0 B/s";
}
return prettyBytes((bytes / seconds).round()) + "/s";
}
}
class FileDownloadProgress {
@ -412,13 +425,27 @@ class FileDownloadProgress {
int chunksTotal = 1;
bool complete = false;
bool gotManifest = false;
DateTime? timeStart;
DateTime? timeEnd;
FileDownloadProgress(this.chunksTotal);
FileDownloadProgress(this.chunksTotal, this.timeStart);
double progress() {
return 1.0 * chunksDownloaded / chunksTotal;
}
}
String prettyBytes(int bytes) {
if (bytes > 1000000000) {
return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB";
} else if (bytes > 1000000) {
return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB";
} else if (bytes > 1000) {
return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB";
} else {
return bytes.toString() + " B";
}
}
enum ContactAuthorization { unknown, approved, blocked }
ContactAuthorization stringToContactAuthorization(String authStr) {

2
lib/views/globalsettingsview.dart

@ -189,7 +189,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.labelFileSharing, style: TextStyle(color: settings.current().mainTextColor())),
title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing),
value: settings.isExperimentEnabled(FileSharingExperiment),
onChanged: (bool value) {

3
lib/views/messageview.dart

@ -40,6 +40,9 @@ class _MessageViewState extends State<MessageView> {
@override
void initState() {
scrollListener.itemPositions.addListener(() {
if (scrollListener.itemPositions.value.length == 0) {
return;
}
var first = scrollListener.itemPositions.value.first.index;
var last = scrollListener.itemPositions.value.last.index;
// sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows]

28
lib/widgets/filebubble.dart

@ -63,9 +63,9 @@ class FileBubbleState extends State<FileBubble> {
var wdgMessage = !showFileSharing
? Text(AppLocalizations.of(context)!.messageEnableFileSharing)
: fromMe
? senderInviteChrome(
? senderFileChrome(
AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize)
: (inviteChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize));
: (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize, Provider.of<ProfileInfoState>(context).downloadSpeed(widget.fileKey())));
Widget wdgDecorations;
if (!showFileSharing) {
@ -147,9 +147,8 @@ class FileBubbleState extends State<FileBubble> {
file = File(selectedFileName);
print("saving to " + file.path);
var manifestPath = file.path + ".manifest";
setState(() {
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file!.path, manifestPath, widget.fileKey());
});
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey());
}
} catch (e) {
print(e);
@ -158,8 +157,8 @@ class FileBubbleState extends State<FileBubble> {
}
// Construct an invite chrome for the sender
Widget senderInviteChrome(String chrome, String fileName, String rootHash, int fileSize) {
return Wrap(children: [
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
return Wrap(direction: Axis.vertical,children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
@ -179,7 +178,7 @@ class FileBubbleState extends State<FileBubble> {
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
fileSize.toString() + 'B\u202F',
prettyBytes(fileSize) + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
@ -200,7 +199,7 @@ class FileBubbleState extends State<FileBubble> {
}
// Construct an invite chrome
Widget inviteChrome(String chrome, String fileName, String rootHash, int fileSize) {
Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) {
var prettyHash = rootHash;
if (rootHash.length == 128) {
prettyHash = rootHash.substring(0, 32) + '\n' +
@ -230,7 +229,7 @@ class FileBubbleState extends State<FileBubble> {
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
AppLocalizations.of(context)!.labelFilesize + ': ' + fileSize.toString() + 'B\u202F',
AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
@ -247,6 +246,15 @@ class FileBubbleState extends State<FileBubble> {
maxLines: 4,
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
speed + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
textAlign: TextAlign.left,
maxLines: 1,
textWidthBasis: TextWidthBasis.longestLine,
),
]);
}
}

2
lib/widgets/messagerow.dart

@ -163,7 +163,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// For desktop...
onHover: (event) {
setState(() {
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context).messageIndex;
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
});
},
onExit: (event) {

29
pubspec.lock

@ -21,7 +21,7 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.1"
version: "2.8.2"
boolean_selector:
dependency: transitive
description:
@ -106,6 +106,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
file_picker:
dependency: "direct main"
description:
name: file_picker
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
file_picker_desktop:
dependency: "direct main"
description:
name: file_picker_desktop
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -116,6 +130,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
flutter_test:
dependency: "direct main"
description: flutter
@ -174,7 +195,7 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
version: "0.12.11"
meta:
dependency: transitive
description:
@ -382,7 +403,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
version: "0.4.3"
typed_data:
dependency: transitive
description:
@ -427,4 +448,4 @@ packages:
version: "3.1.0"
sdks:
dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.0"
flutter: ">=2.0.0"

2
pubspec.yaml

@ -41,6 +41,8 @@ dependencies:
flutter_test:
sdk: flutter
scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.0.1
file_picker_desktop: ^1.1.0
dev_dependencies:
msix: ^2.1.3

Loading…
Cancel
Save