fix(android): save files outside Download folder (#1548)

This commit is contained in:
Tien Do Nam
2024-07-22 04:14:43 +02:00
committed by GitHub
parent a8a24a2836
commit e7ca621fb7
9 changed files with 75 additions and 19 deletions
@@ -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
View File
@@ -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)
+9 -1
View File
@@ -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';
+2 -2
View File
@@ -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) {
@@ -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) {
@@ -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._();
+28 -8
View File
@@ -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;