diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index cbcef2fc92b..fbf6bb793da 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -788,6 +788,10 @@ [b]Note:[/b] On Android and Linux, [param show_hidden] is ignored. [b]Note:[/b] On Android and macOS, native file dialogs have no title. [b]Note:[/b] On macOS, sandboxed apps will save security-scoped bookmarks to retain access to the opened folders across multiple sessions. Use [method OS.get_granted_permissions] to get a list of saved bookmarks. + [b]Note:[/b] On Android, this method uses the Android Storage Access Framework (SAF). + The file picker returns a URI instead of a filesystem path. This URI can be passed directly to [FileAccess] to perform read/write operations. + When using [constant FILE_DIALOG_MODE_OPEN_DIR], it returns a tree URI that grants full access to the selected directory. File operations inside this directory can be performed by passing a path in the form [code]treeUri#relative/path/to/file[/code] to [FileAccess]. + Tree URIs should be saved and reused; they remain valid across app restarts as long as the directory is not moved, renamed, or deleted. diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 85b6362c1f3..66ad96d29c8 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -13,6 +13,7 @@ apply from: "../scripts/publish-module.gradle" dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" + implementation 'androidx.documentfile:documentfile:1.1.0' testImplementation "junit:junit:4.13.2" } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt index 5112396f349..3c22e38e2a9 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/FilePicker.kt @@ -35,12 +35,13 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import android.os.Environment import android.provider.DocumentsContract import android.util.Log import android.webkit.MimeTypeMap -import androidx.annotation.RequiresApi +import androidx.core.net.toUri import org.godotengine.godot.GodotLib -import org.godotengine.godot.io.file.MediaStoreData +import java.io.File /** * Utility class for managing file selection and file picker activities. @@ -51,7 +52,6 @@ import org.godotengine.godot.io.file.MediaStoreData internal class FilePicker { companion object { private const val FILE_PICKER_REQUEST = 1000 - private const val FILE_SAVE_REQUEST = 1001 private val TAG = FilePicker::class.java.simpleName // Constants for fileMode values @@ -69,48 +69,49 @@ internal class FilePicker { * @param resultCode The result code returned by the activity. * @param data The intent data containing the selected file(s) or directory. */ - @RequiresApi(Build.VERSION_CODES.Q) fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == FILE_PICKER_REQUEST || requestCode == FILE_SAVE_REQUEST) { + if (requestCode == FILE_PICKER_REQUEST) { if (resultCode == Activity.RESULT_CANCELED) { Log.d(TAG, "File picker canceled") GodotLib.filePickerCallback(false, emptyArray()) return } if (resultCode == Activity.RESULT_OK) { - val selectedPaths: MutableList = mutableListOf() - // Handle multiple file selection. + val selectedFiles: MutableList = mutableListOf() val clipData = data?.clipData + if (clipData != null) { + // Handle multiple file selection. for (i in 0 until clipData.itemCount) { val uri = clipData.getItemAt(i).uri uri?.let { - val filepath = MediaStoreData.getFilePathFromUri(context, uri) - if (filepath != null) { - selectedPaths.add(filepath) - } else { - Log.d(TAG, "null filepath URI: $it") + try { + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } catch (e: SecurityException) { + Log.d(TAG, "Unable to persist URI: $it", e) } + selectedFiles.add(it.toString()) } } } else { val uri: Uri? = data?.data uri?.let { - val filepath = MediaStoreData.getFilePathFromUri(context, uri) - if (filepath != null) { - selectedPaths.add(filepath) - } else { - Log.d(TAG, "null filepath URI: $it") - } - - if (requestCode == FILE_SAVE_REQUEST) { - DocumentsContract.deleteDocument(context.contentResolver, it) + try { + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } catch (e: SecurityException) { + Log.w(TAG, "Unable to persist URI: $it", e) } + selectedFiles.add(it.toString()) } } - - if (selectedPaths.isNotEmpty()) { - GodotLib.filePickerCallback(true, selectedPaths.toTypedArray()) + if (selectedFiles.isNotEmpty()) { + GodotLib.filePickerCallback(true, selectedFiles.toTypedArray()) } else { GodotLib.filePickerCallback(false, emptyArray()) } @@ -129,15 +130,14 @@ internal class FilePicker { * @param fileMode The mode to operate in, specifying open, save, or directory select. * @param filters Array of MIME types to filter file selection. */ - @RequiresApi(Build.VERSION_CODES.Q) fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array) { val intent = when (fileMode) { FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT) else -> Intent(Intent.ACTION_OPEN_DOCUMENT) } - val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) { + val initialDirectory = getUriFromDirectoryPath(context, currentDirectory) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialDirectory != null) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory) } else { Log.d(TAG, "Error cannot set initial directory") @@ -157,11 +157,7 @@ internal class FilePicker { intent.addCategory(Intent.CATEGORY_OPENABLE) } intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true) - if (fileMode == FILE_MODE_SAVE_FILE) { - activity?.startActivityForResult(intent, FILE_SAVE_REQUEST) - } else { - activity?.startActivityForResult(intent, FILE_PICKER_REQUEST) - } + activity?.startActivityForResult(intent, FILE_PICKER_REQUEST) } /** @@ -201,5 +197,37 @@ internal class FilePicker { // Fallback to a generic MIME type if the input is neither a valid extension nor MIME type. return "application/octet-stream" } + + private fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? { + if (directoryPath.startsWith("content://")) { + return directoryPath.toUri() + } + if (!directoryExists(directoryPath)) { + return null + } + + val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath + if (directoryPath.startsWith(externalStorageRoot)) { + val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/') + val uri = Uri.Builder() + .scheme("content") + .authority("com.android.externalstorage.documents") + .appendPath("document") + .appendPath("primary:$relativePath") + .build() + return uri + } + return null + } + + private fun directoryExists(path: String): Boolean { + return try { + val file = File(path) + file.isDirectory && file.exists() + } catch (e: SecurityException) { + Log.d(TAG, "Failed to check directoryExists: $path", e) + false + } + } } } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/StorageScope.kt index 574ecd58eb7..6712d68cb0c 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/StorageScope.kt @@ -30,9 +30,11 @@ package org.godotengine.godot.io +import android.content.ContentResolver import android.content.Context import android.os.Build import android.os.Environment +import androidx.core.net.toUri import java.io.File import org.godotengine.godot.GodotLib @@ -55,6 +57,11 @@ internal enum class StorageScope { */ SHARED, + /** + * Covers the file access using SAF + */ + SAF, + /** * Everything else.. */ @@ -64,6 +71,7 @@ internal enum class StorageScope { companion object { internal const val ASSETS_PREFIX = "assets://" + internal const val CONTENT_PREFIX = "content://" } private val internalAppDir: String? = context.filesDir.canonicalPath @@ -93,6 +101,11 @@ internal enum class StorageScope { return ASSETS } + // If it's either content uri for a file, or starts with a tree uri. + if (path.startsWith(CONTENT_PREFIX)) { + return SAF + } + var pathFile = File(path) if (!pathFile.isAbsolute) { pathFile = File(GodotLib.getProjectResourceDir(), path) diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt index bcc27758a7a..d14fa219b0b 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -109,7 +109,7 @@ class DirectoryAccessHandler(context: Context) { val accessTypeFromStorageScope = when (storageScope) { StorageScope.ASSETS -> ACCESS_RESOURCES StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM - StorageScope.UNKNOWN -> null + StorageScope.SAF, StorageScope.UNKNOWN -> null } if (accessTypeFromStorageScope != null) { diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/DataAccess.kt index daf6af3a1b5..b426decf7a7 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/DataAccess.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/DataAccess.kt @@ -30,9 +30,11 @@ package org.godotengine.godot.io.file +import android.content.ContentResolver import android.content.Context import android.os.Build import android.util.Log +import androidx.core.net.toUri import org.godotengine.godot.error.Error import org.godotengine.godot.io.StorageScope import java.io.FileNotFoundException @@ -64,7 +66,6 @@ internal abstract class DataAccess { val assetData = AssetData(context, filePath, FileAccessFlags.READ) Channels.newInputStream(assetData.readChannel) } - StorageScope.APP -> { val fileData = FileData(filePath, FileAccessFlags.READ) Channels.newInputStream(fileData.fileChannel) @@ -77,6 +78,10 @@ internal abstract class DataAccess { null } } + StorageScope.SAF -> { + val safData = SAFData(context, filePath, FileAccessFlags.READ) + Channels.newInputStream(safData.fileChannel) + } StorageScope.UNKNOWN -> null } @@ -91,14 +96,13 @@ internal abstract class DataAccess { ): DataAccess? { return when (storageScope) { StorageScope.APP -> FileData(filePath, accessFlag) - StorageScope.ASSETS -> AssetData(context, filePath, accessFlag) - StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStoreData(context, filePath, accessFlag) } else { null } + StorageScope.SAF -> SAFData(context, filePath, accessFlag) StorageScope.UNKNOWN -> null } @@ -113,6 +117,7 @@ internal abstract class DataAccess { } else { false } + StorageScope.SAF -> SAFData.fileExists(context, path) StorageScope.UNKNOWN -> false } @@ -127,6 +132,7 @@ internal abstract class DataAccess { } else { 0L } + StorageScope.SAF -> SAFData.fileLastModified(context, path) StorageScope.UNKNOWN -> 0L } @@ -136,8 +142,7 @@ internal abstract class DataAccess { return when(storageScope) { StorageScope.APP -> FileData.fileLastAccessed(path) StorageScope.ASSETS -> AssetData.fileLastAccessed(path) - StorageScope.SHARED -> MediaStoreData.fileLastAccessed(context, path) - StorageScope.UNKNOWN -> 0L + StorageScope.SHARED, StorageScope.SAF, StorageScope.UNKNOWN -> 0L } } @@ -145,7 +150,13 @@ internal abstract class DataAccess { return when(storageScope) { StorageScope.APP -> FileData.fileSize(path) StorageScope.ASSETS -> AssetData.fileSize(path) - StorageScope.SHARED -> MediaStoreData.fileSize(context, path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.fileSize(context, path) + } else { + -1L + } + StorageScope.SAF -> SAFData.fileSize(context, path) + StorageScope.UNKNOWN -> -1L } } @@ -159,6 +170,7 @@ internal abstract class DataAccess { } else { false } + StorageScope.SAF -> SAFData.delete(context, path) StorageScope.UNKNOWN -> false } @@ -173,6 +185,7 @@ internal abstract class DataAccess { } else { false } + StorageScope.SAF -> SAFData.rename(context, from, to) StorageScope.UNKNOWN -> false } diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt index e78b29472fe..3c22aa3d722 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/MediaStoreData.kt @@ -83,10 +83,6 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " + " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?" - private const val AUTHORITY_MEDIA_DOCUMENTS = "com.android.providers.media.documents" - private const val AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS = "com.android.externalstorage.documents" - private const val AUTHORITY_DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents" - private fun getSelectionByPathArguments(path: String): Array { return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path)) } @@ -212,10 +208,6 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi return dataItem.dateModified.toLong() / 1000L } - fun fileLastAccessed(@Suppress("UNUSED_PARAMETER") context: Context, @Suppress("UNUSED_PARAMETER") path: String): Long { - return 0L - } - fun fileSize(context: Context, path: String): Long { val result = queryByPath(context, path) if (result.isEmpty()) { @@ -251,71 +243,6 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi return updated > 0 } - fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? { - if (!directoryExists(directoryPath)) { - return null - } - // Check if the path is under external storage. - val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath - if (directoryPath.startsWith(externalStorageRoot)) { - val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/') - val uri = Uri.Builder() - .scheme("content") - .authority(AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) - .appendPath("document") - .appendPath("primary:$relativePath") - .build() - return uri - } - return null - } - - fun getFilePathFromUri(context: Context, uri: Uri): String? { - // Converts content uri to filepath. - val id = getIdFromUri(uri) ?: return null - - if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) { - val split = id.split(":") - val fileName = split.last() - val relativePath = split.dropLast(1).joinToString("/") - val fullPath = File(Environment.getExternalStorageDirectory(), "$relativePath/$fileName").absolutePath - return fullPath - } else { - val id = id.toLongOrNull() ?: return null - val dataItems = queryById(context, id) - return if (dataItems.isNotEmpty()) { - val dataItem = dataItems[0] - File(Environment.getExternalStorageDirectory(), File(dataItem.relativePath, dataItem.displayName).toString()).absolutePath - } else { - null - } - } - } - - private fun getIdFromUri(uri: Uri): String? { - return try { - if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS || uri.authority == AUTHORITY_MEDIA_DOCUMENTS || uri.authority == AUTHORITY_DOWNLOADS_DOCUMENTS) { - val documentId = uri.lastPathSegment ?: throw IllegalArgumentException("Invalid URI: $uri") - documentId.substringAfter(":") - } else { - throw IllegalArgumentException("Unsupported URI format: $uri") - } - } catch (e: Exception) { - Log.d(TAG, "Failed to parse ID from URI: $uri", e) - null - } - } - - private fun directoryExists(path: String): Boolean { - return try { - val file = File(path) - file.isDirectory && file.exists() - } catch (e: SecurityException) { - Log.d(TAG, "Failed to check directoryExists: $path", e) - false - } - } - } private val id: Long diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt new file mode 100644 index 00000000000..23ca5fcf056 --- /dev/null +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/io/file/SAFData.kt @@ -0,0 +1,190 @@ +/**************************************************************************/ +/* SAFData.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.io.file + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract +import android.util.Log +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.channels.FileChannel + +/** + * Implementation of [DataAccess] which handles file access via a content URI obtained using the Android + * Storage Access Framework (SAF). + */ +internal class SAFData(context: Context, path: String, accessFlag: FileAccessFlags) : + DataAccess.FileChannelDataAccess(path) { + + companion object { + private val TAG = SAFData::class.java.simpleName + + fun fileExists(context: Context, path: String): Boolean { + return try { + val uri = resolvePath(context, path, FileAccessFlags.READ) + context.contentResolver.query(uri, arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME), null, null, null) + ?.use { cursor -> cursor.moveToFirst() } == true + } catch (e: Exception) { + Log.d(TAG, "Error checking file existence", e) + false + } + } + + fun fileLastModified(context: Context, path: String): Long { + return try { + val uri = resolvePath(context, path, FileAccessFlags.READ) + val projection = arrayOf(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + context.contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + if (index != -1) { + return cursor.getLong(index) / 1000L + } + } + } + 0L + } catch (e: Exception) { + Log.d(TAG, "Error reading last modified for", e) + 0L + } + } + + fun fileSize(context: Context, path: String): Long { + return try { + val uri = resolvePath(context, path, FileAccessFlags.READ) + val projection = arrayOf(DocumentsContract.Document.COLUMN_SIZE) + context.contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) + if (index != -1) { + return cursor.getLong(index) + } + } + } + -1L + } catch (e: Exception) { + Log.d(TAG, "Error reading file size", e) + -1L + } + } + + fun delete(context: Context, path: String): Boolean { + return try { + val uri = resolvePath(context, path, FileAccessFlags.READ) + DocumentsContract.deleteDocument(context.contentResolver, uri) + } catch (e: Exception) { + Log.d(TAG, "Error deleting file", e) + false + } + } + + fun rename(context: Context, from: String, to: String): Boolean { + // See https://github.com/godotengine/godot/pull/112215#discussion_r2479311235 + return false + } + + private fun resolvePath(context: Context, path: String, accessFlag: FileAccessFlags): Uri { + val uri = path.toUri() + val fragment = uri.fragment + + if (fragment == null) { + return uri + } + + // For directory format: content://treeUri#relative/path/to/file + val treeUri = uri.buildUpon().fragment(null).build() + val relativePath = fragment + + val rootDir = DocumentFile.fromTreeUri(context, treeUri) + ?: throw IllegalStateException("Unable to resolve tree URI: $treeUri") + + val parts = relativePath.split('/') + val filename = parts.last() + val folderParts = parts.dropLast(1) + + var current: DocumentFile? = rootDir + + val isWriteMode = when (accessFlag) { + FileAccessFlags.WRITE, + FileAccessFlags.READ_WRITE, + FileAccessFlags.WRITE_READ -> true + else -> false + } + + for (folder in folderParts) { + var next = current?.findFile(folder) + + if (next == null) { + if (isWriteMode) { + next = current?.createDirectory(folder) + ?: throw IllegalStateException("Failed to create directory: $folder") + } else { + throw IllegalStateException("Directory not found: $folder") + } + } + + current = next + } + + var file = current?.findFile(filename) + + if (file == null) { + if (isWriteMode) { + file = current?.createFile("*/*", filename) + ?: throw IllegalStateException("Failed to create file: $filename") + } else { + throw IllegalStateException("File does not exist: $relativePath") + } + } + + return file.uri + } + } + + override val fileChannel: FileChannel + init { + val uri = resolvePath(context, path, accessFlag) + val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, accessFlag.getMode()) + ?: throw IllegalStateException("Unable to access file descriptor") + fileChannel = if (accessFlag == FileAccessFlags.READ) { + FileInputStream(parcelFileDescriptor.fileDescriptor).channel + } else { + FileOutputStream(parcelFileDescriptor.fileDescriptor).channel + } + + if (accessFlag.shouldTruncate()) { + fileChannel.truncate(0) + } + } +}