This commit is contained in:
José Cadete 2026-06-05 14:01:23 -06:00 committed by GitHub
commit dbdf703097
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 764 additions and 25 deletions

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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);