1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-06 12:20:30 +00:00

Add full support for Android scoped storage.

This was done by refactoring directory and file access handling for the Android platform so that any general filesystem access type go through the Android layer.
This allows us to validate whether the access is unrestricted, or whether it falls under scoped storage and thus act appropriately.
This commit is contained in:
Fredia Huya-Kouadio
2021-07-10 18:39:31 -07:00
committed by Fredia Huya-Kouadio
parent 100d223736
commit f9c19298ce
40 changed files with 2435 additions and 299 deletions

View File

@@ -34,6 +34,8 @@ import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.WINDOW_SERVICE;
import org.godotengine.godot.input.GodotEditText;
import org.godotengine.godot.io.directory.DirectoryAccessHandler;
import org.godotengine.godot.io.file.FileAccessHandler;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
import org.godotengine.godot.tts.GodotTTS;
@@ -164,9 +166,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private Sensor mMagnetometer;
private Sensor mGyroscope;
public static GodotIO io;
public static GodotNetUtils netUtils;
public static GodotTTS tts;
public GodotIO io;
public GodotNetUtils netUtils;
public GodotTTS tts;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@@ -458,16 +460,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
final Activity activity = getActivity();
io = new GodotIO(activity);
GodotLib.io = io;
netUtils = new GodotNetUtils(activity);
tts = new GodotTTS(activity);
Context context = getContext();
DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion);
GodotLib.initialize(activity,
this,
activity.getAssets(),
io,
netUtils,
directoryAccessHandler,
fileAccessHandler,
use_apk_expansion,
tts);
result_callback = null;

View File

@@ -36,7 +36,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
@@ -46,12 +45,10 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.WindowInsets;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
@@ -60,7 +57,6 @@ import java.util.Locale;
public class GodotIO {
private static final String TAG = GodotIO.class.getSimpleName();
private final AssetManager am;
private final Activity activity;
private final String uniqueId;
GodotEditText edit;
@@ -73,100 +69,8 @@ public class GodotIO {
final int SCREEN_SENSOR_PORTRAIT = 5;
final int SCREEN_SENSOR = 6;
/////////////////////////
/// DIRECTORIES
/////////////////////////
static class AssetDir {
public String[] files;
public int current;
public String path;
}
private int last_dir_id = 1;
private final SparseArray<AssetDir> dirs;
public int dir_open(String path) {
AssetDir ad = new AssetDir();
ad.current = 0;
ad.path = path;
try {
ad.files = am.list(path);
// no way to find path is directory or file exactly.
// but if ad.files.length==0, then it's an empty directory or file.
if (ad.files.length == 0) {
return -1;
}
} catch (IOException e) {
System.out.printf("Exception on dir_open: %s\n", e);
return -1;
}
++last_dir_id;
dirs.put(last_dir_id, ad);
return last_dir_id;
}
public boolean dir_is_dir(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return false;
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
int idx = ad.current;
if (idx > 0)
idx--;
if (idx >= ad.files.length)
return false;
String fname = ad.files[idx];
try {
if (ad.path.equals(""))
am.open(fname);
else
am.open(ad.path + "/" + fname);
return false;
} catch (Exception e) {
return true;
}
}
public String dir_next(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return "";
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
if (ad.current >= ad.files.length) {
ad.current++;
return "";
}
String r = ad.files[ad.current];
ad.current++;
return r;
}
public void dir_close(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_close: invalid dir id: %d\n", id);
return;
}
dirs.remove(id);
}
GodotIO(Activity p_activity) {
am = p_activity.getAssets();
activity = p_activity;
dirs = new SparseArray<>();
String androidId = Settings.Secure.getString(activity.getContentResolver(),
Settings.Secure.ANDROID_ID);
if (androidId == null) {

View File

@@ -31,8 +31,13 @@
package org.godotengine.godot;
import org.godotengine.godot.gl.GodotRenderer;
import org.godotengine.godot.io.directory.DirectoryAccessHandler;
import org.godotengine.godot.io.file.FileAccessHandler;
import org.godotengine.godot.tts.GodotTTS;
import org.godotengine.godot.utils.GodotNetUtils;
import android.app.Activity;
import android.content.res.AssetManager;
import android.hardware.SensorEvent;
import android.view.Surface;
@@ -42,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10;
* Wrapper for native library
*/
public class GodotLib {
public static GodotIO io;
static {
System.loadLibrary("godot_android");
}
@@ -51,7 +54,15 @@ public class GodotLib {
/**
* Invoked on the main thread to initialize Godot native layer.
*/
public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
public static native void initialize(Activity activity,
Godot p_instance,
AssetManager p_asset_manager,
GodotIO godotIO,
GodotNetUtils netUtils,
DirectoryAccessHandler directoryAccessHandler,
FileAccessHandler fileAccessHandler,
boolean use_apk_expansion,
GodotTTS tts);
/**
* Invoked on the main thread to clean up Godot native layer.

View File

@@ -0,0 +1,114 @@
/*************************************************************************/
/* StorageScope.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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
import android.content.Context
import android.os.Build
import android.os.Environment
import java.io.File
/**
* Represents the different storage scopes.
*/
internal enum class StorageScope {
/**
* Covers internal and external directories accessible to the app without restrictions.
*/
APP,
/**
* Covers shared directories (from Android 10 and higher).
*/
SHARED,
/**
* Everything else..
*/
UNKNOWN;
companion object {
/**
* Determines which [StorageScope] the given path falls under.
*/
fun getStorageScope(context: Context, path: String?): StorageScope {
if (path == null) {
return UNKNOWN
}
val pathFile = File(path)
if (!pathFile.isAbsolute) {
return UNKNOWN
}
val canonicalPathFile = pathFile.canonicalPath
val internalAppDir = context.filesDir.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(internalAppDir)) {
return APP
}
val internalCacheDir = context.cacheDir.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(internalCacheDir)) {
return APP
}
val externalAppDir = context.getExternalFilesDir(null)?.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(externalAppDir)) {
return APP
}
val sharedDir = Environment.getExternalStorageDirectory().canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(sharedDir)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// Before R, apps had access to shared storage so long as they have the right
// permissions (and flag on Q).
return APP
}
// Post R, access is limited based on the target destination
// 'Downloads' and 'Documents' are still accessible
val downloadsSharedDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
?: return SHARED
val documentsSharedDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
?: return SHARED
if (canonicalPathFile.startsWith(downloadsSharedDir) || canonicalPathFile.startsWith(documentsSharedDir)) {
return APP
}
return SHARED
}
return UNKNOWN
}
}
}

