From 6a60194ffba6e1bba9a47254d3061f796b483850 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 10 Nov 2024 17:27:27 +0800 Subject: [PATCH] support setting new download path on android --- android/app/src/main/AndroidManifest.xml | 3 + .../com/github/wgh136/venera/MainActivity.kt | 78 +++++++++++++++++++ lib/foundation/local.dart | 6 +- lib/pages/settings/app.dart | 26 +++++-- lib/pages/settings/settings_page.dart | 1 + lib/utils/io.dart | 7 ++ pubspec.lock | 48 ++++++++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 10 files changed, 167 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b2c3459..4319b96 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + + + Unit)? = null + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == pickDirectoryCode) { @@ -43,6 +54,11 @@ class MainActivity : FlutterActivity() { result.error("Failed to Copy Files", e.toString(), null) } }.start() + } else if (requestCode == storageRequestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + storagePermissionRequest?.invoke(Environment.isExternalStorageManager()) + } + storagePermissionRequest = null } } @@ -89,6 +105,13 @@ class MainActivity : FlutterActivity() { listening = false } }) + + val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage") + storageChannel.setMethodCallHandler { _, res -> + requestStoragePermission {result -> + res.success(result) + } + } } private fun getProxy(): String { @@ -145,6 +168,61 @@ class MainActivity : FlutterActivity() { } } } + + private fun requestStoragePermission(result: (Boolean) -> Unit) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + val readPermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + + val writePermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + + if (!readPermission || !writePermission) { + storagePermissionRequest = result + ActivityCompat.requestPermissions( + this, + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + storageRequestCode + ) + } else { + result(true) + } + } else { + if (!Environment.isExternalStorageManager()) { + try { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.addCategory("android.intent.category.DEFAULT") + intent.data = Uri.parse("package:" + context.packageName) + startActivityForResult(intent, storageRequestCode) + } catch (e: Exception) { + result(false) + } + } else { + result(true) + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if(requestCode == storageRequestCode) { + storagePermissionRequest?.invoke(grantResults.all { + it == PackageManager.PERMISSION_GRANTED + }) + storagePermissionRequest = null + } + } } class VolumeListen{ diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index a539644..c9469b9 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/reader/reader.dart'; import 'package:venera/utils/ext.dart'; @@ -158,12 +159,13 @@ class LocalManager with ChangeNotifier { return "Directory is not empty"; } try { - await copyDirectory( + await copyDirectoryIsolate( Directory(path), newDir, ); await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(path); - } catch (e) { + } catch (e, s) { + Log.error("IO", e, s); return e.toString(); } await Directory(path).deleteIgnoreError(recursive:true); diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index fae96fd..c491f51 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -32,12 +32,28 @@ class _AppSettingsState extends State { title: "Set New Storage Path".tl, actionTitle: "Set".tl, callback: () async { - var result; + String? result; if (App.isAndroid) { - context.showMessage(message: "Not supported".tl); - return; - } - else if (App.isIOS) { + var channel = const MethodChannel("venera/storage"); + var permission = await channel.invokeMethod(''); + if(permission != true) { + context.showMessage(message: "Permission denied".tl); + return; + } + var path = await selectDirectory(); + if(path != null) { + // check if the path is writable + var testFile = File(FilePath.join(path, "test")); + try { + await testFile.writeAsBytes([1]); + await testFile.delete(); + } catch (e) { + context.showMessage(message: "Permission denied".tl); + return; + } + result = path; + } + } else if (App.isIOS) { result = await selectDirectoryIOS(); } else { result = await selectDirectory(); diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 4066c61..ee6337e 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; diff --git a/lib/utils/io.dart b/lib/utils/io.dart index c714c69..5431f18 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:flutter/services.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; @@ -113,6 +114,12 @@ Future copyDirectory(Directory source, Directory destination) async { } } +Future copyDirectoryIsolate(Directory source, Directory destination) async { + await Isolate.run(() { + copyDirectory(source, destination); + }); +} + String findValidDirectoryName(String path, String directory) { var name = sanitizeFileName(directory); var dir = Directory("$path/$name"); diff --git a/pubspec.lock b/pubspec.lock index c1c24c8..4fd2dae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -593,6 +593,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + url: "https://pub.dev" + source: hosted + version: "12.0.13" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + url: "https://pub.dev" + source: hosted + version: "9.4.5" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + url: "https://pub.dev" + source: hosted + version: "0.1.3+2" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5822a0a..f102304 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: git: url: https://github.com/wgh136/webdav_client ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 079a7f9..11d2b2f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterQjsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterQjsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1f31462..e7ccd0e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows flutter_inappwebview_windows flutter_qjs + permission_handler_windows screen_retriever_windows share_plus sqlite3_flutter_libs