mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
fix(android): save files outside Download folder (#1548)
This commit is contained in:
@@ -10,7 +10,8 @@ import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
private const val CHANNEL = "org.localsend.localsend_app/localsend"
|
||||
private const val REQUEST_CODE_PICK_DIRECTORY = 1
|
||||
private const val REQUEST_CODE_PICK_FILE = 2
|
||||
private const val REQUEST_CODE_PICK_DIRECTORY_PATH = 2
|
||||
private const val REQUEST_CODE_PICK_FILE = 3
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var pendingResult: MethodChannel.Result? = null
|
||||
@@ -24,21 +25,25 @@ class MainActivity : FlutterActivity() {
|
||||
when (call.method) {
|
||||
"pickDirectory" -> {
|
||||
pendingResult = result
|
||||
openDirectoryPicker()
|
||||
openDirectoryPicker(onlyPath = false)
|
||||
}
|
||||
"pickFiles" -> {
|
||||
pendingResult = result
|
||||
openFilePicker()
|
||||
}
|
||||
"pickDirectoryPath" -> {
|
||||
pendingResult = result
|
||||
openDirectoryPicker(onlyPath = true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDirectoryPicker() {
|
||||
private fun openDirectoryPicker(onlyPath: Boolean) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY)
|
||||
startActivityForResult(intent, if (onlyPath) REQUEST_CODE_PICK_DIRECTORY_PATH else REQUEST_CODE_PICK_DIRECTORY)
|
||||
}
|
||||
|
||||
private fun openFilePicker() {
|
||||
@@ -86,6 +91,19 @@ class MainActivity : FlutterActivity() {
|
||||
pendingResult = null
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_PICK_DIRECTORY_PATH -> {
|
||||
val uri: Uri? = data.data
|
||||
val takeFlags: Int =
|
||||
data.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
if (uri != null) {
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
pendingResult?.success(uri.toString())
|
||||
pendingResult = null
|
||||
} else {
|
||||
pendingResult?.error("Error", "Failed to access directory", null)
|
||||
pendingResult = null
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_PICK_FILE -> {
|
||||
val uriList: List<Uri> = when {
|
||||
data.clipData != null -> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## 1.15.2 (unreleased)
|
||||
|
||||
- fix: memory leak when receiving files, properly receive files that exceed available RAM (@Tienisto)
|
||||
- fix(android): save files outside Download folder (@Tienisto)
|
||||
- fix(windows): make installer work on arm64 (@Tienisto)
|
||||
|
||||
## 1.15.1 (2024-07-18)
|
||||
|
||||
@@ -10,10 +10,12 @@ import 'package:localsend_app/pages/changelog_page.dart';
|
||||
import 'package:localsend_app/pages/donation/donation_page.dart';
|
||||
import 'package:localsend_app/pages/language_page.dart';
|
||||
import 'package:localsend_app/pages/tabs/settings_tab_controller.dart';
|
||||
import 'package:localsend_app/provider/device_info_provider.dart';
|
||||
import 'package:localsend_app/provider/settings_provider.dart';
|
||||
import 'package:localsend_app/provider/version_provider.dart';
|
||||
import 'package:localsend_app/theme.dart';
|
||||
import 'package:localsend_app/util/device_type_ext.dart';
|
||||
import 'package:localsend_app/util/native/file_picker_android.dart';
|
||||
import 'package:localsend_app/util/native/pick_directory_path.dart';
|
||||
import 'package:localsend_app/util/native/platform_check.dart';
|
||||
import 'package:localsend_app/widget/custom_dropdown_button.dart';
|
||||
@@ -191,7 +193,13 @@ class SettingsTab extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
final directory = await pickDirectoryPath();
|
||||
final String? directory;
|
||||
if (defaultTargetPlatform == TargetPlatform.android &&
|
||||
(ref.read(deviceRawInfoProvider).androidSdkInt ?? 0) >= contentUriMinSdk) {
|
||||
directory = await pickDirectoryPathAndroid();
|
||||
} else {
|
||||
directory = await pickDirectoryPath();
|
||||
}
|
||||
if (directory != null) {
|
||||
await ref.notifier(settingsProvider).setDestination(directory);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:localsend_app/util/file_path_helper.dart';
|
||||
import 'package:localsend_app/util/native/cache_helper.dart';
|
||||
import 'package:localsend_app/util/native/content_uri_helper.dart';
|
||||
import 'package:localsend_app/util/native/cross_file_converters.dart';
|
||||
import 'package:localsend_app/util/native/pick_directory.dart';
|
||||
import 'package:localsend_app/util/native/file_picker_android.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:refena_flutter/refena_flutter.dart';
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:localsend_app/model/cross_file.dart';
|
||||
import 'package:localsend_app/util/file_path_helper.dart';
|
||||
import 'package:localsend_app/util/native/pick_directory.dart';
|
||||
import 'package:localsend_app/util/native/file_picker_android.dart';
|
||||
import 'package:share_handler/share_handler.dart';
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:localsend_app/provider/selection/selected_sending_files_provider
|
||||
import 'package:localsend_app/theme.dart';
|
||||
import 'package:localsend_app/util/determine_image_type.dart';
|
||||
import 'package:localsend_app/util/native/cross_file_converters.dart';
|
||||
import 'package:localsend_app/util/native/pick_directory.dart';
|
||||
import 'package:localsend_app/util/native/file_picker_android.dart';
|
||||
import 'package:localsend_app/util/native/pick_directory_path.dart';
|
||||
import 'package:localsend_app/util/native/platform_check.dart';
|
||||
import 'package:localsend_app/util/sleep.dart';
|
||||
@@ -199,7 +199,7 @@ Future<void> _pickFolder(BuildContext context, Ref ref) async {
|
||||
);
|
||||
await sleepAsync(200); // Wait for the dialog to be shown
|
||||
try {
|
||||
if (defaultTargetPlatform == TargetPlatform.android && (ref.read(deviceRawInfoProvider).androidSdkInt ?? 0) >= 28) {
|
||||
if (defaultTargetPlatform == TargetPlatform.android && (ref.read(deviceRawInfoProvider).androidSdkInt ?? 0) >= contentUriMinSdk) {
|
||||
// Android 8 and above have more predictable content URIs that we can parse.
|
||||
final result = await pickDirectoryAndroid();
|
||||
if (result != null) {
|
||||
|
||||
+10
-1
@@ -1,10 +1,14 @@
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
part 'pick_directory.mapper.dart';
|
||||
part 'file_picker_android.mapper.dart';
|
||||
|
||||
const _methodChannel = MethodChannel('org.localsend.localsend_app/localsend');
|
||||
|
||||
/// From Android 9 (Pie) and above, we need to use the Storage Access Framework (SAF) to access files.
|
||||
/// Older versions might also work but the encoded content URI is not guaranteed to work with our algorithm.
|
||||
const contentUriMinSdk = 28;
|
||||
|
||||
Future<PickDirectoryResult?> pickDirectoryAndroid() async {
|
||||
final result = await _methodChannel.invokeMethod<Map>('pickDirectory');
|
||||
if (result == null) {
|
||||
@@ -17,6 +21,11 @@ Future<PickDirectoryResult?> pickDirectoryAndroid() async {
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> pickDirectoryPathAndroid() async {
|
||||
final result = await _methodChannel.invokeMethod<String>('pickDirectoryPath');
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<FileInfo>?> pickFilesAndroid() async {
|
||||
final result = await _methodChannel.invokeMethod<List>('pickFiles');
|
||||
if (result == null) {
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
|
||||
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
|
||||
|
||||
part of 'pick_directory.dart';
|
||||
part of 'file_picker_android.dart';
|
||||
|
||||
class PickDirectoryResultMapper extends ClassMapperBase<PickDirectoryResult> {
|
||||
PickDirectoryResultMapper._();
|
||||
@@ -7,6 +7,7 @@ import 'package:localsend_app/util/file_path_helper.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:saf_stream/saf_stream.dart';
|
||||
import 'package:saf_stream/saf_stream_method_channel.dart';
|
||||
|
||||
final _logger = Logger('FileSaver');
|
||||
|
||||
@@ -25,16 +26,29 @@ Future<void> saveFile({
|
||||
required DateTime? lastAccessed,
|
||||
required void Function(int savedBytes) onProgress,
|
||||
}) async {
|
||||
if (androidSdkInt != null && androidSdkInt <= 29) {
|
||||
final sdCardPath = getSdCardPath(destinationPath);
|
||||
if (sdCardPath != null) {
|
||||
// Use Android SAF to save the file to the SD card
|
||||
final info = await _saf.startWriteStream(
|
||||
Uri.parse('content://com.android.externalstorage.documents/tree/${sdCardPath.sdCardId}:${sdCardPath.path}'),
|
||||
if (androidSdkInt != null) {
|
||||
SafWriteStreamInfo? safInfo;
|
||||
|
||||
if (destinationPath.startsWith('content://')) {
|
||||
safInfo = await _saf.startWriteStream(
|
||||
Uri.parse(destinationPath),
|
||||
name,
|
||||
isImage ? 'image/*' : '*/*',
|
||||
);
|
||||
final sessionID = info.session;
|
||||
} else if (androidSdkInt <= 29) {
|
||||
final sdCardPath = getSdCardPath(destinationPath);
|
||||
if (sdCardPath != null) {
|
||||
// Use Android SAF to save the file to the SD card
|
||||
safInfo = await _saf.startWriteStream(
|
||||
Uri.parse('content://com.android.externalstorage.documents/tree/${sdCardPath.sdCardId}:${sdCardPath.path}'),
|
||||
name,
|
||||
isImage ? 'image/*' : '*/*',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (safInfo != null) {
|
||||
final sessionID = safInfo.session;
|
||||
await _saveFile(
|
||||
destinationPath: destinationPath,
|
||||
saveToGallery: saveToGallery,
|
||||
@@ -142,7 +156,13 @@ Future<String> digestFilePathAndPrepareDirectory({required String parentDirector
|
||||
final fileNameParts = p.split(fileName);
|
||||
final dir = p.joinAll([parentDirectory, ...fileNameParts.take(fileNameParts.length - 1)]);
|
||||
|
||||
Directory(dir).createSync(recursive: true);
|
||||
if (!dir.startsWith('content://')) {
|
||||
try {
|
||||
Directory(dir).createSync(recursive: true);
|
||||
} catch (e) {
|
||||
_logger.warning('Could not create directory', e);
|
||||
}
|
||||
}
|
||||
|
||||
String destinationPath;
|
||||
int counter = 1;
|
||||
|
||||
Reference in New Issue
Block a user