View File

@@ -0,0 +1,177 @@
/*************************************************************************/
/* AssetsDirectoryAccess.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.directory
import android.content.Context
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
import java.io.File
import java.io.IOException
/**
* Handles directories access within the Android assets directory.
*/
internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
companion object {
private val TAG = AssetsDirectoryAccess::class.java.simpleName
}
private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
private val assetManager = context.assets
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<AssetDir>()
private fun getAssetsPath(originalPath: String): String {
if (originalPath.startsWith(File.separatorChar)) {
return originalPath.substring(1)
}
return originalPath
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int {
val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID
try {
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
if (files.isEmpty()) {
return INVALID_DIR_ID
}
val ad = AssetDir(assetsPath, files)
dirs.put(++lastDirId, ad)
return lastDirId
} catch (e: IOException) {
Log.e(TAG, "Exception on dirOpen", e)
return INVALID_DIR_ID
}
}
override fun dirExists(path: String): Boolean {
val assetsPath = getAssetsPath(path)
try {
val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
return files.isNotEmpty()
} catch (e: IOException) {
Log.e(TAG, "Exception on dirExists", e)
return false
}
}
override fun fileExists(path: String): Boolean {
val assetsPath = getAssetsPath(path) ?: return false
try {
val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
return files.isEmpty()
} catch (e: IOException) {
Log.e(TAG, "Exception on fileExists", e)
return false
}
}
override fun dirIsDir(dirId: Int): Boolean {
val ad: AssetDir = dirs[dirId]
var idx = ad.current
if (idx > 0) {
idx--
}
if (idx >= ad.files.size) {
return false
}
val fileName = ad.files[idx]
// List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a
// directory
val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}"
val fileContents = assetManager.list(filePath)
return (fileContents?.size?: 0) > 0
}
override fun isCurrentHidden(dirId: Int): Boolean {
val ad = dirs[dirId]
var idx = ad.current
if (idx > 0) {
idx--
}
if (idx >= ad.files.size) {
return false
}
val fileName = ad.files[idx]
return fileName.startsWith('.')
}
override fun dirNext(dirId: Int): String {
val ad: AssetDir = dirs[dirId]
if (ad.current >= ad.files.size) {
ad.current++
return ""
}
return ad.files[ad.current++]
}
override fun dirClose(dirId: Int) {
dirs.remove(dirId)
}
override fun getDriveCount() = 0
override fun getDrive(drive: Int) = ""
override fun makeDir(dir: String) = false
override fun getSpaceLeft() = 0L
override fun rename(from: String, to: String) = false
override fun remove(filename: String) = false
}

