Android: add support for content:// directory traversal

This commit is contained in:
crudelios 2026-05-26 14:36:15 +01:00
parent d8f42ee069
commit 6d5ace46f9
5 changed files with 682 additions and 18 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;
@ -556,6 +560,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);
}
@ -775,13 +786,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. */
@ -2092,21 +2108,283 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
return true;
}
public static class SAFDocument {
static private final HashMap<Uri, SAFDocument> cache = new HashMap<>();
static private boolean cacheInvalidationRequested = false;
static public void requestCacheInvalidation() {
cacheInvalidationRequested = true;
}
private boolean dirty;
private final String id;
private final String mimeType;
private long lastModified;
private long size;
private final Uri uri;
private final Uri tree;
private HashMap <String, SAFDocument> children;
private SAFDocument(Cursor cursor, Uri uri) {
this.id = cursor.getString(0);
this.mimeType = cursor.getString(2);
this.lastModified = cursor.getLong(3);
this.size = cursor.getLong(4);
this.tree = uri;
this.uri = uri;
this.dirty = false;
}
private SAFDocument(Cursor cursor, SAFDocument parent) {
this.id = cursor.getString(0);
this.mimeType = cursor.getString(2);
this.lastModified = cursor.getLong(3);
this.size = cursor.getLong(4);
this.tree = parent.tree;
this.uri = DocumentsContract.buildDocumentUriUsingTree(this.tree, this.id);
this.dirty = false;
}
private SAFDocument(Uri tree) {
this.id = DocumentsContract.getTreeDocumentId(tree);
this.mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
this.tree = tree;
this.uri = DocumentsContract.buildDocumentUriUsingTree(this.tree, this.id);
this.dirty = true;
}
private SAFDocument(Uri uri, Uri tree, String mimeType)
{
this.id = DocumentsContract.getDocumentId(uri);
this.mimeType = mimeType;
this.lastModified = System.currentTimeMillis();
this.tree = tree;
this.uri = uri;
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();
if (cacheInvalidationRequested) {
cache.clear();
cacheInvalidationRequested = false;
}
SAFDocument treeInfo = cache.get(tree);
if (treeInfo == null) {
treeInfo = new SAFDocument(tree);
cache.put(tree, treeInfo);
}
return treeInfo;
}
private static SAFDocument fromUri(Uri uri) throws FileNotFoundException {
/* Tree URIs get special handling */
if (!DocumentsContract.isDocumentUri(mSingleton.getApplicationContext(), uri)) {
return new SAFDocument(uri);
}
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();
SAFDocument document = new SAFDocument(cursor, uri);
cursor.close();
return document;
}
private static SAFDocument fromPath(Uri uri, String[] pathSegments) throws FileNotFoundException {
SAFDocument document = SAFDocument.getTree(uri);
for (String segment : pathSegments) {
if (segment.isEmpty()) {
continue;
}
document = document.getChildren().get(segment);
if (document == null) {
throw new FileNotFoundException("Failed to resolve path segment \"" + segment + "\" in path \"" + String.join("/", pathSegments) + "\"");
}
}
return document;
}
public static SAFDocument get(Uri uri) throws FileNotFoundException {
/* Original "content://" URI, parse directly */
if (uri.getFragment() == null) {
return SAFDocument.fromUri(uri);
} else {
/* SDL "content://" URI, with "#path" at the end, parse by directory */
return SAFDocument.fromPath(uri, uri.getFragment().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 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 newFileInfo = new SAFDocument(newFileUri, parent.tree, SAFDocument.newDocumentMimeType);
parent.children.put(newFileName, newFileInfo);
return newFileInfo;
}
public HashMap <String, SAFDocument> getChildren() throws FileNotFoundException {
if (!this.isDirectory()) {
throw new FileNotFoundException(this.id + " is not a directory for uri: " + this.uri);
}
if (this.children != null) {
return this.children;
}
Uri children = DocumentsContract.buildChildDocumentsUriUsingTree(this.tree, this.id);
Cursor cursor = mSingleton.getContentResolver().query(children, queryColumns,
null, null, null);
if (cursor == null) {
throw new FileNotFoundException("The URI " + uri + " is not a valid");
}
this.children = new HashMap<>();
/* Get the directory contents */
while (cursor.moveToNext()) {
SAFDocument document = new SAFDocument(cursor, this);
this.children.put(cursor.getString(1), document);
}
cursor.close();
return this.children;
}
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 boolean isDirectory() {
return this.mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
}
public long getLastModified() {
if (this.dirty) {
this.update();
}
return this.lastModified;
}
public long getSize() {
if (this.dirty) {
this.update();
}
return this.size;
}
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;
return SAFDocument.get(Uri.parse(uri)).getChildren().keySet().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) throws FileNotFoundException {
if (mSingleton == null) {
throw new IllegalStateException("SDLActivity is not initialized");
}
Uri contentUri = Uri.parse(uri);
if (contentUri.getFragment() != null) {
try {
SAFDocument document = SAFDocument.get(contentUri);
contentUri = document.uri;
if (mode.contains("w")) {
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;
}
/**
@ -2145,8 +2423,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;
}
}
@ -2196,10 +2478,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

@ -398,6 +398,8 @@ static jmethodID midShowTextInput;
static jmethodID midSupportsRelativeMouse;
static jmethodID midOpenFileDescriptor;
static jmethodID midShowFileDialog;
static jmethodID midGetDocumentChildrenNames;
static jmethodID midGetSAFDocument;
static jmethodID midGetPreferredLocales;
// audio manager
@ -434,6 +436,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 jmethodID midIsDirectory = NULL;
static jmethodID midGetLastModified = NULL;
static jmethodID midGetSize = NULL;
static jmethodID midGetUri = NULL;
static SDL_Mutex *Android_ActivityMutex = NULL;
static SDL_Mutex *Android_LifecycleMutex = NULL;
static SDL_Semaphore *Android_LifecycleEventSem = NULL;
@ -689,6 +698,8 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetupJNI)(JNIEnv *env, jclass cl
midSupportsRelativeMouse = (*env)->GetStaticMethodID(env, mActivityClass, "supportsRelativeMouse", "()Z");
midOpenFileDescriptor = (*env)->GetStaticMethodID(env, mActivityClass, "openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)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 ||
@ -721,6 +732,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?");
}
@ -2495,6 +2508,338 @@ 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) {
if (*uri_cursor == '/' || *uri_cursor == '\\') {
uri_cursor++;
continue;
}
if (uri_cursor[0] == '.' &&
(uri_cursor[1] == '\0' || uri_cursor[1] == '/' || uri_cursor[1] == '\\')) {
uri_cursor++;
continue;
}
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--;
while (path_cursor != path && *(path_cursor - 1) != '/') {
*path_cursor = 0;
path_cursor--;
}
uri_cursor += 2;
continue;
}
while (*uri_cursor && *uri_cursor != '/' && *uri_cursor != '\\') {
*path_cursor++ = *uri_cursor++;
}
if (path_cursor != path) {
*path_cursor++ = '/';
}
}
if (!should_append_slash && *(path_cursor -1) == '/') {
path_cursor--;
}
*path_cursor = '\0';
return normalized;
}
static bool CreateSAFDocumentClass(JNIEnv *env, jobject jSAFDocument)
{
if (mSAFDocumentClass && midIsDirectory && midGetLastModified && midGetSize && 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 (!midIsDirectory) {
midIsDirectory = (*env)->GetMethodID(env, mSAFDocumentClass, "isDirectory", "()Z");
if (Android_JNI_ExceptionOccurred(false)) {
return false;
}
}
if (!midGetLastModified) {
midGetLastModified = (*env)->GetMethodID(env, mSAFDocumentClass, "getLastModified", "()J");
if (Android_JNI_ExceptionOccurred(false)) {
return false;
}
}
if (!midGetSize) {
midGetSize = (*env)->GetMethodID(env, mSAFDocumentClass, "getSize", "()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 = 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)) {
LocalReferenceHolder_Cleanup(&refs);
return false;
}
jsize length = (*env)->GetArrayLength(env, jFileNamesArray);
bool success = true;
// 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(dirname, '#')) {
SDL_free(dirname);
dirname = GetDocumentURIFromTreeURI(env, uri);
if (!dirname) {
LocalReferenceHolder_Cleanup(&refs);
return false;
}
}
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)->CallBooleanMethod(env, jSAFDocument, midIsDirectory);
if (Android_JNI_ExceptionOccurred(false)) {
LocalReferenceHolder_Cleanup(&refs);
return false;
}
long last_modified = (*env)->CallLongMethod(env, jSAFDocument, midGetLastModified);
if (Android_JNI_ExceptionOccurred(false)) {
LocalReferenceHolder_Cleanup(&refs);
return false;
}
long size = (*env)->CallLongMethod(env, jSAFDocument, midGetSize);
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);
@ -3320,12 +3665,23 @@ int Android_JNI_OpenFileDescriptor(const char *uri, const char *mode)
}
JNIEnv *env = Android_JNI_GetEnv();
jstring jstringUri = (*env)->NewStringUTF(env, uri);
char *normalizedUri = GetURIWithNormalizedPath(uri, false);
if (!normalizedUri) {
return -1;
}
jstring jstringUri = (*env)->NewStringUTF(env, normalizedUri);
jstring jstringMode = (*env)->NewStringUTF(env, contentResolverMode);
jint fd = (*env)->CallStaticIntMethod(env, mActivityClass, midOpenFileDescriptor, jstringUri, jstringMode);
SDL_free(normalizedUri);
(*env)->DeleteLocalRef(env, jstringUri);
(*env)->DeleteLocalRef(env, jstringMode);
if (Android_JNI_ExceptionOccurred(false)) {
return -1;
}
if (fd == -1) {
SDL_SetError("Unspecified error in JNI");
}
@ -3458,8 +3814,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

@ -89,6 +89,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);