Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libretro-common/include/vfs/vfs_implementation_saf.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ int retro_vfs_file_open_saf(const char *tree, const char *path, unsigned mode);

int retro_vfs_file_remove_saf(const char *tree, const char *path);

/*
* Rename a file, replacing the destination if it exists, returning 0 if successful or -1 if not.
* Both paths must be in the same tree and the same directory.
* The replacement is not atomic, but an existing destination is only deleted once the file has been moved into its place.
*/
int retro_vfs_file_rename_saf(const char *old_tree, const char *old_path, const char *new_tree, const char *new_path);

int retro_vfs_stat_saf(const char *tree, const char *path, int64_t *size);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public final class VfsImplementationSaf
Document.COLUMN_MIME_TYPE,
};

private static final String[] QUERY_ARGS_DISPLAY_NAME = {
Document.COLUMN_DISPLAY_NAME,
};

private static String normalizePath(String path) {
try
{
Expand Down Expand Up @@ -182,6 +186,142 @@ public static boolean removeSafFile(ContentResolver content, String tree, String
return true;
}

private static Uri getDocumentUri(Uri treeUri, String path) {
return DocumentsContract.buildDocumentUriUsingTree(treeUri, path.length() == 1 ? DocumentsContract.getTreeDocumentId(treeUri) : DocumentsContract.getTreeDocumentId(treeUri) + path);
}

private static String getDisplayName(ContentResolver content, Uri fileUri) {
final Cursor cursor;
try
{
cursor = content.query(fileUri, QUERY_ARGS_DISPLAY_NAME, null, null, null);
}
catch (RuntimeException e)
{
return null;
}

if (cursor == null)
return null;

try
{
return cursor.moveToNext() ? cursor.getString(0) : null;
}
finally
{
cursor.close();
}
}

/**
* Rename a document, returning whether or not it ended up named exactly name.
* Some providers pick a different name (such as "name (1)") instead of failing when the name is taken,
* so on a mismatch the document is renamed back to originalName.
*/
private static boolean renameDocumentExact(ContentResolver content, Uri fileUri, String name, String originalName) {
Uri renamedUri;
try
{
renamedUri = DocumentsContract.renameDocument(content, fileUri, name);
}
catch (FileNotFoundException | RuntimeException e)
{
return false;
}

// null means the document was renamed without its ID changing
if (renamedUri == null)
renamedUri = fileUri;

if (name.equals(getDisplayName(content, renamedUri)))
return true;

try
{
DocumentsContract.renameDocument(content, renamedUri, originalName);
}
catch (FileNotFoundException | RuntimeException e)
{}

return false;
}

/**
* Rename a Storage Access Framework file, replacing the destination if it exists, returning whether or not the operation succeeded.
* Both paths must be in the same directory.
* Storage Access Framework has no atomic replace, so an existing destination is first renamed to the destination path with ".old" appended,
* and is only deleted once the file has been renamed into its place; if that fails, the destination is restored.
* @param content the content resolver returned by getContentResolver()
* @param tree the URI returned by the ACTION_OPEN_DOCUMENT_TREE intent action
* @param oldPath path of the file to rename, relative to the root directory of the tree
* @param newPath path to rename the file to, relative to the root directory of the tree
*/
public static boolean renameSafFile(ContentResolver content, String tree, String oldPath, String newPath)
{
if (Build.VERSION.SDK_INT < 21)
return false;

final Uri treeUri = Uri.parse(tree);

if (isDocument(treeUri))
return false;

oldPath = normalizePath(oldPath);
newPath = normalizePath(newPath);

if (oldPath.equals(newPath))
return new SafStat(content, tree, oldPath, false).getIsOpen();

final String parentPath = getPathParent(oldPath);
final String oldName = getPathFileName(oldPath);
final String newName = getPathFileName(newPath);

if (parentPath == null || oldName == null || newName == null || !parentPath.equals(getPathParent(newPath)))
return false;

final String asidePath = newPath + ".old";
final String asideName = newName + ".old";
final Uri newUri = getDocumentUri(treeUri, newPath);
final Uri asideUri = getDocumentUri(treeUri, asidePath);
boolean movedAside = false;

final SafStat newStat = new SafStat(content, tree, newPath, false);
if (newStat.getIsOpen())
{
if (newStat.getIsDirectory())
return false;
// A leftover from an earlier rename that could not clean up
if (new SafStat(content, tree, asidePath, false).getIsOpen())
removeSafFile(content, tree, asidePath);

if (!renameDocumentExact(content, newUri, asideName, newName))
return false;

movedAside = true;
}

if (renameDocumentExact(content, getDocumentUri(treeUri, oldPath), newName, oldName))
{
if (movedAside)
{
// The file is in place; a failed cleanup only leaves a stale ".old" for the next rename to remove
try
{
removeSafFile(content, tree, asidePath);
}
catch (RuntimeException e)
{}
}
return true;
}

if (movedAside)
renameDocumentExact(content, asideUri, newName, asideName);

return false;
}

public static class SafStat
{
private static final String[] QUERY_ARGS_WITH_SIZE = {
Expand Down
47 changes: 47 additions & 0 deletions libretro-common/vfs/vfs_implementation_saf.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ static jobject vfs_saf_content_resolver_object;
static jclass vfs_saf_vfs_implementation_saf_class;
static jmethodID vfs_saf_open_saf_file_method;
static jmethodID vfs_saf_remove_saf_file_method;
static jmethodID vfs_saf_rename_saf_file_method;
static jclass vfs_saf_saf_stat_class;
static jmethodID vfs_saf_saf_stat_init_method;
static jmethodID vfs_saf_saf_stat_get_is_open_method;
Expand Down Expand Up @@ -133,6 +134,9 @@ bool retro_vfs_init_saf(JNIEnv *(*get_jni_env)(void), jobject activity_object)
vfs_saf_remove_saf_file_method = (*env)->GetStaticMethodID(env, vfs_saf_vfs_implementation_saf_class, "removeSafFile", "(Landroid/content/ContentResolver;Ljava/lang/String;Ljava/lang/String;)Z");
if ((*env)->ExceptionOccurred(env)) goto error;

vfs_saf_rename_saf_file_method = (*env)->GetStaticMethodID(env, vfs_saf_vfs_implementation_saf_class, "renameSafFile", "(Landroid/content/ContentResolver;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z");
if ((*env)->ExceptionOccurred(env)) goto error;

vfs_saf_saf_stat_init_method = (*env)->GetMethodID(env, vfs_saf_saf_stat_class, "<init>", "(Landroid/content/ContentResolver;Ljava/lang/String;Ljava/lang/String;Z)V");
if ((*env)->ExceptionOccurred(env)) goto error;

Expand Down Expand Up @@ -625,6 +629,49 @@ int retro_vfs_file_remove_saf(const char *tree, const char *path)

int retro_vfs_file_rename_saf(const char *old_tree, const char *old_path, const char *new_tree, const char *new_path)
{
JNIEnv *env;
jstring tree_object;
jstring old_path_object;
jstring new_path_object;
bool ret;

/* A document can only be renamed within the tree it was granted through */
if (strcmp(old_tree, new_tree) != 0)
return -1;

if (vfs_saf_get_jni_env == NULL)
return -1;
env = vfs_saf_get_jni_env();
if (env == NULL)
return -1;

(*env)->PushLocalFrame(env, 3);
if ((*env)->ExceptionOccurred(env))
{
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
return -1;
}

tree_object = (*env)->NewStringUTF(env, old_tree);
if ((*env)->ExceptionOccurred(env)) goto error;

old_path_object = (*env)->NewStringUTF(env, old_path);
if ((*env)->ExceptionOccurred(env)) goto error;

new_path_object = (*env)->NewStringUTF(env, new_path);
if ((*env)->ExceptionOccurred(env)) goto error;

ret = (*env)->CallStaticBooleanMethod(env, vfs_saf_vfs_implementation_saf_class, vfs_saf_rename_saf_file_method, vfs_saf_content_resolver_object, tree_object, old_path_object, new_path_object);
if ((*env)->ExceptionOccurred(env)) goto error;

(*env)->PopLocalFrame(env, NULL);
return ret ? 0 : -1;

error:
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
(*env)->PopLocalFrame(env, NULL);
return -1;
}

Expand Down
Loading