View File

@@ -0,0 +1,224 @@
/*************************************************************************/
/* DirectoryAccessHandler.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.directory
import android.content.Context
import android.util.Log
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
/**
* Handles files and directories access and manipulation for the Android platform
*/
class DirectoryAccessHandler(context: Context) {
companion object {
private val TAG = DirectoryAccessHandler::class.java.simpleName
internal const val INVALID_DIR_ID = -1
internal const val STARTING_DIR_ID = 1
private fun getAccessTypeFromNative(accessType: Int): AccessType? {
return when (accessType) {
ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
else -> null
}
}
}
private enum class AccessType(val nativeValue: Int) {
ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
}
internal interface DirectoryAccess {
fun dirOpen(path: String): Int
fun dirNext(dirId: Int): String
fun dirClose(dirId: Int)
fun dirIsDir(dirId: Int): Boolean
fun dirExists(path: String): Boolean
fun fileExists(path: String): Boolean
fun hasDirId(dirId: Int): Boolean
fun isCurrentHidden(dirId: Int): Boolean
fun getDriveCount() : Int
fun getDrive(drive: Int): String
fun makeDir(dir: String): Boolean
fun getSpaceLeft(): Long
fun rename(from: String, to: String): Boolean
fun remove(filename: String): Boolean
}
private val assetsDirAccess = AssetsDirectoryAccess(context)
private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
}
}
fun dirOpen(nativeAccessType: Int, path: String?): Int {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return INVALID_DIR_ID
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
}
}
fun dirNext(nativeAccessType: Int, dirId: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
return ""
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
}
}
fun dirClose(nativeAccessType: Int, dirId: Int) {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
return
}
when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
}
}
fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
}
}
fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
}
}
fun dirExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
}
}
fun fileExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
}
}
fun getDriveCount(nativeAccessType: Int): Int {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
return when(accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
}
}
fun getDrive(nativeAccessType: Int, drive: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
}
}
fun makeDir(nativeAccessType: Int, dir: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
}
}
fun getSpaceLeft(nativeAccessType: Int): Long {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
}
}
fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
}
}
fun remove(nativeAccessType: Int, filename: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
}
}
}

View File

