diff --git a/Common/File/AndroidStorage.cpp b/Common/File/AndroidStorage.cpp index 73de8b7b27..35cfcee09c 100644 --- a/Common/File/AndroidStorage.cpp +++ b/Common/File/AndroidStorage.cpp @@ -11,6 +11,7 @@ static jmethodID openContentUri; static jmethodID listContentUriDir; static jmethodID contentUriCreateFile; static jmethodID contentUriCreateDirectory; +static jmethodID contentUriCopyFile; static jmethodID contentUriRemoveFile; static jmethodID contentUriRenameFileTo; static jmethodID contentUriGetFileInfo; @@ -33,6 +34,8 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) { _dbg_assert_(contentUriCreateDirectory); contentUriCreateFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriCreateFile", "(Ljava/lang/String;Ljava/lang/String;)Z"); _dbg_assert_(contentUriCreateFile); + contentUriCopyFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriCopyFile", "(Ljava/lang/String;Ljava/lang/String;)Z"); + _dbg_assert_(contentUriCopyFile); contentUriRemoveFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriRemoveFile", "(Ljava/lang/String;)Z"); _dbg_assert_(contentUriRemoveFile); contentUriRenameFileTo = env->GetMethodID(env->GetObjectClass(obj), "contentUriRenameFileTo", "(Ljava/lang/String;Ljava/lang/String;)Z"); @@ -95,6 +98,16 @@ bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fil return env->CallBooleanMethod(g_nativeActivity, contentUriCreateFile, paramRoot, paramFileName); } +bool Android_CopyFile(const std::string &fileUri, const std::string &destParentUri) { + if (!g_nativeActivity) { + return false; + } + auto env = getEnv(); + jstring paramFileName = env->NewStringUTF(fileUri.c_str()); + jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str()); + return env->CallBooleanMethod(g_nativeActivity, contentUriCopyFile, paramFileName, paramDestParentUri); +} + bool Android_RemoveFile(const std::string &fileUri) { if (!g_nativeActivity) { return false; @@ -130,7 +143,7 @@ static bool ParseFileInfo(const std::string &line, File::FileInfo *fileInfo) { fileInfo->access = fileInfo->isDirectory ? 0666 : 0777; // TODO: For read-only mappings, reflect that here, similarly as with isWritable. uint64_t lastModifiedMs = 0; - sscanf(parts[4].c_str(), "%" PRIu64, &lastModifiedMs); + sscanf(parts[3].c_str(), "%" PRIu64, &lastModifiedMs); // Convert from milliseconds uint32_t lastModified = lastModifiedMs / 1000; diff --git a/Common/File/AndroidStorage.h b/Common/File/AndroidStorage.h index 5370cb2be2..a217b3d4e0 100644 --- a/Common/File/AndroidStorage.h +++ b/Common/File/AndroidStorage.h @@ -25,6 +25,7 @@ bool Android_IsContentUri(const std::string &uri); int Android_OpenContentUriFd(const std::string &uri, const Android_OpenContentUriMode mode); bool Android_CreateDirectory(const std::string &parentTreeUri, const std::string &dirName); bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName); +bool Android_CopyFile(const std::string &fileUri, const std::string &destParentUri); bool Android_RemoveFile(const std::string &fileUri); bool Android_RenameFileTo(const std::string &fileUri, const std::string &newName); bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *info); @@ -44,6 +45,7 @@ inline bool Android_IsContentUri(const std::string &uri) { return false; } inline int Android_OpenContentUriFd(const std::string &uri, const Android_OpenContentUriMode mode) { return -1; } inline bool Android_CreateDirectory(const std::string &parentTreeUri, const std::string &dirName) { return false; } inline bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) { return false; } +inline bool Android_CopyFile(const std::string &fileUri, const std::string &destParentUri) { return false; } inline bool Android_RemoveFile(const std::string &fileUri) { return false; } inline bool Android_RenameFileTo(const std::string &fileUri, const std::string &newName) { return false; } inline bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *info) { return false; } diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 9db14467c1..aba075a2f7 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -173,35 +173,36 @@ public class PpssppActivity extends NativeActivity { // TODO: Replace with a proper query: // * https://stackoverflow.com/questions/42186820/documentfile-is-very-slow public String[] listContentUriDir(String uriString) { + Cursor c = null; try { Uri uri = Uri.parse(uriString); - final ContentResolver resolver = getContentResolver(); - final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, - DocumentsContract.getDocumentId(uri)); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + uri, DocumentsContract.getDocumentId(uri)); final ArrayList listing = new ArrayList<>(); - Cursor c = null; - try { - c = resolver.query(childrenUri, columns, null, null, null); - while (c.moveToNext()) { - String str = cursorToString(c); - if (str != null) { - listing.add(str); - } - } - } catch (Exception e) { - Log.w(TAG, "Failed query: " + e); - } finally { - if (c != null) { - c.close(); + c = resolver.query(childrenUri, columns, null, null, null); + while (c.moveToNext()) { + String str = cursorToString(c); + if (str != null) { + listing.add(str); } } // Is ArrayList weird or what? String[] strings = new String[listing.size()]; return listing.toArray(strings); - } catch (Exception e) { + } + catch (IllegalArgumentException e) { + // Due to sloppy exception handling in resolver.query, we get this wrapping + // a FileNotFoundException if the directory doesn't exist. + return new String[]{}; + } + catch (Exception e) { Log.e(TAG, "listContentUriDir exception: " + e.toString()); return new String[]{}; + } finally { + if (c != null) { + c.close(); + } } } @@ -255,15 +256,27 @@ public class PpssppActivity extends NativeActivity { } } + // NOTE: The destination is the parent directory! This means that contentUriCopyFile + // cannot rename things as part of the operation. + public boolean contentUriCopyFile(String srcFileUri, String dstParentDirUri) { + try { + Uri srcUri = Uri.parse(srcFileUri); + Uri dstParentUri = Uri.parse(dstParentDirUri); + DocumentsContract.copyDocument(getContentResolver(), srcUri, dstParentUri); + return true; + } catch (Exception e) { + Log.e(TAG, "contentUriCopyFile exception: " + e.toString()); + return false; + } + } + public boolean contentUriRenameFileTo(String fileUri, String newName) { try { Uri uri = Uri.parse(fileUri); - // Due to a design flaw, we can't use DocumentFile.renameTo(). // Instead we use the DocumentsContract API directly. // See https://stackoverflow.com/questions/37168200/android-5-0-new-sd-card-access-api-documentfile-renameto-unsupportedoperation. Uri newUri = DocumentsContract.renameDocument(getContentResolver(), uri, newName); - // Log.i(TAG, "New uri: " + newUri.toString()); return true; } catch (Exception e) { Log.e(TAG, "contentUriRenameFile exception: " + e.toString()); @@ -271,23 +284,30 @@ public class PpssppActivity extends NativeActivity { } } - // Possibly faster than contentUriGetFileInfo. + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + // Probably slightly faster than contentUriGetFileInfo. + // Smaller difference now than before I changed that one to a query... public boolean contentUriFileExists(String fileUri) { + Cursor c = null; try { Uri uri = Uri.parse(fileUri); - DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri); - if (documentFile != null) { - if (documentFile.exists()) { - return true; - } else { - return false; - } - } else { - return false; - } + c = getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + return c.getCount() > 0; } catch (Exception e) { - Log.e(TAG, "contentUriFileExists exception: " + e.toString()); + // Log.w(TAG, "Failed query: " + e); return false; + } finally { + closeQuietly(c); } }