diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7ba90b..15ded1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: - run: flutter config --enable-linux-desktop - name: Running flutter tests - run: xvfb-run flutter test integration_test + run: xvfb-run flutter test integration_test/app_test.dart - name: Start linux build run: flutter build linux @@ -166,7 +166,7 @@ jobs: - run: flutter config --enable-windows-desktop - name: Running flutter tests - run: flutter test integration_test + run: flutter test integration_test\app_test.dart - name: Build for windows run: flutter build windows diff --git a/android/app/build.gradle b/android/app/build.gradle index ebdf5dc..80a1592 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -40,6 +40,9 @@ android { ndkVersion flutter.ndkVersion compileOptions { + //flutter_local_notifications + coreLibraryDesugaringEnabled true + //------ sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -93,4 +96,8 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:multidex:2.0.1' + //for flutter_local_notifications + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 116bc22..f113eb4 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1 +1,29 @@ --keep class androidx.lifecycle.DefaultLifecycleObserver \ No newline at end of file +-keep class androidx.lifecycle.DefaultLifecycleObserver + +## flutter_local_notification +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 72840b0..b76846b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,10 @@ android:requestLegacyExternalStorage="true"> + + + + []; var platformFiles = []; late Directory subdir; + //TODO: Refactor setUpAll(() async { final tempDir = await getTemporaryDirectory(); subdir = tempDir.createTempSync("sending"); @@ -43,30 +45,60 @@ void main() { name: path.basename(sendingFiles[index].path), path: sendingFiles[index].path)); }); - group('IO tests', () { - var downloadedFiles = []; - final recieve = Receiver( - saveToTemp: true, - useDb: false, - onAllFilesDownloaded: (files) => downloadedFiles = files); - testWidgets('Discover, send and receive files', (_) async { - final code = await recieve.listen(); - var allDevices = []; - while (allDevices.isEmpty) { - allDevices = await Discover.discover(); - } - final devices = allDevices.where((device) => device.code == code); - expect(devices, isNotEmpty, reason: "Expected to discover itself"); - await Sender.send(devices.first, platformFiles, useDb: false); - for (var i = 0; i < sendingFiles.length; i++) { - final gidenDosya = sendingFiles[i]; - final gelenDosya = File(downloadedFiles[i].path); - expect( - gidenDosya.readAsBytesSync(), equals(gelenDosya.readAsBytesSync()), - reason: "All sent files expected to has same content as originals"); - } + group("Database tests", () { + final db = DatabaseManager(); + setUp(() => db.clear()); + testWidgets("Downloaded file insert", (_) async { + final file = DbFile( + name: "test1", + path: "/.../../", + time: DateTime.now(), + fileStatus: DbFileStatus.download); + await db.insert(file); + final savedFiles = await db.files; + expect(savedFiles, equals([file])); }); + testWidgets("Uploaded file insert", (_) async { + final file = DbFile( + name: "test1", + path: "/.../../", + time: DateTime.now(), + fileStatus: DbFileStatus.upload); + await db.insert(file); + final savedFiles = await db.files; + expect(savedFiles, equals([file])); + }); + tearDown(() => db.close()); + }); + group('IO tests', () { + var downloadedFiles = []; + Receiver? receiver; + testWidgets( + 'Discover, send and receive files', + (_) async { + receiver = Receiver( + saveToTemp: true, + useDb: false, + onAllFilesDownloaded: (files) => downloadedFiles = files); + final code = await receiver!.listen(); + var allDevices = []; + while (allDevices.isEmpty) { + allDevices = await Discover.discover(); + } + final devices = allDevices.where((device) => device.code == code); + expect(devices, isNotEmpty, reason: "Expected to discover itself"); + await Sender().send(devices.first, platformFiles, useDb: false); + for (var i = 0; i < sendingFiles.length; i++) { + final gidenDosya = sendingFiles[i]; + final gelenDosya = File(downloadedFiles[i].path); + expect(gidenDosya.readAsBytesSync(), + equals(gelenDosya.readAsBytesSync()), + reason: + "All sent files expected to has same content as originals"); + } + }, + ); tearDown(() { for (var file in downloadedFiles) { @@ -75,7 +107,7 @@ void main() { downloadedFiles = []; }); tearDownAll(() async { - await recieve.stopListening(); + await receiver?.stopListening(); }); }); @@ -83,7 +115,7 @@ void main() { testWidgets("Handle no_receiver error", (_) async { final rand1 = Random().nextInt(30); final rand2 = Random().nextInt(30); - final sendFuture = Sender.send( + final sendFuture = Sender().send( Device(adress: "192.168.$rand1.$rand2", code: 1000, port: 2326), platformFiles, useDb: false); @@ -91,6 +123,7 @@ void main() { }, retry: 2); testWidgets("Handle connection lost while reciving", (_) async { FileDropException? throwedError; + final sender = Sender(); final code = await Receiver( onDownloadError: (error) => throwedError = error, useDb: false, @@ -101,8 +134,8 @@ void main() { devices = await Discover.discover(); } expect(devices.where((device) => device.code == code), isNotEmpty); - Future.delayed(const Duration(milliseconds: 500), Sender.cancel); - await Sender.send( + Future.delayed(const Duration(milliseconds: 500), sender.cancel); + await sender.send( devices.first, [ PlatformFile( diff --git a/integration_test/fake_path_provider.dart b/integration_test/fake_path_provider.dart index 2a5f737..a0f2b4e 100644 --- a/integration_test/fake_path_provider.dart +++ b/integration_test/fake_path_provider.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/integration_test/mobile_test.dart b/integration_test/mobile_test.dart new file mode 100644 index 0000000..c7b5009 --- /dev/null +++ b/integration_test/mobile_test.dart @@ -0,0 +1,81 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:weepy/classes/discover.dart'; +import 'package:weepy/classes/workers/worker_interface.dart'; +import 'package:weepy/models.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + var sendingFiles = []; + var platformFiles = []; + var downloadedFiles = []; + IsolatedReceiver? receiver; + late Directory subdir; + setUpAll(() async { + final tempDir = await getTemporaryDirectory(); + subdir = tempDir.createTempSync("sending"); + final testImageData = await rootBundle.load("assets/test_image.png"); + final testImageFile = File(path.join(subdir.path, "test_image.png")); + testImageFile.writeAsBytesSync(testImageData.buffer.asInt8List(), + mode: FileMode.writeOnly); + sendingFiles = [ + File(path.join(subdir.path, "deneme 1.txt")), + File(path.join(subdir.path, "deneme 2.txt")), + testImageFile + ]; + for (var gidenDosya in sendingFiles) { + if (path.extension(gidenDosya.path) == ".txt") { + gidenDosya.writeAsStringSync("deneme gövdesi", + mode: FileMode.writeOnly); + } + } + platformFiles = List.generate( + sendingFiles.length, + (index) => PlatformFile( + //readStream: sendingFiles[index].openRead(), + size: sendingFiles[index].lengthSync(), + name: path.basename(sendingFiles[index].path), + path: sendingFiles[index].path)); + }); + + final dbStatusVariant = ValueVariant({true, false}); + + testWidgets("Test IsolatedReceiver & IsolatedSender", (_) async { + receiver = IsolatedReceiver( + saveToTemp: true, + useDb: dbStatusVariant.currentValue!, + onAllFilesDownloaded: (files) => downloadedFiles = files, + onDownloadError: (error) => throw error, + progressNotification: true); + final code = await receiver!.listen(); + var allDevices = []; + while (allDevices.isEmpty) { + allDevices = await Discover.discover(); + } + final devices = allDevices.where((device) => device.code == code); + expect(devices, isNotEmpty, reason: "Expected to discover itself"); + await IsolatedSender(progressNotification: false).send( + devices.first, platformFiles, + useDb: dbStatusVariant.currentValue!); + for (var i = 0; i < sendingFiles.length; i++) { + final gidenDosya = sendingFiles[i]; + final gelenDosya = File(downloadedFiles[i].path); + expect(gidenDosya.readAsBytesSync(), equals(gelenDosya.readAsBytesSync()), + reason: "All sent files expected to has same content as originals"); + } + }, variant: dbStatusVariant); + tearDown(() { + for (var file in downloadedFiles) { + File(file.path).deleteSync(); + } + downloadedFiles = []; + }); + tearDownAll(() async { + await receiver?.stopListening(); + }); +} diff --git a/ios/Flutter/Generated.xcconfig b/ios/Flutter/Generated.xcconfig index 3bfc81f..b68a238 100644 --- a/ios/Flutter/Generated.xcconfig +++ b/ios/Flutter/Generated.xcconfig @@ -1,6 +1,6 @@ // This is a generated file; do not edit or check into version control. FLUTTER_ROOT=C:\flutter -FLUTTER_APPLICATION_PATH=C:\Flutter projects\filedrop +FLUTTER_APPLICATION_PATH=C:\Flutter_projects\filedrop COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_TARGET=lib\main.dart FLUTTER_BUILD_DIR=build diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 5e5d1ff..611f32f 100644 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=C:\flutter" -export "FLUTTER_APPLICATION_PATH=C:\Flutter projects\filedrop" +export "FLUTTER_APPLICATION_PATH=C:\Flutter_projects\filedrop" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib\main.dart" export "FLUTTER_BUILD_DIR=build" diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m index 776948b..cbaa9b9 100644 --- a/ios/Runner/GeneratedPluginRegistrant.m +++ b/ios/Runner/GeneratedPluginRegistrant.m @@ -30,6 +30,12 @@ @import firebase_remote_config; #endif +#if __has_include() +#import +#else +@import flutter_local_notifications; +#endif + #if __has_include() #import #else @@ -90,6 +96,12 @@ @import url_launcher_ios; #endif +#if __has_include() +#import +#else +@import workmanager; +#endif + @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { @@ -97,6 +109,7 @@ + (void)registerWithRegistry:(NSObject*)registry { [FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]]; [FLTFirebaseCrashlyticsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCrashlyticsPlugin"]]; [FLTFirebaseRemoteConfigPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseRemoteConfigPlugin"]]; + [FlutterLocalNotificationsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterLocalNotificationsPlugin"]]; [FlutterStoreListingPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterStoreListingPlugin"]]; [IntegrationTestPlugin registerWithRegistrar:[registry registrarForPlugin:@"IntegrationTestPlugin"]]; [OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]]; @@ -107,6 +120,7 @@ + (void)registerWithRegistry:(NSObject*)registry { [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; [Sqlite3FlutterLibsPlugin registerWithRegistrar:[registry registrarForPlugin:@"Sqlite3FlutterLibsPlugin"]]; [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; + [WorkmanagerPlugin registerWithRegistrar:[registry registrarForPlugin:@"WorkmanagerPlugin"]]; } @end diff --git a/lib/classes/database.dart b/lib/classes/database.dart index d30d5e1..45f89bd 100644 --- a/lib/classes/database.dart +++ b/lib/classes/database.dart @@ -1,26 +1,50 @@ import 'dart:io'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../models.dart'; class DatabaseManager { bool _initalised = false; - Future get _db { + Future get _db async { if (!_initalised && (Platform.isLinux || Platform.isWindows)) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfiNoIsolate; } + final db = await openDatabase("files.db", version: 2, + onCreate: (db, version) async { + await db.execute( + "create table downloaded (ID integer primary key autoincrement, name text not null, path text not null, type text, timeepoch int not null)"); + await db.execute( + "create table uploaded (ID integer primary key autoincrement, name text not null, path text not null, type text, timeepoch int not null)"); + }, onUpgrade: (db, oldVersion, newVersion) async { + if (oldVersion == 1 && newVersion == 2) { + try { + await db.execute( + "alter table downloaded rename column time to timeepoch"); + await db + .execute("alter table uploaded rename column time to timeepoch"); + } on Exception catch (e) { + //Some old android devices does not support 'alert table' + // + //Workaround: Dropping table then recreating table + //since database contains only file history that would not be a problem + await FirebaseCrashlytics.instance.recordError(e, null, + reason: + "Database version update $oldVersion to $newVersion failed."); + await db.execute("drop table IF EXISTS downloaded"); + await db.execute("drop table IF EXISTS uploaded"); + await db.execute( + "create table downloaded (ID integer primary key autoincrement, name text not null, path text not null, type text, timeepoch int not null)"); + await db.execute( + "create table uploaded (ID integer primary key autoincrement, name text not null, path text not null, type text, timeepoch int not null)"); + } + } else { + throw UnsupportedError("Unsupported db version"); + } + }); _initalised = true; - return openDatabase( - "files.db", - version: 1, - onCreate: (db, version) async { - await db.execute( - "create table downloaded (ID integer primary key autoincrement, name text not null, path text not null, type text, time int not null)"); - await db.execute( - "create table uploaded (ID integer primary key autoincrement, name text not null, path text not null, type text, time int not null)"); - }, - ); + return db; } ///Insert a uploaded or downloaded file information @@ -28,12 +52,18 @@ class DatabaseManager { final db = await _db; switch (file.fileStatus) { case DbFileStatus.upload: - await db.insert("uploaded", - {"name": file.name, "time": file.timeEpoch, "path": file.path}); + await db.insert("uploaded", { + "name": file.name, + "timeepoch": file.timeEpoch, + "path": file.path + }); break; case DbFileStatus.download: - await db.insert("downloaded", - {"name": file.name, "time": file.timeEpoch, "path": file.path}); + await db.insert("downloaded", { + "name": file.name, + "timeepoch": file.timeEpoch, + "path": file.path + }); break; } } @@ -58,12 +88,12 @@ class DatabaseManager { ///Get all downloaded/uploaded file information as list. Future> get files async { final db = await _db; - final uploadedMaps = - await db.query("uploaded", columns: ["name", "type", "time", "path"]); + final uploadedMaps = await db + .query("uploaded", columns: ["name", "type", "timeepoch", "path"]); final uploadedFiles = uploadedMaps.map((e) => DbFile.uploadedFromMap(e)).toList(); - final downloadedMaps = - await db.query("downloaded", columns: ["name", "type", "time", "path"]); + final downloadedMaps = await db + .query("downloaded", columns: ["name", "type", "timeepoch", "path"]); final downloadedFiles = downloadedMaps.map((e) => DbFile.downloadedFromMap(e)).toList(); List files = []; diff --git a/lib/classes/exceptions.dart b/lib/classes/exceptions.dart index cd2bbb4..6ebd1fc 100644 --- a/lib/classes/exceptions.dart +++ b/lib/classes/exceptions.dart @@ -29,8 +29,7 @@ class IpException implements FileDropException { /// ///Use [getErrorMessage] for localised error message. /// - ///Usually throws when don't connected to a network. - + ///Usually throws when didn't connected to a network. IpException(); @override String getErrorMessage(AppLocalizations appLocalizations) => diff --git a/lib/classes/notifications.dart b/lib/classes/notifications.dart new file mode 100644 index 0000000..a9a8c9d --- /dev/null +++ b/lib/classes/notifications.dart @@ -0,0 +1,67 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:num_remap/num_remap.dart'; + +final _localNotifications = FlutterLocalNotificationsPlugin(); + +enum Type { download, upload } + +///Create notification details for updating or creating progress notification +/// +///Make sure [progress] within range 0 to 100 +AndroidNotificationDetails _androidDetails(int progress, Type type) { + assert(progress.isWithinRange(0, 100)); + final id = switch (type) { + Type.download => "download_progress", + Type.upload => "upload_progress", + }; + final name = switch (type) { + Type.download => "Download progress for FileDrop", + Type.upload => "Upload progress for FileDrop", + }; + return AndroidNotificationDetails(id, name, + priority: Priority.low, + importance: Importance.low, + playSound: false, + enableVibration: false, + autoCancel: false, + showProgress: true, + maxProgress: 100, + progress: progress, + category: AndroidNotificationCategory.progress); +} + +///Show or update download progress notification. +/// +///Make sure [progress] within range 0 to 100 +Future showDownload(int progress) => _localNotifications.show( + Type.download.index, + null, + null, + NotificationDetails(android: _androidDetails(progress, Type.download))); + +///Show or update upload progress notification. +/// +///Make sure [progress] within range 0 to 100 +Future showUpload(int progress) => _localNotifications.show( + Type.upload.index, + null, + null, + NotificationDetails(android: _androidDetails(progress, Type.upload))); + +Future cancelDownload() async { + //_localNotifications.cancel(Type.download.index); +} + +Future cancelUpload() async { + //_localNotifications.cancel(Type.upload.index); +} + +///Inıtalise local notifications. +/// +///Call before using any method. +Future initalise() async { + final status = await _localNotifications.initialize( + const InitializationSettings( + android: AndroidInitializationSettings("@mipmap/ic_launcher"))); + return status ?? false; +} diff --git a/lib/classes/receiver.dart b/lib/classes/receiver.dart index ce90bf0..5005c50 100644 --- a/lib/classes/receiver.dart +++ b/lib/classes/receiver.dart @@ -22,9 +22,9 @@ import '../constants.dart'; ///Available methods are [listen] and [stopListening] class Receiver { final _files = []; - late MediaStore _ms; + late final _ms = MediaStore(); final _tempDir = getTemporaryDirectory(); - late int _code; + final int code; HttpServer? _server; bool _isBusy = false; @@ -37,8 +37,15 @@ class Receiver { final bool saveToTemp; ///If [downloadAnimC] is set, progress will be sent to it. + @Deprecated( + "Prefer downloadUpdatePercent() because it allows updating UI from isolates") final AnimationController? downloadAnimC; + ///[onDownloadUpdatePercent] will be called for each saved chunk in download operation. + /// + ///Use for animating progress. + final void Function(double percent)? onDownloadUpdatePercent; + ///[port] listened for incoming connections. Should not set except testing or ///other devices will require manual port setting. final int? port; @@ -55,41 +62,57 @@ class Receiver { ///[onDownloadError] will be called when error happened while saving file. /// ///When [onDownloadError] called, no other callback will be called, - ///no exception thrown and server will wait new connection. + ///no exception thrown and server will wait for new connection. /// ///See [FileDropException] final void Function(FileDropException error)? onDownloadError; ///Listen and receive files from other devices. /// - ///Set [downloadAnimC], [onDownloadStart], [onFileDownloaded], [onAllFilesDownloaded] for animating download progess. + ///Set [onDownloadUpdatePercent], [onDownloadStart], [onFileDownloaded], [onAllFilesDownloaded] for animating download progess. /// ///Call [listen] for start listening. - Receiver( - {this.downloadAnimC, - this.useDb = true, - this.saveToTemp = false, - this.port, - this.onDownloadStart, - this.onFileDownloaded, - this.onAllFilesDownloaded, - this.onDownloadError}); + Receiver({ + this.downloadAnimC, + this.onDownloadUpdatePercent, + this.useDb = true, + this.saveToTemp = false, + this.port, + this.onDownloadStart, + this.onFileDownloaded, + this.onAllFilesDownloaded, + this.onDownloadError, + int? code, + }) : code = code ?? Random().nextInt(8888) + 1111; + + ///Get storage permission for Android and IOS + /// + ///For other platforms always returns [true] + Future checkPermission() async { + //For Android we need storage permissions + if (Platform.isAndroid) { + MediaStore.appFolder = Constants.saveFolder; + final sdkVersion = await _ms.getPlatformSDKInt(); + //For android sdk 33+ get permission for MediaStore + final perm = sdkVersion >= 33 + ? (await _ms.requestForAccess(initialRelativePath: null)) != null + : (await Permission.storage.request()).isGranted; + return perm; + } else { + return true; + } + } ///Starts listening for discovery and recieving file(s). ///Handles one connection at once. If another device tires to match, ///sends `400 Bad request` as response /// ///Returns the code generated for discovery. Other devices should select this code for - ///connecting to this device + ///connecting to [Receiver] Future listen() async { - if (!saveToTemp && (Platform.isAndroid || Platform.isIOS)) { - //These platforms needs storage permissions (only tested on Android) - final perm = await Permission.storage.request(); - if (!perm.isGranted) throw NoStoragePermissionException(); - _ms = MediaStore(); + if (Platform.isAndroid) { MediaStore.appFolder = Constants.saveFolder; } - _code = Random().nextInt(8888) + 1111; _isBusy = false; for (var port = Constants.minPort; port <= Constants.maxPort; port++) { @@ -107,10 +130,10 @@ class Receiver { } } if (_server != null) { - log("Listening for new file with port: ${_server!.port}, code: $_code", + log("Listening for new file with port: ${_server!.port}, code: $code", name: "Receive"); } - return _code; + return code; } Future _requestMethod(Request request) async { @@ -121,10 +144,10 @@ class Receiver { } if (request.method == "GET") { //Response to discovery requests - log("Discovery request recieved, returned code $_code", + log("Discovery request recieved, returned code $code", name: "Receive server"); return Response.ok( - jsonEncode({"message": Constants.meeting, "code": _code})); + jsonEncode({"message": Constants.meeting, "code": code})); } else if (request.method == "POST") { //Reciving file log("Reciving file...", name: "Receive server"); @@ -157,9 +180,12 @@ class Receiver { } final totalLengh = request.contentLength!; final fileWriter = file.openWrite(); + var downloadPercent = 0.0; await for (var bytes in mime.timeout(const Duration(seconds: 10))) { fileWriter.add(bytes); - downloadAnimC?.value += bytes.length / totalLengh; + downloadPercent += bytes.length / totalLengh; + downloadAnimC?.value = downloadPercent; + onDownloadUpdatePercent?.call(downloadPercent); } await fileWriter.flush(); await fileWriter.close(); @@ -195,8 +221,8 @@ class Receiver { } log("Recived file(s) $_files", name: "Receive server"); return Response.ok(null); - } catch (_) { - log("Download error", name: "Receiver"); + } catch (e) { + log("Download error", name: "Receiver", error: e); onDownloadError?.call(ConnectionLostException()); return Response.badRequest(); } finally { @@ -229,4 +255,23 @@ class Receiver { /// ///Is is safe to call before [listen] or after [listen] . Future stopListening() async => await _server?.close(); + + Map get map => { + "useDb": useDb, + "saveToTemp": saveToTemp, + if (port != null) "port": port!, + "code": code, + }; + + Receiver.fromMap(Map map, + {this.onAllFilesDownloaded, + this.downloadAnimC, + this.onDownloadError, + this.onDownloadStart, + this.onFileDownloaded, + this.onDownloadUpdatePercent}) + : useDb = map["useDb"], + saveToTemp = map["saveToTemp"], + port = map["port"], + code = map["code"]; } diff --git a/lib/classes/sender.dart b/lib/classes/sender.dart index c2e8894..4bf030e 100644 --- a/lib/classes/sender.dart +++ b/lib/classes/sender.dart @@ -14,19 +14,18 @@ import 'package:http/http.dart' as http; /// ///Available methods are [filePick] and [send] class Sender { - static final _dio = Dio(); - static final _senderCancelToken = CancelToken(); + final _dio = Dio(); + final _senderCancelToken = CancelToken(); ///Pick files which are about to send. /// ///You should pass them to [send] method. - static Future?> filePick() async { + Future?> filePick() async { final result = await FilePicker.platform.pickFiles(allowMultiple: true); - return result?.files; } - static void cancel() { + void cancel() { log("Request cancelled", name: "Sender"); _senderCancelToken.cancel(); } @@ -35,14 +34,17 @@ class Sender { /// ///[files] will send to [device] /// - ///If [uploadAnimC] is set, progess will be sent to it. + ///If [uploadAnimC] or [onUploadProgress] is set, progess will be sent to it. /// ///If [useDb] is `true`, file informations will be saved to sqflite database. ///Must set to `false` for prevent database usage. /// ///Throws [OtherDeviceBusyException] if other device is busy. - static Future send(Device device, List files, - {AnimationController? uploadAnimC, bool useDb = true}) async { + Future send(Device device, Iterable files, + {@Deprecated("Prefer onUploadProgress() instead") + AnimationController? uploadAnimC, + bool useDb = true, + void Function(double percent)? onUploadProgress}) async { final multiPartFiles = await Future.wait(files.map((e) async { final readStream = e.readStream; if (readStream == null) { @@ -75,6 +77,7 @@ class Sender { assert(mappedValue <= Assets.uploadAnimEnd && mappedValue >= Assets.uploadAnimStart); uploadAnimC?.animateTo(mappedValue.toDouble()); + onUploadProgress?.call(newValue); })); uploadAnimC?.animateTo(1.0); } on DioException catch (e) { diff --git a/lib/classes/workers/worker_interface.dart b/lib/classes/workers/worker_interface.dart new file mode 100644 index 0000000..15a573c --- /dev/null +++ b/lib/classes/workers/worker_interface.dart @@ -0,0 +1,245 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart'; +import 'package:weepy/classes/exceptions.dart'; +import 'package:weepy/classes/sender.dart'; +import 'package:weepy/models.dart'; + +import '../notifications.dart' as notifications; +import 'worker_messages.dart' as messages; +import 'dart:isolate'; +import 'dart:ui'; +import 'package:workmanager/workmanager.dart'; + +import '../receiver.dart'; + +enum MyTasks { receive, send } + +final _workManager = Workmanager(); + +@pragma("vm:entry-point") +void _callBack() { + _workManager.executeTask((taskName, inputData) async { + try { + final task = + MyTasks.values.singleWhere((element) => element.name == taskName); + switch (task) { + case MyTasks.receive: + SendPort? getReceiverPort() => + IsolateNameServer.lookupPortByName(MyTasks.receive.name); + final exitBlock = Completer(); + final receiverMap = inputData!; + final receiver = + Receiver.fromMap(receiverMap, onDownloadUpdatePercent: (percent) { + final sendPort = getReceiverPort(); + sendPort?.send(messages.UpdatePercent(percent).map); + }, onDownloadError: (error) { + final sendPort = getReceiverPort(); + if (sendPort != null) { + sendPort.send(messages.FiledropError(error).map); + } else { + exitBlock.completeError(error); + } + }, onDownloadStart: () { + final sendPort = getReceiverPort(); + sendPort?.send(const messages.DownloadStarted().map); + }, onAllFilesDownloaded: (files) { + final sendPort = getReceiverPort(); + sendPort?.send(messages.AllFilesDownloaded(files).map); + exitBlock.complete(true); + }, onFileDownloaded: (file) { + final sendPort = getReceiverPort(); + sendPort?.send(messages.FileDownloaded(file).map); + }); + await receiver.listen(); + return exitBlock.future; + case MyTasks.send: + final senderMap = inputData!; + final fileDirs = []; + for (var a = 0; senderMap["file$a"] != null; a++) { + fileDirs.add(senderMap["file$a"]); + } + assert(fileDirs.isNotEmpty); + final files = fileDirs.map((e) => File(e)); + final platformFiles = files.map((e) => PlatformFile( + path: e.path, name: basename(e.path), size: e.lengthSync())); + final device = Device.fromMap(senderMap); + SendPort? getSenderPort() => + IsolateNameServer.lookupPortByName(MyTasks.send.name); + await Sender().send(device, platformFiles, + useDb: senderMap["useDb"], + onUploadProgress: (percent) => + getSenderPort()?.send(messages.UpdatePercent(percent).map)); + getSenderPort()?.send(const messages.Completed().map); + return true; + } + } on Exception { + rethrow; + } + }); +} + +class IsolatedSender extends Sender { + ///If [true] creates and manages progress notification. + /// + ///[progressNotification] will change to [false] if user denies permission + bool progressNotification; + IsolatedSender({this.progressNotification = true}); + + ///Run [Sender] from a worker. + /// + ///Call [initalize()] before. + @override + Future send(Device device, Iterable files, + {@Deprecated("It has no effect. Prefer onUploadProgress instead") + AnimationController? uploadAnimC, + bool useDb = true, + void Function(double percent)? onUploadProgress}) async { + await initalize(); + if (progressNotification) { + progressNotification = await notifications.initalise(); + } + final fileDirs = files.map((e) => e.path!); + final map = device.map; + for (var i = 0; i < fileDirs.length; i++) { + map["file$i"] = fileDirs.elementAt(i); + } + map["useDb"] = useDb; + final port = ReceivePort(); + IsolateNameServer.registerPortWithName(port.sendPort, MyTasks.send.name); + + final exitBlock = Completer(); + port.listen((data) async { + switch (messages.MessageType.values[data["type"]]) { + case messages.MessageType.updatePercent: + final message = messages.UpdatePercent.fromMap(data); + if (progressNotification) { + await notifications.showUpload((message.newPercent * 100).round()); + } + onUploadProgress?.call(message.newPercent); + break; + case messages.MessageType.completed: + final _ = messages.Completed.fromMap(data); + exitBlock.complete(); + default: + throw Error(); + } + }); + await _workManager.registerOneOffTask(MyTasks.send.name, MyTasks.send.name, + inputData: map); + if (progressNotification) { + await notifications.showUpload(0); + } + await exitBlock.future; + if (progressNotification) { + await notifications.cancelUpload(); + } + } + + @override + Future cancel() => _workManager.cancelByUniqueName(MyTasks.send.name); +} + +Future initalize() async { + await _workManager.initialize(_callBack, isInDebugMode: kDebugMode); +} + +class IsolatedReceiver extends Receiver { + ///If [true] creates and manages progress notification. + bool progressNotification; + + ///Runs [Receiver] from a worker. + /// + ///Call [initalize()] before. + IsolatedReceiver( + {super.onDownloadUpdatePercent, + super.useDb, + super.saveToTemp, + super.port, + super.onDownloadStart, + super.onFileDownloaded, + super.onAllFilesDownloaded, + super.onDownloadError, + super.code, + this.progressNotification = true}); + + ///Starts worker and runs [Receiver.listen] + /// + ///If necessary requests permission. Throws [NoStoragePermissionException] + ///if permission rejected by user. + @override + Future listen() async { + await initalize(); + if (progressNotification) { + progressNotification = await notifications.initalise(); + } + if (!saveToTemp) { + final permissionStatus = await super.checkPermission(); + if (!permissionStatus) { + throw NoStoragePermissionException(); + } + } + final port = ReceivePort(); + IsolateNameServer.registerPortWithName(port.sendPort, MyTasks.receive.name); + port.listen(_portCallback); + await _workManager.registerOneOffTask( + MyTasks.receive.name, MyTasks.receive.name, + inputData: super.map, existingWorkPolicy: ExistingWorkPolicy.keep); + return super.code; + } + + @override + Future stopListening() async { + if (progressNotification) { + await notifications.cancelDownload(); + } + await _workManager.cancelByUniqueName(MyTasks.receive.name); + } + + Future _portCallback(data) async { + try { + final type = messages.MessageType.values[data["type"]]; + switch (type) { + case messages.MessageType.updatePercent: + final message = messages.UpdatePercent.fromMap(data); + if (progressNotification) { + await notifications + .showDownload((message.newPercent * 100).round()); + } + super.onDownloadUpdatePercent?.call(message.newPercent); + break; + case messages.MessageType.filedropError: + final message = messages.FiledropError.fromMap(data); + if (progressNotification) { + await notifications.cancelDownload(); + } + super.onDownloadError?.call(message.exception); + break; + case messages.MessageType.fileDownloaded: + final message = messages.FileDownloaded.fromMap(data); + super.onFileDownloaded?.call(message.file); + break; + case messages.MessageType.allFilesDownloaded: + final message = messages.AllFilesDownloaded.fromMap(data); + if (progressNotification) { + await notifications.cancelDownload(); + } + super.onAllFilesDownloaded?.call(message.files.toList()); + break; + case messages.MessageType.downloadStarted: + final _ = messages.DownloadStarted.fromMap(data); + super.onDownloadStart?.call(); + default: + throw Error(); + } + } on Exception catch (e) { + log("Interface error", name: "IsolatedReceiver", error: e); + rethrow; + } + } +} diff --git a/lib/classes/workers/worker_messages.dart b/lib/classes/workers/worker_messages.dart new file mode 100644 index 0000000..9870319 --- /dev/null +++ b/lib/classes/workers/worker_messages.dart @@ -0,0 +1,134 @@ +import 'package:weepy/classes/exceptions.dart'; +import 'package:weepy/models.dart'; +import 'worker_interface.dart'; + +enum MessageType { + updatePercent, + filedropError, + fileDownloaded, + allFilesDownloaded, + downloadStarted, + completed +} + +///Message which should send between [IsolatedSender] and main isolate. +abstract class SenderMessage { + final MessageType type; + + SenderMessage(this.type) { + assert(MessageType.values[map["type"]] == type); + } + Map get map; +} + +///Message which should send between [IsolatedReceiver] and main isolate. +abstract class ReceiverMessage { + final MessageType type; + ReceiverMessage(this.type) { + assert(MessageType.values[map["type"]] == type); + } + Map get map; +} + +///Sends when a progress updated. +/// +///Read new percent from [newPercent] +class UpdatePercent implements SenderMessage, ReceiverMessage { + ///The new percent of progress. + /// + ///It maybe same previous sent [newPercent] and it must between `0.00` and `1.00` + final double newPercent; + const UpdatePercent(this.newPercent); + + @override + Map get map => + {"type": type.index, "newPercent": newPercent}; + + @override + final type = MessageType.updatePercent; + + UpdatePercent.fromMap(Map map) + : newPercent = map["newPercent"] { + assert(MessageType.values[map["type"]] == type); + } +} + +///Sends when an error caused from worker. +/// +///Read error details from [exception.getErrorMessage(appLocalizations)] +class FiledropError implements SenderMessage, ReceiverMessage { + final FileDropException exception; + const FiledropError(this.exception); + + @override + Map get map => + {"type": type.index, "exceptionType": exception.runtimeType}; + + @override + final type = MessageType.filedropError; + + FiledropError.fromMap(Map map) + : exception = map["exceptionType"] { + assert(MessageType.values[map["type"]] == type); + } +} + +class FileDownloaded implements ReceiverMessage { + final DbFile file; + const FileDownloaded(this.file); + + @override + Map get map => {"type": type.index, "file": file.map}; + + @override + final type = MessageType.fileDownloaded; + + FileDownloaded.fromMap(Map map) + : file = DbFile.fromMap(map["file"]) { + assert(MessageType.values[map["type"]] == type); + } +} + +class AllFilesDownloaded implements ReceiverMessage { + final List files; + const AllFilesDownloaded(this.files); + @override + Map get map => + {"type": type.index, "files": files.map((e) => e.map).toList()}; + + @override + final type = MessageType.allFilesDownloaded; + + AllFilesDownloaded.fromMap(Map map) + : files = + (map["files"] as Iterable).map((e) => DbFile.fromMap(e)).toList() { + assert(MessageType.values[map["type"]] == type); + } +} + +class DownloadStarted implements ReceiverMessage { + const DownloadStarted(); + @override + Map get map => {"type": type.index}; + + @override + final type = MessageType.downloadStarted; + + DownloadStarted.fromMap(Map map) { + assert(MessageType.values[map["type"]] == type); + } +} + +///Sent when [IsolatedSender] completed uploading. +class Completed implements SenderMessage { + const Completed(); + @override + Map get map => {"type": type.index}; + + @override + MessageType get type => MessageType.completed; + + Completed.fromMap(Map map) { + assert(MessageType.values[map["type"]] == type); + } +} diff --git a/lib/models.dart b/lib/models.dart index f22a2d3..8e65fdf 100644 --- a/lib/models.dart +++ b/lib/models.dart @@ -37,14 +37,20 @@ class DbFile { DbFile.uploadedFromMap(Map map) : name = map["name"], fileStatus = DbFileStatus.upload, - time = DateTime.fromMillisecondsSinceEpoch(map["time"]), + time = DateTime.fromMillisecondsSinceEpoch(map["timeepoch"]), path = map["path"]; ///Use this constructor for load an downloaded file infos from database. DbFile.downloadedFromMap(Map map) : name = map["name"], fileStatus = DbFileStatus.download, - time = DateTime.fromMillisecondsSinceEpoch(map["time"]), + time = DateTime.fromMillisecondsSinceEpoch(map["timeepoch"]), + path = map["path"]; + + DbFile.fromMap(Map map) + : name = map["name"], + fileStatus = DbFileStatus.values[map["fileStatus"]], + time = DateTime.fromMillisecondsSinceEpoch(map["timeepoch"]), path = map["path"]; ///Icon for showing in UI. @@ -71,6 +77,25 @@ class DbFile { @override String toString() => "dbFile{name: $name, time: $time, fileStatus: ${fileStatus.name}}"; + + Map get map => { + "name": name, + "path": path, + "timeepoch": timeEpoch, + "fileStatus": fileStatus.index + }; + @override + int get hashCode => + path.hashCode ^ timeEpoch.hashCode ^ fileStatus.index.hashCode; + + @override + bool operator ==(Object other) { + if (other is DbFile) { + return other.hashCode == hashCode; + } else { + return false; + } + } } class Device { @@ -105,6 +130,14 @@ class Device { } } + Map get map => + {"adress": adress, "code": code, "port": port}; + + Device.fromMap(Map map) + : adress = map["adress"], + code = map["code"], + port = map["port"]; + ///deviceModel{Adress: [adress], Code: [code], Port: [port]} @override String toString() => diff --git a/lib/screens/receive_page.dart b/lib/screens/receive_page.dart index 3e73ac8..29938e0 100644 --- a/lib/screens/receive_page.dart +++ b/lib/screens/receive_page.dart @@ -1,10 +1,11 @@ +import 'package:flutter/foundation.dart'; +import 'package:weepy/classes/receiver.dart'; import 'package:weepy/files_riverpod.dart'; import 'package:weepy/models.dart'; - import '../classes/exceptions.dart'; +import '../classes/workers/worker_interface.dart'; import '../constants.dart'; import 'package:flutter/material.dart'; -import '../classes/receiver.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -31,58 +32,73 @@ class ReceivePageInner extends ConsumerStatefulWidget { } class _ReceivePageInnerState extends ConsumerState - with TickerProviderStateMixin { + with SingleTickerProviderStateMixin { late AnimationController _downloadAnimC; - late Receiver _receiveClass; - late int _code; + late final _receiver = defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS + ? IsolatedReceiver( + onDownloadStart: () => uiStatus = _UiState.downloading, + onAllFilesDownloaded: (files) async { + await ref.read(filesProvider.notifier).addFiles(files); + _files = files; + uiStatus = _UiState.complete; + }, + onDownloadError: (e) { + errorMessage = e.getErrorMessage(AppLocalizations.of(context)!); + uiStatus = _UiState.error; + }, + onDownloadUpdatePercent: (percent) { + _downloadAnimC.value = percent; + }) + : Receiver( + onDownloadStart: () => uiStatus = _UiState.downloading, + onAllFilesDownloaded: (files) async { + await ref.read(filesProvider.notifier).addFiles(files); + _files = files; + uiStatus = _UiState.complete; + }, + onDownloadError: (e) { + errorMessage = e.getErrorMessage(AppLocalizations.of(context)!); + uiStatus = _UiState.error; + }, + onDownloadUpdatePercent: (percent) { + _downloadAnimC.value = percent; + }); late List _files; late String errorMessage; + late int _code; ///Use [uiStatus] setter for updating state without [setState] var _uiStatus = _UiState.loading; - ///Setter for ui state. - /// - ///Don't need warp with [setState]. - set uiStatus(_UiState uiStatus) => setState(() => _uiStatus = uiStatus); @override - initState() { + void initState() { _downloadAnimC = AnimationController(vsync: this) ..addListener(() { setState(() {}); }); - _receiveClass = Receiver( - downloadAnimC: _downloadAnimC, - onDownloadStart: () => uiStatus = _UiState.downloading, - onAllFilesDownloaded: (files) async { - await ref.read(filesProvider.notifier).addFiles(files); - _files = files; - uiStatus = _UiState.complete; - }, - onDownloadError: (e) { - errorMessage = e.getErrorMessage(AppLocalizations.of(context)!); - uiStatus = _UiState.error; - }); - _receive(); super.initState(); + _receive(); } + ///Setter for ui state. + /// + ///Don't need warp with [setState]. + set uiStatus(_UiState uiStatus) => setState(() => _uiStatus = uiStatus); + Future _receive() async { - try { - _code = await _receiveClass.listen(); - uiStatus = _UiState.listening; - } on FileDropException catch (err) { - if (context.mounted) { - errorMessage = err.getErrorMessage(AppLocalizations.of(context)!); - uiStatus = _UiState.error; - } + final permissionStatus = await _receiver.checkPermission(); + if (!permissionStatus) { + throw NoStoragePermissionException(); } + _code = await _receiver.listen(); + uiStatus = _UiState.listening; } @override void dispose() { _downloadAnimC.dispose(); - _receiveClass.stopListening(); + _receiver.stopListening(); super.dispose(); } diff --git a/lib/screens/send_page.dart b/lib/screens/send_page.dart index e2362db..7508736 100644 --- a/lib/screens/send_page.dart +++ b/lib/screens/send_page.dart @@ -1,9 +1,12 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; +import 'package:num_remap/num_remap.dart'; import 'package:weepy/classes/exceptions.dart'; import 'package:weepy/files_riverpod.dart'; import '../classes/discover.dart' as discover_class; //for prevent collusion import '../classes/sender.dart'; +import '../classes/workers/worker_interface.dart'; import '../constants.dart'; import '../models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -31,6 +34,10 @@ class SendPageInner extends ConsumerStatefulWidget { class _SendPageInnerState extends ConsumerState with TickerProviderStateMixin { + final _sender = defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS + ? IsolatedSender() + : Sender(); var _uiState = _UiState.scanning; List _ipList = []; late AnimationController _uploadAnimC; @@ -76,7 +83,7 @@ class _SendPageInnerState extends ConsumerState @override void dispose() { _uploadAnimC.dispose(); - Sender.cancel(); + _sender.cancel(); super.dispose(); } @@ -126,7 +133,7 @@ class _SendPageInnerState extends ConsumerState title: Text(device.code.toString()), leading: const Icon(Icons.phone_android), onTap: () { - _send(device, _uploadAnimC); + _send(device); }, ); }); @@ -136,12 +143,19 @@ class _SendPageInnerState extends ConsumerState } } - Future _send(Device device, AnimationController uploadAnimC) async { - final file = await Sender.filePick(); + Future _send(Device device) async { + final file = await _sender.filePick(); if (file != null) { uiState = _UiState.sending; try { - await Sender.send(device, file, uploadAnimC: uploadAnimC); + _uploadAnimC.animateTo(Assets.uploadAnimStart); + await _sender.send(device, file, onUploadProgress: (percent) { + final mappedValue = percent.remapAndClamp( + 0.0, 1.0, Assets.uploadAnimStart, Assets.uploadAnimEnd); + assert(mappedValue <= Assets.uploadAnimEnd && + mappedValue >= Assets.uploadAnimStart); + _uploadAnimC.animateTo(mappedValue.toDouble()); + }); final filesNotifier = ref.read(filesProvider.notifier); await filesNotifier.addFiles(file .map((e) => DbFile( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8ccd917..234dd5c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import firebase_core import firebase_crashlytics import firebase_remote_config +import flutter_local_notifications import package_info_plus import path_provider_foundation import shared_preferences_foundation @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FLTFirebaseRemoteConfigPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseRemoteConfigPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index f4d305a..7eb67b9 100644 --- a/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -1,6 +1,6 @@ // This is a generated file; do not edit or check into version control. FLUTTER_ROOT=C:\flutter -FLUTTER_APPLICATION_PATH=C:\Flutter projects\filedrop +FLUTTER_APPLICATION_PATH=C:\Flutter_projects\filedrop COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build FLUTTER_BUILD_NAME=1.0.0 diff --git a/macos/Flutter/ephemeral/flutter_export_environment.sh b/macos/Flutter/ephemeral/flutter_export_environment.sh index cfa8620..8f9269d 100644 --- a/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -1,7 +1,7 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=C:\flutter" -export "FLUTTER_APPLICATION_PATH=C:\Flutter projects\filedrop" +export "FLUTTER_APPLICATION_PATH=C:\Flutter_projects\filedrop" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=1.0.0" diff --git a/pubspec.lock b/pubspec.lock index 778044b..5bf7aa2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: dd68ecea9f1e3556d385521bd21c7bafd6311a8c1e11abe2595ca27974f468ee + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 url: "https://pub.dev" source: hosted - version: "1.3.13" + version: "1.3.16" analyzer: dependency: transitive description: @@ -177,14 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" dio: dependency: "direct main" description: name: dio - sha256: "01870acd87986f768e0c09cc4d7a19a59d814af7b34cbeb0b437d2c33bdfea4c" + sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" url: "https://pub.dev" source: hosted - version: "5.3.4" + version: "5.4.0" fake_async: dependency: transitive description: @@ -221,10 +229,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "471b46ea6a9af503184d4de691566887daedd312aec5baac5baa42d819f56446" + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" url: "https://pub.dev" source: hosted - version: "2.23.0" + version: "2.24.2" firebase_core_platform_interface: dependency: transitive description: @@ -237,50 +245,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "0631a2ec971dbc540275e2fa00c3a8a2676f0a7adbc3c197d6fba569db689d97" + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.10.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "27f78b1fdad2a7f557abea17c3e0ba882bd0430ddffb7844634d41e51422e43e" + sha256: "5ccdf05de039f9544d0ba41c5ae2052ca2425985d32229911b09f69981164518" url: "https://pub.dev" source: hosted - version: "3.4.5" + version: "3.4.8" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "48b6cfb3e2fe3955ce1dfe16a0cceacb7d293277fda77eb47c058bfff94268e0" + sha256: "359197344def001589c84f8d1d57c05f6e2e773f559205610ce58c25e2045a57" url: "https://pub.dev" source: hosted - version: "3.6.13" + version: "3.6.16" firebase_remote_config: dependency: "direct main" description: name: firebase_remote_config - sha256: "95f9d38083ea2fc2732ff2e71f330bd38d7ba0858227b7ec3dbe5b08857d6ea1" + sha256: "60fc92273d1db338a6fad1839c42dedc4ad64f812043acad0cbb200702f5c9ce" url: "https://pub.dev" source: hosted - version: "4.3.5" + version: "4.3.8" firebase_remote_config_platform_interface: dependency: transitive description: name: firebase_remote_config_platform_interface - sha256: "9095b69c6a4ea6551f9d70fd559b79026dc074395c266ddc2b898cfa37b91785" + sha256: "41813ef8dfbc40ef7a59a73f9e5acef2608dbcb2933241b6c03d52e90677040f" url: "https://pub.dev" source: hosted - version: "1.4.13" + version: "1.4.16" firebase_remote_config_web: dependency: transitive description: name: firebase_remote_config_web - sha256: "8a32955ab675d1abf54f075cb222ffc04b6ae00e38cba9d83b241619d202481c" + sha256: "089e92f333c2fb2c05c640c80fecea9d1e06dada0ba85efe34a580987ef94a0a" url: "https://pub.dev" source: hosted - version: "1.4.13" + version: "1.4.16" flutter: dependency: "direct main" description: flutter @@ -299,6 +307,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e + url: "https://pub.dev" + source: hosted + version: "16.2.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + url: "https://pub.dev" + source: hosted + version: "7.0.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -316,10 +348,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: d261b0f2461e0595b96f92ed807841eb72cea84a6b12b8fd0c76e5ed803e7921 + sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" flutter_store_listing: dependency: "direct main" description: @@ -371,10 +403,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_parser: dependency: "direct main" description: @@ -616,10 +648,10 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: d96ff56a757b7f04fa825c469d296c5aebc55f743e87bd639fef91a466a24da8 + sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" url: "https://pub.dev" source: hosted - version: "0.1.0+1" + version: "0.1.0+2" permission_handler_platform_interface: dependency: transitive description: @@ -636,6 +668,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -688,10 +728,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "08451ddbaad6eae73e2422d8109775885623340d721c6637b8719c9f4b478848" + sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" riverpod_analyzer_utils: dependency: transitive description: @@ -704,10 +744,10 @@ packages: dependency: "direct dev" description: name: riverpod_lint - sha256: "6fc64ae102ba39b0889b7aa7f4ef6c5a8f71a2ad215b90c787f319a9407a128b" + sha256: "944929ef82c9bfeaa455ccab97920abcf847a0ffed5c9f6babc520a95db25176" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" rxdart: dependency: transitive description: @@ -829,18 +869,18 @@ packages: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "35d2fce1e971707c227cc4775cc017d5eafe06c2654c3435ebd5c3ad6c170f5f" + sha256: "873677ee78738a723d1ded4ccb23980581998d873d30ee9c331f6a81748663ff" url: "https://pub.dev" source: hosted - version: "2.3.0+4" + version: "2.3.1" sqlite3: dependency: transitive description: name: sqlite3 - sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb + sha256: "8922805564b78eb7aa9386c10056d377a541ac7270dc6a1589176277ebb4d15d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" sqlite3_flutter_libs: dependency: "direct main" description: @@ -921,6 +961,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" + source: hosted + version: "0.9.2" typed_data: dependency: transitive description: @@ -933,10 +981,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.2" url_launcher_android: dependency: transitive description: @@ -981,10 +1029,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" + sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" url_launcher_windows: dependency: transitive description: @@ -1045,10 +1093,18 @@ packages: dependency: transitive description: name: win32 - sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "0.5.2" xdg_directories: dependency: transitive description: @@ -1057,6 +1113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e53e4fb..56e024a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.19.6 <3.0.0' + sdk: '>=3.2.0 <4.0.0' dependencies: network_discovery: ^1.0.0 @@ -39,6 +39,8 @@ dependencies: flutter_riverpod: ^2.4.0 cupertino_icons: ^1.0.6 num_remap: ^1.0.1 + workmanager: ^0.5.2 + flutter_local_notifications: ^16.2.0 dev_dependencies: flutter_test: diff --git a/test/unit_test.dart b/test/unit_test.dart new file mode 100644 index 0000000..088a8cf --- /dev/null +++ b/test/unit_test.dart @@ -0,0 +1,23 @@ +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Nesne test', () { + final sayi = Random().nextInt(1000); + expect(_ParentNesne(sayi).getSayi(), sayi); + }); +} + +class _ChildNesne { + final int code; + _ChildNesne([int? code]) : code = code ?? Random().nextInt(1000); +} + +class _ParentNesne extends _ChildNesne { + int getSayi() { + return super.code; + } + + _ParentNesne([super.randomSayi]); +} diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index bb6a79f..03d00f6 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,3 +1 @@ -Fixed send/receive never stop even user cancelled. -Add error messages if connection lost while send/receive. -Add new upload animation \ No newline at end of file +Now send & receive progress shown as notification and contiunes progress from background (beta) \ No newline at end of file diff --git a/whatsnew/whatsnew-tr-TR b/whatsnew/whatsnew-tr-TR index a79385e..5913760 100644 --- a/whatsnew/whatsnew-tr-TR +++ b/whatsnew/whatsnew-tr-TR @@ -1,3 +1 @@ -Gönderme/alma işleminin kullanıcı iptal etse bile durmaması sorunu düzeltildi. -Gönderme/alma yarıda kesilmesi durumu için hata mesajı eklendi. -Yeni gönderme animasyonu eklendi. \ No newline at end of file +Artık gönderme & alma işlemi bildirim olarak gösteriliyor ve arkaplanda işleme devam ediyor (beta) \ No newline at end of file