@@ -0,0 +1,230 @@
/*************************************************************************/
/* FileSystemDirectoryAccess.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.directory
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.storage.StorageManager
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
import org.godotengine.godot.io.file.FileAccessHandler
import java.io.File
/**
* Handles directories access with the internal and external filesystem.
*/
internal class FilesystemDirectoryAccess(private val context: Context):
DirectoryAccessHandler.DirectoryAccess {
companion object {
private val TAG = FilesystemDirectoryAccess::class.java.simpleName
}
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<DirData>()
private fun inScope(path: String): Boolean {
// Directory access is available for shared storage on Android 11+
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
// tag is available.
return StorageScope.getStorageScope(context, path) != StorageScope.UNKNOWN
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int {
if (!inScope(path)) {
Log.w(TAG, "Path $path is not accessible.")
return INVALID_DIR_ID
}
// Check this is a directory.
val dirFile = File(path)
if (!dirFile.isDirectory) {
return INVALID_DIR_ID
}
// Get the files in the directory
val files = dirFile.listFiles()?: return INVALID_DIR_ID
// Create the data representing this directory
val dirData = DirData(dirFile, files)
dirs.put(++lastDirId, dirData)
return lastDirId
}
override fun dirExists(path: String): Boolean {
if (!inScope(path)) {
Log.w(TAG, "Path $path is not accessible.")
return false
}
try {
return File(path).isDirectory
} catch (e: SecurityException) {
return false
}
}
override fun fileExists(path: String) = FileAccessHandler.fileExists(context, path)
override fun dirNext(dirId: Int): String {
val dirData = dirs[dirId]
if (dirData.current >= dirData.files.size) {
dirData.current++
return ""
}
return dirData.files[dirData.current++].name
}
override fun dirClose(dirId: Int) {
dirs.remove(dirId)
}
override fun dirIsDir(dirId: Int): Boolean {
val dirData = dirs[dirId]
var index = dirData.current
if (index > 0) {
index--
}
if (index >= dirData.files.size) {
return false
}
return dirData.files[index].isDirectory
}
override fun isCurrentHidden(dirId: Int): Boolean {
val dirData = dirs[dirId]
var index = dirData.current
if (index > 0) {
index--
}
if (index >= dirData.files.size) {
return false
}
return dirData.files[index].isHidden
}
override fun getDriveCount(): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
storageManager.storageVolumes.size
} else {
0
}
}
override fun getDrive(drive: Int): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return ""
}
if (drive < 0 || drive >= storageManager.storageVolumes.size) {
return ""
}
val storageVolume = storageManager.storageVolumes[drive]
return storageVolume.getDescription(context)
}
override fun makeDir(dir: String): Boolean {
if (!inScope(dir)) {
Log.w(TAG, "Directory $dir is not accessible.")
return false
}
try {
val dirFile = File(dir)
return dirFile.isDirectory || dirFile.mkdirs()
} catch (e: SecurityException) {
return false
}
}
@SuppressLint("UsableSpace")
override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L
override fun rename(from: String, to: String): Boolean {
if (!inScope(from) || !inScope(to)) {
Log.w(TAG, "Argument filenames are not accessible:\n" +
"from: $from\n" +
"to: $to")
return false
}
return try {
val fromFile = File(from)
if (fromFile.isDirectory) {
fromFile.renameTo(File(to))
} else {
FileAccessHandler.renameFile(context, from, to)
}
} catch (e: SecurityException) {
false
}
}
override fun remove(filename: String): Boolean {
if (!inScope(filename)) {
Log.w(TAG, "Filename $filename is not accessible.")
return false
}
return try {
val deleteFile = File(filename)
if (deleteFile.exists()) {
if (deleteFile.isDirectory) {
deleteFile.delete()
} else {
FileAccessHandler.removeFile(context, filename)
}
} else {
true
}
} catch (e: SecurityException) {
false
}
}
}

View File

@@ -0,0 +1,186 @@
/*************************************************************************/
/* DataAccess.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.os.Build
import android.util.Log
import org.godotengine.godot.io.StorageScope
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.math.max
/**
* Base class for file IO operations.
*
* Its derived instances provide concrete implementations to handle regular file access, as well
* as file access through the media store API on versions of Android were scoped storage is enabled.
*/
internal abstract class DataAccess(private val filePath: String) {
companion object {
private val TAG = DataAccess::class.java.simpleName
fun generateDataAccess(
storageScope: StorageScope,
context: Context,
filePath: String,
accessFlag: FileAccessFlags
): DataAccess? {
return when (storageScope) {
StorageScope.APP -> FileData(filePath, accessFlag)
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStoreData(context, filePath, accessFlag)
} else {
null
}
StorageScope.UNKNOWN -> null
}
}
fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.fileExists(path)
StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
StorageScope.UNKNOWN -> false
}
}
fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
return when(storageScope) {
StorageScope.APP -> FileData.fileLastModified(path)
StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
StorageScope.UNKNOWN -> 0L
}
}
fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.delete(path)
StorageScope.SHARED -> MediaStoreData.delete(context, path)
StorageScope.UNKNOWN -> false
}
}
fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
return when(storageScope) {
StorageScope.APP -> FileData.rename(from, to)
StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
StorageScope.UNKNOWN -> false
}
}
}
protected abstract val fileChannel: FileChannel
internal var endOfFile = false
private set
fun close() {
try {
fileChannel.close()
} catch (e: IOException) {
Log.w(TAG, "Exception when closing file $filePath.", e)
}
}
fun flush() {
try {
fileChannel.force(false)
} catch (e: IOException) {
Log.w(TAG, "Exception when flushing file $filePath.", e)
}
}
fun seek(position: Long) {
try {
fileChannel.position(position)
if (position <= size()) {
endOfFile = false
}
} catch (e: Exception) {
Log.w(TAG, "Exception when seeking file $filePath.", e)
}
}
fun seekFromEnd(positionFromEnd: Long) {
val positionFromBeginning = max(0, size() - positionFromEnd)
seek(positionFromBeginning)
}
fun position(): Long {
return try {
fileChannel.position()
} catch (e: IOException) {
Log.w(
TAG,
"Exception when retrieving position for file $filePath.",
e
)
0L
}
}
fun size() = try {
fileChannel.size()
} catch (e: IOException) {
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
0L
}
fun read(buffer: ByteBuffer): Int {
return try {
val readBytes = fileChannel.read(buffer)
if (readBytes == -1) {
endOfFile = true
0
} else {
readBytes
}
} catch (e: IOException) {
Log.w(TAG, "Exception while reading from file $filePath.", e)
0
}
}
fun write(buffer: ByteBuffer) {
try {
val writtenBytes = fileChannel.write(buffer)
if (writtenBytes > 0) {
endOfFile = false
}
} catch (e: IOException) {
Log.w(TAG, "Exception while writing to file $filePath.", e)
}
}
}

