mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-06-05 22:30:29 +00:00
Merge 0342340621 into fa2a726cc3
This commit is contained in:
commit
dbdf703097
5 changed files with 764 additions and 25 deletions
|
|
@ -14,6 +14,7 @@ import android.content.pm.ActivityInfo;
|
|||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
|
|
@ -51,6 +52,9 @@ import android.widget.Toast;
|
|||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Locale;
|
||||
|
||||
|
|
@ -559,6 +563,13 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
Log.v(TAG, "onResume()");
|
||||
super.onResume();
|
||||
|
||||
/*
|
||||
Clear the SAF cache as the user may have edited files while
|
||||
app was paused. This is far from a perfect check, but good
|
||||
enough for our use case.
|
||||
*/
|
||||
SAFDocument.requestCacheInvalidation();
|
||||
|
||||
if (mHIDDeviceManager != null) {
|
||||
mHIDDeviceManager.setFrozen(false);
|
||||
}
|
||||
|
|
@ -787,13 +798,18 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
} else {
|
||||
/* If the user selected a directory and the persistent permission hint has been set,
|
||||
make the permission persistable */
|
||||
if (mFileDialogState.type == SDL_FILEDIALOG_OPENFOLDER && mFileDialogState.persistable) {
|
||||
mSingleton.getContentResolver().takePersistableUriPermission(singleFileUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (mFileDialogState.type == SDL_FILEDIALOG_OPENFOLDER) {
|
||||
if (mFileDialogState.persistable) {
|
||||
mSingleton.getContentResolver().takePersistableUriPermission(singleFileUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
}
|
||||
/* Add an empty fragment to the URI to mark it as generated by SDL */
|
||||
filelist = new String[]{singleFileUri + "#"};
|
||||
} else {
|
||||
/* Only one file is selected. */
|
||||
filelist = new String[]{singleFileUri.toString()};
|
||||
}
|
||||
/* Only one file is selected. */
|
||||
filelist = new String[]{singleFileUri.toString()};
|
||||
}
|
||||
} else {
|
||||
/* User cancelled the request. */
|
||||
|
|
@ -2101,21 +2117,343 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
return true;
|
||||
}
|
||||
|
||||
public static class SAFDocument {
|
||||
static private final HashMap<String, SAFDocument> cache = new HashMap<>();
|
||||
static private boolean cacheInvalidationRequested = false;
|
||||
static public void requestCacheInvalidation() {
|
||||
cacheInvalidationRequested = true;
|
||||
}
|
||||
|
||||
private boolean dirty;
|
||||
private final String id;
|
||||
public final boolean isDirectory;
|
||||
public long lastModified;
|
||||
public long size;
|
||||
public final Uri uri;
|
||||
private ArrayList<String> children;
|
||||
|
||||
private SAFDocument(Cursor cursor, Uri uri, boolean uri_is_tree) {
|
||||
this.id = cursor.getString(0);
|
||||
this.lastModified = cursor.getLong(3);
|
||||
this.size = cursor.getLong(4);
|
||||
this.isDirectory = cursor.getString(2).equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
this.uri = uri_is_tree ? DocumentsContract.buildDocumentUriUsingTree(uri, this.id) : uri;
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
private SAFDocument(Uri tree) {
|
||||
this.id = DocumentsContract.getTreeDocumentId(tree);
|
||||
this.isDirectory = true;
|
||||
this.uri = DocumentsContract.buildDocumentUriUsingTree(tree, this.id);
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private SAFDocument(Uri uri, Uri tree, String mimeType)
|
||||
{
|
||||
this.id = DocumentsContract.getDocumentId(uri);
|
||||
this.isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
this.lastModified = System.currentTimeMillis();
|
||||
this.uri = DocumentsContract.buildDocumentUriUsingTree(tree, this.id);
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
static private final String[] queryColumns = new String[] {
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||
DocumentsContract.Document.COLUMN_SIZE
|
||||
};
|
||||
|
||||
static private final String newDocumentMimeType = "application/octet-stream";
|
||||
|
||||
private static SAFDocument getTree(Uri uri) {
|
||||
Uri tree = uri.buildUpon().fragment(null).build();
|
||||
|
||||
SAFDocument document = SAFDocument.cache.get(tree.toString());
|
||||
if (document == null) {
|
||||
document = new SAFDocument(tree);
|
||||
cache.put(tree.toString(), document);
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
private static SAFDocument fromUri(Uri uri) throws FileNotFoundException {
|
||||
String document_id;
|
||||
|
||||
try {
|
||||
document_id = DocumentsContract.getDocumentId(uri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
/* Tree URIs get special handling */
|
||||
return SAFDocument.getTree(uri);
|
||||
}
|
||||
|
||||
String tree_id;
|
||||
|
||||
try {
|
||||
tree_id = DocumentsContract.getTreeDocumentId(uri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
tree_id = null;
|
||||
}
|
||||
|
||||
String cache_key;
|
||||
|
||||
if (tree_id != null) {
|
||||
cache_key = uri.getAuthority() + "#" + tree_id + "/" + document_id;
|
||||
} else {
|
||||
cache_key = uri.getAuthority() + "#" + document_id;
|
||||
}
|
||||
|
||||
SAFDocument document = SAFDocument.cache.get(cache_key);
|
||||
if (document != null) {
|
||||
return document;
|
||||
}
|
||||
|
||||
Cursor cursor = mSingleton.getContentResolver().query(uri, SAFDocument.queryColumns,
|
||||
null, null, null);
|
||||
|
||||
if (cursor == null) {
|
||||
throw new FileNotFoundException("The URI " + uri + " is not valid");
|
||||
}
|
||||
|
||||
cursor.moveToFirst();
|
||||
document = new SAFDocument(cursor, uri, false);
|
||||
cursor.close();
|
||||
|
||||
SAFDocument.cache.put(cache_key, document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static SAFDocument fromPath(Uri uri, String[] pathSegments) throws FileNotFoundException {
|
||||
SAFDocument document = SAFDocument.cache.get(uri.toString());
|
||||
|
||||
if (document != null) {
|
||||
return document;
|
||||
}
|
||||
|
||||
document = SAFDocument.getTree(uri);
|
||||
|
||||
String createdPath = document.getTree() + "#";
|
||||
int prefix = createdPath.length();
|
||||
|
||||
for (String segment : pathSegments) {
|
||||
if (segment.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
document.getChildren(createdPath.substring(prefix));
|
||||
|
||||
if (createdPath.endsWith("#")) {
|
||||
createdPath += segment;
|
||||
} else {
|
||||
createdPath += "/" + segment;
|
||||
}
|
||||
|
||||
document = SAFDocument.cache.get(createdPath);
|
||||
|
||||
if (document == null) {
|
||||
throw new FileNotFoundException("Failed to resolve path segment \"" + segment + "\" in path \"" + String.join("/", pathSegments) + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (document.dirty) {
|
||||
document.update();
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public static SAFDocument get(Uri uri) throws FileNotFoundException {
|
||||
if (cacheInvalidationRequested) {
|
||||
cache.clear();
|
||||
cacheInvalidationRequested = false;
|
||||
}
|
||||
|
||||
String fragment = uri.getFragment();
|
||||
|
||||
/* Original "content://" URI, parse directly */
|
||||
if (fragment == null) {
|
||||
return SAFDocument.fromUri(uri);
|
||||
}
|
||||
|
||||
/* SDL "content://" URI, with "#path" at the end, parse by directory */
|
||||
return SAFDocument.fromPath(uri, fragment.split("/"));
|
||||
}
|
||||
|
||||
public static SAFDocument create(Uri uri) throws IllegalArgumentException, FileNotFoundException {
|
||||
if (uri.getFragment() == null) {
|
||||
throw new IllegalArgumentException("Not an SDL generated URI to get a parent");
|
||||
}
|
||||
|
||||
/* It's safe to assume this method is only called when the provided URI is confirmed as not existing */
|
||||
ArrayList<String> path = new ArrayList<>(Arrays.asList(uri.getFragment().split("/")));
|
||||
path.removeAll(Collections.singleton(""));
|
||||
if (path.isEmpty()) {
|
||||
/* If original path is "" and it does not exist, something went very wrong */
|
||||
throw new IllegalArgumentException("Invalid path for document creation: " + uri.getFragment());
|
||||
}
|
||||
|
||||
/* Separate the new file name and the parent path, then create the new file in the parent directory */
|
||||
String newFileName = path.get(path.size() - 1);
|
||||
path.remove(path.size() - 1);
|
||||
SAFDocument parent = SAFDocument.fromPath(uri, path.toArray(new String[0]));
|
||||
|
||||
Uri tree = parent.getTree();
|
||||
|
||||
Uri newFileUri = DocumentsContract.createDocument(mSingleton.getContentResolver(),
|
||||
parent.uri, SAFDocument.newDocumentMimeType, newFileName);
|
||||
if (newFileUri == null) {
|
||||
throw new IllegalArgumentException("Unable to create a new file for writing");
|
||||
}
|
||||
|
||||
SAFDocument document = new SAFDocument(newFileUri, tree, SAFDocument.newDocumentMimeType);
|
||||
parent.children.add(newFileName);
|
||||
SAFDocument.cache.put(tree + "#" + uri.getFragment(), document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private ArrayList<String> getChildren(String path) throws FileNotFoundException {
|
||||
Uri tree = this.getTree();
|
||||
|
||||
if (!this.isDirectory || tree == null) {
|
||||
throw new FileNotFoundException(this.id + " is not a directory for uri: " + this.uri);
|
||||
}
|
||||
if (this.children != null) {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
Uri children = DocumentsContract.buildChildDocumentsUriUsingTree(tree, this.id);
|
||||
|
||||
Cursor cursor = mSingleton.getContentResolver().query(children, queryColumns,
|
||||
null, null, null);
|
||||
|
||||
if (cursor == null) {
|
||||
throw new FileNotFoundException("The URI " + uri + " is not valid");
|
||||
}
|
||||
|
||||
String cache_key_prefix;
|
||||
|
||||
if (path == null) {
|
||||
cache_key_prefix = this.uri.getAuthority() + "#" +
|
||||
DocumentsContract.getTreeDocumentId(tree) + "/";
|
||||
} else {
|
||||
if (path.isEmpty()) {
|
||||
cache_key_prefix = tree + "#";
|
||||
} else {
|
||||
cache_key_prefix = tree + "#" + path + "/";
|
||||
}
|
||||
}
|
||||
|
||||
this.children = new ArrayList<>();
|
||||
|
||||
/* Get the directory contents */
|
||||
while (cursor.moveToNext()) {
|
||||
SAFDocument document = new SAFDocument(cursor, tree, true);
|
||||
String name = cursor.getString(1);
|
||||
this.children.add(name);
|
||||
|
||||
/* Cache the result */
|
||||
SAFDocument.cache.put(cache_key_prefix +
|
||||
(path == null ? document.id : name), document);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return this.children;
|
||||
}
|
||||
|
||||
private Uri getTree() {
|
||||
try {
|
||||
return DocumentsContract.buildTreeDocumentUri(this.uri.getAuthority(),
|
||||
DocumentsContract.getTreeDocumentId(this.uri));
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void requestUpdate() {
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
Cursor cursor = mSingleton.getContentResolver().query(this.uri, SAFDocument.queryColumns,
|
||||
null, null, null);
|
||||
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.moveToFirst();
|
||||
this.lastModified = cursor.getLong(3);
|
||||
this.size = cursor.getLong(4);
|
||||
cursor.close();
|
||||
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return this.uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int openFileDescriptor(String uri, String mode) throws Exception {
|
||||
public static String[] getSAFDocumentChildrenNames(String uri) throws FileNotFoundException {
|
||||
if (mSingleton == null) {
|
||||
return -1;
|
||||
throw new IllegalStateException("SDLActivity is not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode);
|
||||
return pfd != null ? pfd.detachFd() : -1;
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
if (uri.endsWith("/")) {
|
||||
uri = uri.substring(0, uri.length() - 1);
|
||||
} else if (uri.endsWith("%2F")) {
|
||||
uri = uri.substring(0, uri.length() - 3);
|
||||
}
|
||||
|
||||
Uri parsedUri = Uri.parse(uri);
|
||||
return SAFDocument.get(parsedUri).getChildren(parsedUri.getFragment()).toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static SAFDocument getSAFDocument(String uri) throws FileNotFoundException {
|
||||
if (mSingleton == null) {
|
||||
throw new IllegalStateException("SDLActivity is not initialized");
|
||||
}
|
||||
|
||||
return SAFDocument.get(Uri.parse(uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
public static int openFileDescriptor(String uri, String mode, boolean no_overwrite) throws FileNotFoundException, IllegalAccessException {
|
||||
if (mSingleton == null) {
|
||||
throw new IllegalStateException("SDLActivity is not initialized");
|
||||
}
|
||||
|
||||
Uri contentUri = Uri.parse(uri);
|
||||
try {
|
||||
SAFDocument document = SAFDocument.get(contentUri);
|
||||
contentUri = document.uri;
|
||||
if (mode.contains("w")) {
|
||||
if (no_overwrite) {
|
||||
throw new IllegalAccessException("The requested file \"" + uri + "\" already exists.");
|
||||
}
|
||||
document.requestUpdate();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
if (!mode.contains("w")) {
|
||||
throw e;
|
||||
}
|
||||
/* Maybe we want to create a new file and write to it? */
|
||||
contentUri = SAFDocument.create(contentUri).uri;
|
||||
}
|
||||
|
||||
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(contentUri, mode);
|
||||
return pfd != null ? pfd.detachFd() : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2154,8 +2492,12 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
if (initialPath != null && !initialPath.isEmpty()) {
|
||||
try {
|
||||
initialPathUri = Uri.parse(initialPath);
|
||||
if (initialPathUri.getFragment() != null) {
|
||||
initialPathUri = SAFDocument.get(initialPathUri).uri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to parse initial path URI, ignoring initial path", e);
|
||||
initialPathUri = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2205,10 +2547,13 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|||
intent.addFlags(intent_flags);
|
||||
}
|
||||
|
||||
if (initialPathUri != null) {
|
||||
if (initialPathUri != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialPathUri);
|
||||
}
|
||||
|
||||
intent.putExtra("android.content.extra.FANCY", true);
|
||||
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
|
||||
|
||||
/* Display the file/folder dialog */
|
||||
try {
|
||||
mSingleton.startActivityForResult(intent, requestCode);
|
||||
|
|
|
|||
|
|
@ -232,6 +232,20 @@ extern SDL_DECLSPEC void SDLCALL SDL_ShowSaveFileDialog(SDL_DialogFileCallback c
|
|||
* requires an event-handling loop. Apps that do not use SDL to handle events
|
||||
* should add a call to SDL_PumpEvents in their main loop.
|
||||
*
|
||||
* On Android, if folder selection is successful, the callback will receive a
|
||||
* custom `content://` URI with a `#` at the end. These custom URI's can have
|
||||
* a regular path, separated by either forward or backward slashes, after the
|
||||
* `#`.
|
||||
*
|
||||
* This allows the user to treat the selected folder as a regular filesystem
|
||||
* base directory, and access its contents with the usual SDL file/directory
|
||||
* I/O functions. Internally, the path after the `#` will be properly converted
|
||||
* to the corresponding child document URI when needed.
|
||||
*
|
||||
* Both regular `content://` URIs and the ones with a `#<path>` at the end
|
||||
* are supported by SDL_IOFromFile() with appropriate modes, as well as
|
||||
* SDL_EnumerateDirectory() and SDL_GetPathInfo().
|
||||
*
|
||||
* \param callback a function pointer to be invoked when the user selects a
|
||||
* file and accepts, or cancels the dialog, or an error
|
||||
* occurs.
|
||||
|
|
@ -249,6 +263,7 @@ extern SDL_DECLSPEC void SDLCALL SDL_ShowSaveFileDialog(SDL_DialogFileCallback c
|
|||
* different one, depending on the OS's constraints.
|
||||
*
|
||||
* \since This function is available since SDL 3.2.0.
|
||||
* On Android, the function is implemented since SDL 3.6.0.
|
||||
*
|
||||
* \sa SDL_DialogFileCallback
|
||||
* \sa SDL_ShowOpenFileDialog
|
||||
|
|
|
|||
|
|
@ -393,6 +393,8 @@ static jmethodID midShowTextInput;
|
|||
static jmethodID midSupportsRelativeMouse;
|
||||
static jmethodID midOpenFileDescriptor;
|
||||
static jmethodID midShowFileDialog;
|
||||
static jmethodID midGetDocumentChildrenNames;
|
||||
static jmethodID midGetSAFDocument;
|
||||
static jmethodID midGetPreferredLocales;
|
||||
|
||||
// audio manager
|
||||
|
|
@ -427,6 +429,13 @@ static void Internal_Android_Destroy_AssetManager(void);
|
|||
static AAssetManager *asset_manager = NULL;
|
||||
static jobject javaAssetManagerRef = 0;
|
||||
|
||||
// Android content file handling
|
||||
static jclass mSAFDocumentClass = NULL;
|
||||
static jfieldID fidIsDirectory = NULL;
|
||||
static jfieldID fidLastModified = NULL;
|
||||
static jfieldID fidSize = NULL;
|
||||
static jmethodID midGetUri = NULL;
|
||||
|
||||
static SDL_Mutex *Android_ActivityMutex = NULL;
|
||||
static SDL_Mutex *Android_LifecycleMutex = NULL;
|
||||
static SDL_Semaphore *Android_LifecycleEventSem = NULL;
|
||||
|
|
@ -680,8 +689,10 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
|
|||
midShouldMinimizeOnFocusLoss = (*env)->GetStaticMethodID(env, mActivityClass, "shouldMinimizeOnFocusLoss", "()Z");
|
||||
midShowTextInput = (*env)->GetStaticMethodID(env, mActivityClass, "showTextInput", "(IIIII)Z");
|
||||
midSupportsRelativeMouse = (*env)->GetStaticMethodID(env, mActivityClass, "supportsRelativeMouse", "()Z");
|
||||
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I");
|
||||
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;Z)I");
|
||||
midShowFileDialog = (*env)->GetStaticMethodID(env, mActivityClass, "showFileDialog", "([Ljava/lang/String;ZILjava/lang/String;I)Z");
|
||||
midGetDocumentChildrenNames = (*env)->GetStaticMethodID(env, mActivityClass, "getSAFDocumentChildrenNames", "(Ljava/lang/String;)[Ljava/lang/String;");
|
||||
midGetSAFDocument = (*env)->GetStaticMethodID(env, mActivityClass, "getSAFDocument", "(Ljava/lang/String;)Lorg/libsdl/app/SDLActivity$SAFDocument;");
|
||||
midGetPreferredLocales = (*env)->GetStaticMethodID(env, mActivityClass, "getPreferredLocales", "()Ljava/lang/String;");
|
||||
|
||||
if (!midClipboardGetText ||
|
||||
|
|
@ -714,6 +725,8 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
|
|||
!midSupportsRelativeMouse ||
|
||||
!midOpenFileDescriptor ||
|
||||
!midShowFileDialog ||
|
||||
!midGetDocumentChildrenNames ||
|
||||
!midGetSAFDocument ||
|
||||
!midGetPreferredLocales) {
|
||||
__android_log_print(ANDROID_LOG_WARN, "SDL", "Missing some Java callbacks, do you have the latest version of SDLActivity.java?");
|
||||
}
|
||||
|
|
@ -2461,6 +2474,345 @@ bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirector
|
|||
return (result != SDL_ENUM_FAILURE);
|
||||
}
|
||||
|
||||
static char *GetURIWithNormalizedPath(const char *uri, bool should_append_slash)
|
||||
{
|
||||
if (!uri) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *uri_cursor = SDL_strchr(uri, '#');
|
||||
|
||||
// Don't try to normalize real URI's or empty paths
|
||||
if (!uri_cursor || !uri_cursor[1]) {
|
||||
char *normalized = SDL_strdup(uri);
|
||||
if (!normalized) {
|
||||
SDL_SetError("Out of memory");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
uri_cursor++;
|
||||
|
||||
// Final size needs to account for a slash at the end and a null char after it
|
||||
char *normalized = SDL_malloc(SDL_strlen(uri) + should_append_slash + 1);
|
||||
if (!normalized) {
|
||||
SDL_SetError("Out of memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Copy "content://<tree>#" to the normalized string
|
||||
SDL_strlcpy(normalized, uri, uri_cursor - uri + 1);
|
||||
char *path = SDL_strchr(normalized, '#') + 1;
|
||||
char *path_cursor = path;
|
||||
|
||||
while (*uri_cursor) {
|
||||
// Skip empty path segments (double slashes)
|
||||
if (*uri_cursor == '/' || *uri_cursor == '\\') {
|
||||
uri_cursor++;
|
||||
continue;
|
||||
}
|
||||
// Single dot path segment, skip
|
||||
if (uri_cursor[0] == '.' &&
|
||||
(uri_cursor[1] == '\0' || uri_cursor[1] == '/' || uri_cursor[1] == '\\')) {
|
||||
uri_cursor++;
|
||||
continue;
|
||||
}
|
||||
// Double dot path segment, remove the previous segment from the normalized path or
|
||||
// fail if there is no previous segment to remove (attempting to traverse over the root path)
|
||||
if (uri_cursor[0] == '.' && uri_cursor[1] == '.' &&
|
||||
(uri_cursor[2] == '\0' || uri_cursor[2] == '/' || uri_cursor[2] == '\\')) {
|
||||
if (path_cursor == path) {
|
||||
SDL_SetError("Traversing over the root path is not allowed. Path: %s",
|
||||
SDL_strchr(uri, '#') + 1);
|
||||
SDL_free(normalized);
|
||||
return NULL;
|
||||
}
|
||||
path_cursor--;
|
||||
// Move the path cursor back to the previous path segment,
|
||||
// effectively removing the last segment from the normalized path
|
||||
while (path_cursor != path && *(path_cursor - 1) != '/') {
|
||||
*path_cursor = 0;
|
||||
path_cursor--;
|
||||
}
|
||||
uri_cursor += 2;
|
||||
continue;
|
||||
}
|
||||
// Copy the current path segment from the URI to the normalized path until the next slash or end of string
|
||||
while (*uri_cursor && *uri_cursor != '/' && *uri_cursor != '\\') {
|
||||
*path_cursor++ = *uri_cursor++;
|
||||
}
|
||||
// Append a slash to the normalized path if we already have a path segment
|
||||
if (path_cursor != path) {
|
||||
*path_cursor++ = '/';
|
||||
}
|
||||
}
|
||||
// Remove the trailing slash if it was not requested
|
||||
if (!should_append_slash && *(path_cursor -1) == '/') {
|
||||
path_cursor--;
|
||||
}
|
||||
*path_cursor = '\0';
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
static bool CreateSAFDocumentClass(JNIEnv *env, jobject jSAFDocument)
|
||||
{
|
||||
if (mSAFDocumentClass && fidIsDirectory && fidLastModified && fidSize && midGetUri) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!env || !jSAFDocument) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mSAFDocumentClass) {
|
||||
jclass jLocalSAFDocumentClass = (*env)->GetObjectClass(env, jSAFDocument);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mSAFDocumentClass = (*env)->NewGlobalRef(env, jLocalSAFDocumentClass);
|
||||
(*env)->DeleteLocalRef(env, jLocalSAFDocumentClass);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fidIsDirectory) {
|
||||
fidIsDirectory = (*env)->GetFieldID(env, mSAFDocumentClass, "isDirectory", "Z");
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fidLastModified) {
|
||||
fidLastModified = (*env)->GetFieldID(env, mSAFDocumentClass, "lastModified", "J");
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fidSize) {
|
||||
fidSize = (*env)->GetFieldID(env, mSAFDocumentClass, "size", "J");
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!midGetUri) {
|
||||
midGetUri = (*env)->GetMethodID(env, mSAFDocumentClass, "getUri", "()Ljava/lang/String;");
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static char *GetDocumentURIFromTreeURI(JNIEnv *env, const char *uri)
|
||||
{
|
||||
// Set "uri" parameter for JNI call
|
||||
jstring juri = (*env)->NewStringUTF(env, uri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Invoke JNI
|
||||
jobject jSAFDocument = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetSAFDocument, juri);
|
||||
|
||||
(*env)->DeleteLocalRef(env, juri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!CreateSAFDocumentClass(env, jSAFDocument)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the uri from the SAFDocument
|
||||
jstring jdocument_uri = (*env)->CallObjectMethod(env, jSAFDocument, midGetUri);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *document_uri = (*env)->GetStringUTFChars(env, jdocument_uri, NULL);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// We need to reserve space for an encoded separator ("%2F") at the end
|
||||
char *final_uri = SDL_calloc(SDL_strlen(document_uri) + 4, sizeof(char));
|
||||
if (!final_uri) {
|
||||
return NULL;
|
||||
}
|
||||
size_t offset = SDL_strlcpy(final_uri, document_uri, SDL_strlen(document_uri) + 1);
|
||||
SDL_strlcpy(final_uri + offset, "%2F", 4);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jdocument_uri, document_uri);
|
||||
(*env)->DeleteLocalRef(env, jdocument_uri);
|
||||
(*env)->DeleteLocalRef(env, jSAFDocument);
|
||||
|
||||
return final_uri;
|
||||
}
|
||||
|
||||
bool Android_JNI_EnumerateContentDirectory(const char *uri, SDL_EnumerateDirectoryCallback cb, void *userdata)
|
||||
{
|
||||
SDL_assert(uri != NULL);
|
||||
SDL_assert(SDL_strncmp(uri, "content://", 10) == 0);
|
||||
|
||||
struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION);
|
||||
JNIEnv *env = Android_JNI_GetEnv();
|
||||
|
||||
if (!LocalReferenceHolder_Init(&refs, env)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
char *dirname = NULL;
|
||||
|
||||
// If we are traversing a genuine tree URI and not an SDL generated one, the "dirname"
|
||||
// parameter on the enumeration callback must provide the document URI, not the tree URI.
|
||||
// Because of that, if we're using a tree URI, we need to fetch the document URI from Java.
|
||||
if (!SDL_strchr(uri, '#')) {
|
||||
dirname = GetDocumentURIFromTreeURI(env, uri);
|
||||
} else {
|
||||
dirname = GetURIWithNormalizedPath(uri, true);
|
||||
}
|
||||
|
||||
if (!dirname) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set "uri" parameter for JNI call
|
||||
jstring juri = (*env)->NewStringUTF(env, dirname);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
SDL_free(dirname);
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invoke JNI
|
||||
jobjectArray jFileNamesArray = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetDocumentChildrenNames, juri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
SDL_free(dirname);
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
jsize length = (*env)->GetArrayLength(env, jFileNamesArray);
|
||||
bool success = true;
|
||||
|
||||
for (int i = 0; i < length; ++i) {
|
||||
jstring jFileName = (*env)->GetObjectArrayElement(env, jFileNamesArray, i);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const char *filename = (*env)->GetStringUTFChars(env, jFileName, NULL);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_EnumerationResult result = cb(userdata, dirname, filename);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jFileName, filename);
|
||||
(*env)->DeleteLocalRef(env, jFileName);
|
||||
|
||||
if (result != SDL_ENUM_CONTINUE) {
|
||||
success = result == SDL_ENUM_SUCCESS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_free(dirname);
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool Android_JNI_GetContentInfo(const char *uri, SDL_PathInfo *info)
|
||||
{
|
||||
SDL_assert(uri != NULL);
|
||||
SDL_assert(SDL_strncmp(uri, "content://", 10) == 0);
|
||||
|
||||
struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION);
|
||||
JNIEnv *env = Android_JNI_GetEnv();
|
||||
|
||||
if (!LocalReferenceHolder_Init(&refs, env)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
char *normalizedUri = GetURIWithNormalizedPath(uri, false);
|
||||
if (!normalizedUri) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set "uri" parameter for JNI call
|
||||
jstring juri = (*env)->NewStringUTF(env, normalizedUri);
|
||||
SDL_free(normalizedUri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invoke JNI
|
||||
jobject jSAFDocument = (*env)->CallStaticObjectMethod(env, mActivityClass, midGetSAFDocument, juri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateSAFDocumentClass(env, jSAFDocument)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the fields from the SAFDocument object
|
||||
bool is_directory = (*env)->GetBooleanField(env, jSAFDocument, fidIsDirectory);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
jlong last_modified = (*env)->GetLongField(env, jSAFDocument, fidLastModified);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
jlong size = (*env)->GetLongField(env, jSAFDocument, fidSize);
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate the SDL_PathInfo structure
|
||||
info->type = is_directory ? SDL_PATHTYPE_DIRECTORY : SDL_PATHTYPE_FILE;
|
||||
info->modify_time = (SDL_Time)(last_modified / 1000); // Convert milliseconds to seconds
|
||||
info->size = (Uint64)size;
|
||||
|
||||
// Android doesn't provide separate create and access times, so we use modify time for those as well.
|
||||
info->create_time = info->modify_time;
|
||||
info->access_time = info->modify_time;
|
||||
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info)
|
||||
{
|
||||
SDL_assert(path != NULL);
|
||||
|
|
@ -3286,6 +3638,7 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
|
|||
{
|
||||
// Get fopen-style modes
|
||||
int moderead = 0, modewrite = 0, modeappend = 0, modeupdate = 0;
|
||||
jboolean no_overwrite = JNI_FALSE;
|
||||
|
||||
for (const char *cmode = mode; *cmode; cmode++) {
|
||||
switch (*cmode) {
|
||||
|
|
@ -3301,6 +3654,9 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
|
|||
case '+':
|
||||
modeupdate = 1;
|
||||
break;
|
||||
case 'x':
|
||||
no_overwrite = JNI_TRUE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -3322,17 +3678,32 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
|
|||
contentResolverMode = modeupdate ? "rw" : "wa";
|
||||
}
|
||||
|
||||
JNIEnv *env = Android_JNI_GetEnv();
|
||||
jstring jstringUri = (*env)->NewStringUTF(env, uri);
|
||||
jstring jstringMode = (*env)->NewStringUTF(env, contentResolverMode);
|
||||
jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode);
|
||||
(*env)->DeleteLocalRef(env, jstringUri);
|
||||
(*env)->DeleteLocalRef(env, jstringMode);
|
||||
char *normalizedUri = GetURIWithNormalizedPath(uri, false);
|
||||
if (!normalizedUri) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fd == -1) {
|
||||
JNIEnv *env = Android_JNI_GetEnv();
|
||||
struct LocalReferenceHolder refs = LocalReferenceHolder_Setup(SDL_FUNCTION);
|
||||
if (!LocalReferenceHolder_Init(&refs, env)) {
|
||||
SDL_free(normalizedUri);
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return -1;
|
||||
}
|
||||
|
||||
jstring jstringUri = (*env)->NewStringUTF(env, normalizedUri);
|
||||
jstring jstringMode = (*env)->NewStringUTF(env, contentResolverMode);
|
||||
jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode, no_overwrite);
|
||||
|
||||
SDL_free(normalizedUri);
|
||||
|
||||
if (Android_JNI_ExceptionOccurred(false)) {
|
||||
fd = -1;
|
||||
} else if (fd == -1) {
|
||||
SDL_SetError("Unspecified error in JNI");
|
||||
}
|
||||
|
||||
LocalReferenceHolder_Cleanup(&refs);
|
||||
return fd;
|
||||
}
|
||||
|
||||
|
|
@ -3461,8 +3832,10 @@ bool Android_JNI_ShowFileDialog(
|
|||
|
||||
// Setup initial path
|
||||
jstring initialPathString = NULL;
|
||||
if (initialPath && *initialPath) {
|
||||
initialPathString = (*env)->NewStringUTF(env, initialPath);
|
||||
char *normalizedInitialPath = GetURIWithNormalizedPath(initialPath, false);
|
||||
if (normalizedInitialPath && *normalizedInitialPath) {
|
||||
initialPathString = (*env)->NewStringUTF(env, normalizedInitialPath);
|
||||
SDL_free(normalizedInitialPath);
|
||||
}
|
||||
|
||||
// Setup data
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ size_t Android_JNI_FileWrite(void *userdata, const void *buffer, size_t size, SD
|
|||
bool Android_JNI_FileClose(void *userdata);
|
||||
bool Android_JNI_EnumerateAssetDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata);
|
||||
bool Android_JNI_GetAssetPathInfo(const char *path, SDL_PathInfo *info);
|
||||
bool Android_JNI_EnumerateContentDirectory(const char *path, SDL_EnumerateDirectoryCallback cb, void *userdata);
|
||||
bool Android_JNI_GetContentInfo(const char *uri, SDL_PathInfo *info);
|
||||
|
||||
// Environment support
|
||||
void Android_JNI_GetManifestEnvironmentVariables(void);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ bool SDL_SYS_EnumerateDirectory(const char *path, SDL_EnumerateDirectoryCallback
|
|||
const bool retval = pathwithsep ? Android_JNI_EnumerateAssetDirectory(pathwithsep, cb, userdata) : false;
|
||||
SDL_free(pathwithsep);
|
||||
return retval;
|
||||
} else if (SDL_strncmp(path, "content://", 10) == 0) {
|
||||
return Android_JNI_EnumerateContentDirectory(path, cb, userdata);
|
||||
}
|
||||
SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
|
||||
#elif defined(SDL_PLATFORM_IOS)
|
||||
|
|
@ -356,6 +358,8 @@ bool SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
|
|||
rc = stat(path, &statbuf);
|
||||
} else if (SDL_strncmp(path, "assets://", 9) == 0) {
|
||||
return Android_JNI_GetAssetPathInfo(path, info);
|
||||
} else if (SDL_strncmp(path, "content://", 10) == 0) {
|
||||
return Android_JNI_GetContentInfo(path, info);
|
||||
} else {
|
||||
char *apath = NULL;
|
||||
SDL_asprintf(&apath, "%s/%s", SDL_GetAndroidInternalStoragePath(), path);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue