feat(android): expose content URI file descriptor

This commit is contained in:
Tien Do Nam
2026-07-15 16:13:19 +02:00
parent 1a6d5eaeae
commit 3bb7627646
2 changed files with 49 additions and 0 deletions
@@ -2,6 +2,7 @@ package org.localsend.localsend_app
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.database.Cursor
@@ -58,6 +59,8 @@ class MainActivity : FlutterActivity() {
"createDirectory" -> handleCreateDirectory(call, result)
"getFileDescriptor" -> handleGetFileDescriptor(call, result)
"openContentUri" -> {
openUri(context, call.argument<String>("uri")!!)
result.success(null)
@@ -82,6 +85,38 @@ class MainActivity : FlutterActivity() {
Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) != 0.0f;
}
private fun handleGetFileDescriptor(call: MethodCall, result: MethodChannel.Result) {
val uriString = call.argument<String>("uri")
if (uriString == null) {
result.error("INVALID_ARGUMENT", "Missing content URI", null)
return
}
val uri = Uri.parse(uriString)
if (uri.scheme != ContentResolver.SCHEME_CONTENT) {
result.error("INVALID_ARGUMENT", "Expected a content:// URI", null)
return
}
try {
val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")
if (parcelFileDescriptor == null) {
result.error("OPEN_FAILED", "The content provider did not return a file descriptor", null)
return
}
// Ownership of the detached descriptor is transferred to the caller. It must be
// closed by Rust (or whichever native consumer receives it) after use.
parcelFileDescriptor.use {
result.success(it.detachFd())
}
} catch (e: SecurityException) {
result.error("PERMISSION_DENIED", e.message ?: "Permission denied for content URI", null)
} catch (e: Exception) {
result.error("OPEN_FAILED", e.message ?: "Failed to open content URI", null)
}
}
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)
@@ -44,6 +44,20 @@ Future<bool> getSystemAnimationsStatusAndroid() async {
return await _methodChannel.invokeMethod('isAnimationsEnabled') ?? true;
}
/// Opens [uri] for reading and returns an owned Linux file descriptor.
///
/// The descriptor stays open after this call and must be closed by the native
/// consumer it is passed to.
Future<int> getFileDescriptorAndroid({required String uri}) async {
final fileDescriptor = await _methodChannel.invokeMethod<int>('getFileDescriptor', {
'uri': uri,
});
if (fileDescriptor == null) {
throw StateError('Android returned no file descriptor for $uri');
}
return fileDescriptor;
}
Future<void> createDirectory({
required String documentUri,
required String directoryName,