View File

@@ -0,0 +1,87 @@
/*************************************************************************/
/* FileAccessFlags.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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
/**
* Android representation of Godot native access flags.
*/
internal enum class FileAccessFlags(val nativeValue: Int) {
/**
* Opens the file for read operations.
* The cursor is positioned at the beginning of the file.
*/
READ(1),
/**
* Opens the file for write operations.
* The file is created if it does not exist, and truncated if it does.
*/
WRITE(2),
/**
* Opens the file for read and write operations.
* Does not truncate the file. The cursor is positioned at the beginning of the file.
*/
READ_WRITE(3),
/**
* Opens the file for read and write operations.
* The file is created if it does not exist, and truncated if it does.
* The cursor is positioned at the beginning of the file.
*/
WRITE_READ(7);
fun getMode(): String {
return when (this) {
READ -> "r"
WRITE -> "w"
READ_WRITE, WRITE_READ -> "rw"
}
}
fun shouldTruncate(): Boolean {
return when (this) {
READ, READ_WRITE -> false
WRITE, WRITE_READ -> true
}
}
companion object {
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
for (flag in values()) {
if (flag.nativeValue == modeFlag) {
return flag
}
}
return null
}
}
}

View File

@@ -0,0 +1,202 @@
/*************************************************************************/
/* FileAccessHandler.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.util.Log
import android.util.SparseArray
import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException
import java.nio.ByteBuffer
/**
* Handles regular and media store file access and interactions.
*/
class FileAccessHandler(val context: Context) {
companion object {
private val TAG = FileAccessHandler::class.java.simpleName
private const val FILE_NOT_FOUND_ERROR_ID = -1
private const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
fun fileExists(context: Context, path: String?): Boolean {
val storageScope = StorageScope.getStorageScope(context, path)
if (storageScope == StorageScope.UNKNOWN) {
return false
}
return try {
DataAccess.fileExists(storageScope, context, path!!)
} catch (e: SecurityException) {
false
}
}
fun removeFile(context: Context, path: String?): Boolean {
val storageScope = StorageScope.getStorageScope(context, path)
if (storageScope == StorageScope.UNKNOWN) {
return false
}
return try {
DataAccess.removeFile(storageScope, context, path!!)
} catch (e: Exception) {
false
}
}
fun renameFile(context: Context, from: String?, to: String?): Boolean {
val storageScope = StorageScope.getStorageScope(context, from)
if (storageScope == StorageScope.UNKNOWN) {
return false
}
return try {
DataAccess.renameFile(storageScope, context, from!!, to!!)
} catch (e: Exception) {
false
}
}
}
private val files = SparseArray<DataAccess>()
private var lastFileId = STARTING_FILE_ID
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
fun fileOpen(path: String?, modeFlags: Int): Int {
val storageScope = StorageScope.getStorageScope(context, path)
if (storageScope == StorageScope.UNKNOWN) {
return INVALID_FILE_ID
}
try {
val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
files.put(++lastFileId, dataAccess)
return lastFileId
} catch (e: FileNotFoundException) {
return FILE_NOT_FOUND_ERROR_ID
} catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e)
return INVALID_FILE_ID
}
}
fun fileGetSize(fileId: Int): Long {
if (!hasFileId(fileId)) {
return 0L
}
return files[fileId].size()
}
fun fileSeek(fileId: Int, position: Long) {
if (!hasFileId(fileId)) {
return
}
files[fileId].seek(position)
}
fun fileSeekFromEnd(fileId: Int, position: Long) {
if (!hasFileId(fileId)) {
return
}
files[fileId].seekFromEnd(position)
}
fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int {
if (!hasFileId(fileId) || byteBuffer == null) {
return 0
}
return files[fileId].read(byteBuffer)
}
fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
if (!hasFileId(fileId) || byteBuffer == null) {
return
}
files[fileId].write(byteBuffer)
}
fun fileFlush(fileId: Int) {
if (!hasFileId(fileId)) {
return
}
files[fileId].flush()
}
fun fileExists(path: String?) = Companion.fileExists(context, path)
fun fileLastModified(filepath: String?): Long {
val storageScope = StorageScope.getStorageScope(context, filepath)
if (storageScope == StorageScope.UNKNOWN) {
return 0L
}
return try {
DataAccess.fileLastModified(storageScope, context, filepath!!)
} catch (e: SecurityException) {
0L
}
}
fun fileGetPosition(fileId: Int): Long {
if (!hasFileId(fileId)) {
return 0L
}
return files[fileId].position()
}
fun isFileEof(fileId: Int): Boolean {
if (!hasFileId(fileId)) {
return false
}
return files[fileId].endOfFile
}
fun fileClose(fileId: Int) {
if (hasFileId(fileId)) {
files[fileId].close()
files.remove(fileId)
}
}
}

