mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-06-05 22:30:29 +00:00
Android: add support for content:// directory traversal
This commit is contained in:
parent
d8f42ee069
commit
6d5ace46f9
5 changed files with 682 additions and 18 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;
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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