View File

@@ -0,0 +1,93 @@
/*************************************************************************/
/* FileData.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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 java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
/**
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
*/
internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
companion object {
private val TAG = FileData::class.java.simpleName
fun fileExists(path: String): Boolean {
return try {
File(path).isFile
} catch (e: SecurityException) {
false
}
}
fun fileLastModified(filepath: String): Long {
return try {
File(filepath).lastModified()
} catch (e: SecurityException) {
0L
}
}
fun delete(filepath: String): Boolean {
return try {
File(filepath).delete()
} catch (e: Exception) {
false
}
}
fun rename(from: String, to: String): Boolean {
return try {
val fromFile = File(from)
fromFile.renameTo(File(to))
} catch (e: Exception) {
false
}
}
}
override val fileChannel: FileChannel
init {
if (accessFlag == FileAccessFlags.WRITE) {
fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
} else {
fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
}
if (accessFlag.shouldTruncate()) {
fileChannel.truncate(0)
}
}
}

View File

@@ -0,0 +1,284 @@
/*************************************************************************/
/* MediaStoreData.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* 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.ContentUris
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.nio.channels.FileChannel
/**
* Implementation of [DataAccess] which handles access and interactions with file and data
* under scoped storage via the MediaStore API.
*/
@RequiresApi(Build.VERSION_CODES.Q)
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
DataAccess(filePath) {
private data class DataItem(
val id: Long,
val uri: Uri,
val displayName: String,
val relativePath: String,
val size: Int,
val dateModified: Int,
val mediaType: Int
)
companion object {
private val TAG = MediaStoreData::class.java.simpleName
private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
private val PROJECTION = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns.RELATIVE_PATH,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_MODIFIED,
MediaStore.Files.FileColumns.MEDIA_TYPE,
)
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
private fun getSelectionByPathArguments(path: String): Array<String> {
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
}
private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? "
private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString())
private fun getMediaStoreDisplayName(path: String) = File(path).name
private fun getMediaStoreRelativePath(path: String): String {
val pathFile = File(path)
val environmentDir = Environment.getExternalStorageDirectory()
var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/')
if (relativePath.isNotBlank()) {
relativePath += "/"
}
return relativePath
}
private fun queryById(context: Context, id: Long): List<DataItem> {
val query = context.contentResolver.query(
COLLECTION,
PROJECTION,
SELECTION_BY_ID,
getSelectionByIdArgument(id),
null
)
return dataItemFromCursor(query)
}
private fun queryByPath(context: Context, path: String): List<DataItem> {
val query = context.contentResolver.query(
COLLECTION,
PROJECTION,
SELECTION_BY_PATH,
getSelectionByPathArguments(path),
null
)
return dataItemFromCursor(query)
}
private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
query?.use { cursor ->
cursor.count
if (cursor.count == 0) {
return emptyList()
}
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
val relativePathColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val result = ArrayList<DataItem>()
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
result.add(
DataItem(
id,
ContentUris.withAppendedId(COLLECTION, id),
cursor.getString(displayNameColumn),
cursor.getString(relativePathColumn),
cursor.getInt(sizeColumn),
cursor.getInt(dateModifiedColumn),
cursor.getInt(mediaTypeColumn)
)
)
}
return result
}
return emptyList()
}
private fun addFile(context: Context, path: String): DataItem? {
val fileDetails = ContentValues().apply {
put(MediaStore.Files.FileColumns._ID, 0)
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path))
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path))
}
context.contentResolver.insert(COLLECTION, fileDetails) ?: return null
// File was successfully added, let's retrieve its info
val infos = queryByPath(context, path)
if (infos.isEmpty()) {
return null
}
return infos[0]
}
fun delete(context: Context, path: String): Boolean {
val itemsToDelete = queryByPath(context, path)
if (itemsToDelete.isEmpty()) {
return false
}
val resolver = context.contentResolver
var itemsDeleted = 0
for (item in itemsToDelete) {
itemsDeleted += resolver.delete(item.uri, null, null)
}
return itemsDeleted > 0
}
fun fileExists(context: Context, path: String): Boolean {
return queryByPath(context, path).isNotEmpty()
}
fun fileLastModified(context: Context, path: String): Long {
val result = queryByPath(context, path)
if (result.isEmpty()) {
return 0L
}
val dataItem = result[0]
return dataItem.dateModified.toLong()
}
fun rename(context: Context, from: String, to: String): Boolean {
// Ensure the source exists.
val sources = queryByPath(context, from)
if (sources.isEmpty()) {
return false
}
// Take the first source
val source = sources[0]
// Set up the updated values
val updatedDetails = ContentValues().apply {
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to))
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to))
}
val updated = context.contentResolver.update(
source.uri,
updatedDetails,
SELECTION_BY_ID,
getSelectionByIdArgument(source.id)
)
return updated > 0
}
}
private val id: Long
private val uri: Uri
override val fileChannel: FileChannel
init {
val contentResolver = context.contentResolver
val dataItems = queryByPath(context, filePath)
val dataItem = when (accessFlag) {
FileAccessFlags.READ -> {
// The file should already exist
if (dataItems.isEmpty()) {
throw FileNotFoundException("Unable to access file $filePath")
}
val dataItem = dataItems[0]
dataItem
}
FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> {
// Create the file if it doesn't exist
val dataItem = if (dataItems.isEmpty()) {
addFile(context, filePath)
} else {
dataItems[0]
}
if (dataItem == null) {
throw FileNotFoundException("Unable to access file $filePath")
}
dataItem
}
}
id = dataItem.id
uri = dataItem.uri
val parcelFileDescriptor = 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)
}
}
}

View File

@@ -32,10 +32,14 @@ package org.godotengine.godot.utils;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
import android.util.Log;
import androidx.core.content.ContextCompat;
@@ -53,7 +57,8 @@ public final class PermissionsUtil {
static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
static final int REQUEST_CAMERA_PERMISSION = 2;
static final int REQUEST_VIBRATE_PERMISSION = 3;
static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
private PermissionsUtil() {
}
@@ -108,13 +113,26 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return true;
List<String> dangerousPermissions = new ArrayList<>();
List<String> requestedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
dangerousPermissions.add(manifestPermission);
if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
} catch (Exception ignored) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
}
}
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
requestedPermissions.add(manifestPermission);
}
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -122,13 +140,12 @@ public final class PermissionsUtil {
}
}
if (dangerousPermissions.isEmpty()) {
if (requestedPermissions.isEmpty()) {
// If list is empty, all of dangerous permissions were granted.
return true;
}
String[] requestedPermissions = dangerousPermissions.toArray(new String[0]);
activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE);
activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
return false;
}
@@ -148,13 +165,19 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return manifestPermissions;
List<String> dangerousPermissions = new ArrayList<>();
List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
dangerousPermissions.add(manifestPermission);
if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
grantedPermissions.add(manifestPermission);
}
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(manifestPermission);
}
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@@ -162,7 +185,7 @@ public final class PermissionsUtil {
}
}
return dangerousPermissions.toArray(new String[0]);
return grantedPermissions.toArray(new String[0]);
}
/**
@@ -177,7 +200,7 @@ public final class PermissionsUtil {
if (permission.equals(p))
return true;
}
} catch (PackageManager.NameNotFoundException e) {
} catch (PackageManager.NameNotFoundException ignored) {
}
return false;