From cfe807baed7134eaf6be07d36797d4b08f1d2f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Nov 2025 09:03:44 +0100 Subject: [PATCH] Android refactor: Move all contentUri utilities to the new class ContentUri --- Common/File/AndroidStorage.cpp | 40 +- android/src/org/ppsspp/ppsspp/ContentUri.java | 431 ++++++++++++++++++ .../src/org/ppsspp/ppsspp/PpssppActivity.java | 405 ---------------- 3 files changed, 451 insertions(+), 425 deletions(-) create mode 100644 android/src/org/ppsspp/ppsspp/ContentUri.java diff --git a/Common/File/AndroidStorage.cpp b/Common/File/AndroidStorage.cpp index fe74605c35..2798cb88f3 100644 --- a/Common/File/AndroidStorage.cpp +++ b/Common/File/AndroidStorage.cpp @@ -27,14 +27,14 @@ static jmethodID isExternalStoragePreservedLegacy; static jmethodID computeRecursiveDirectorySize; static jobject g_nativeActivity; -static jclass g_classActivity; +static jclass g_classContentUri; void Android_StorageSetActivity(jobject nativeActivity) { g_nativeActivity = nativeActivity; } void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) { - jclass localClass = env->FindClass("org/ppsspp/ppsspp/PpssppActivity"); + jclass localClass = env->FindClass("org/ppsspp/ppsspp/ContentUri"); _dbg_assert_(localClass); openContentUri = env->GetStaticMethodID(localClass, "openContentUri", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I"); @@ -66,14 +66,14 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) { computeRecursiveDirectorySize = env->GetStaticMethodID(localClass, "computeRecursiveDirectorySize", "(Landroid/app/Activity;Ljava/lang/String;)J"); _dbg_assert_(computeRecursiveDirectorySize); - g_classActivity = reinterpret_cast(env->NewGlobalRef(localClass)); + g_classContentUri = reinterpret_cast(env->NewGlobalRef(localClass)); env->DeleteLocalRef(localClass); // cleanup local ref } void Android_UnregisterStorageCallbacks(JNIEnv * env) { - if (g_classActivity) { - env->DeleteGlobalRef(g_classActivity); - g_classActivity = nullptr; + if (g_classContentUri) { + env->DeleteGlobalRef(g_classContentUri); + g_classContentUri = nullptr; } g_nativeActivity = nullptr; openContentUri = nullptr; @@ -125,7 +125,7 @@ int Android_OpenContentUriFd(std::string_view filename, Android_OpenContentUriMo } jstring j_filename = env->NewStringUTF(fname.c_str()); jstring j_mode = env->NewStringUTF(modeStr); - int fd = env->CallStaticIntMethod(g_classActivity, openContentUri, g_nativeActivity, j_filename, j_mode); + int fd = env->CallStaticIntMethod(g_classContentUri, openContentUri, g_nativeActivity, j_filename, j_mode); return fd; } @@ -136,7 +136,7 @@ StorageError Android_CreateDirectory(const std::string &rootTreeUri, const std:: auto env = getEnv(); jstring paramRoot = env->NewStringUTF(rootTreeUri.c_str()); jstring paramDirName = env->NewStringUTF(dirName.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCreateDirectory, g_nativeActivity, paramRoot, paramDirName)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateDirectory, g_nativeActivity, paramRoot, paramDirName)); } StorageError Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) { @@ -146,7 +146,7 @@ StorageError Android_CreateFile(const std::string &parentTreeUri, const std::str auto env = getEnv(); jstring paramRoot = env->NewStringUTF(parentTreeUri.c_str()); jstring paramFileName = env->NewStringUTF(fileName.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCreateFile, g_nativeActivity, paramRoot, paramFileName)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateFile, g_nativeActivity, paramRoot, paramFileName)); } StorageError Android_CopyFile(const std::string &fileUri, const std::string &destParentUri) { @@ -156,7 +156,7 @@ StorageError Android_CopyFile(const std::string &fileUri, const std::string &des auto env = getEnv(); jstring paramFileName = env->NewStringUTF(fileUri.c_str()); jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCopyFile, g_nativeActivity, paramFileName, paramDestParentUri)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCopyFile, g_nativeActivity, paramFileName, paramDestParentUri)); } StorageError Android_MoveFile(const std::string &fileUri, const std::string &srcParentUri, const std::string &destParentUri) { @@ -167,7 +167,7 @@ StorageError Android_MoveFile(const std::string &fileUri, const std::string &src jstring paramFileName = env->NewStringUTF(fileUri.c_str()); jstring paramSrcParentUri = env->NewStringUTF(srcParentUri.c_str()); jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriMoveFile, g_nativeActivity, paramFileName, paramSrcParentUri, paramDestParentUri)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriMoveFile, g_nativeActivity, paramFileName, paramSrcParentUri, paramDestParentUri)); } StorageError Android_RemoveFile(const std::string &fileUri) { @@ -176,7 +176,7 @@ StorageError Android_RemoveFile(const std::string &fileUri) { } auto env = getEnv(); jstring paramFileName = env->NewStringUTF(fileUri.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriRemoveFile, g_nativeActivity, paramFileName)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRemoveFile, g_nativeActivity, paramFileName)); } StorageError Android_RenameFileTo(const std::string &fileUri, const std::string &newName) { @@ -186,7 +186,7 @@ StorageError Android_RenameFileTo(const std::string &fileUri, const std::string auto env = getEnv(); jstring paramFileUri = env->NewStringUTF(fileUri.c_str()); jstring paramNewName = env->NewStringUTF(newName.c_str()); - return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriRenameFileTo, g_nativeActivity, paramFileUri, paramNewName)); + return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRenameFileTo, g_nativeActivity, paramFileUri, paramNewName)); } // NOTE: Does not set fullName - you're supposed to already know it. @@ -232,7 +232,7 @@ bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *fileInfo) { auto env = getEnv(); jstring paramFileUri = env->NewStringUTF(fileUri.c_str()); - jstring str = (jstring)env->CallStaticObjectMethod(g_classActivity, contentUriGetFileInfo, g_nativeActivity, paramFileUri); + jstring str = (jstring)env->CallStaticObjectMethod(g_classContentUri, contentUriGetFileInfo, g_nativeActivity, paramFileUri); if (!str) { return false; } @@ -250,7 +250,7 @@ bool Android_FileExists(const std::string &fileUri) { } auto env = getEnv(); jstring paramFileUri = env->NewStringUTF(fileUri.c_str()); - bool exists = env->CallStaticBooleanMethod(g_classActivity, contentUriFileExists, g_nativeActivity, paramFileUri); + bool exists = env->CallStaticBooleanMethod(g_classContentUri, contentUriFileExists, g_nativeActivity, paramFileUri); return exists; } @@ -265,7 +265,7 @@ std::vector Android_ListContentUri(const std::string &uri, const double start = time_now_d(); jstring param = env->NewStringUTF(uri.c_str()); - jobject retval = env->CallStaticObjectMethod(g_classActivity, listContentUriDir, g_nativeActivity, param); + jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param); jobjectArray fileList = (jobjectArray)retval; std::vector items; @@ -306,7 +306,7 @@ int64_t Android_GetFreeSpaceByContentUri(const std::string &uri) { auto env = getEnv(); jstring param = env->NewStringUTF(uri.c_str()); - return env->CallStaticLongMethod(g_classActivity, contentUriGetFreeStorageSpace, g_nativeActivity, param); + return env->CallStaticLongMethod(g_classContentUri, contentUriGetFreeStorageSpace, g_nativeActivity, param); } // Hm, this is never used? We use statvfs instead. @@ -322,7 +322,7 @@ int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath) { } jstring param = env->NewStringUTF(filePath.c_str()); - return env->CallStaticLongMethod(g_classActivity, filePathGetFreeStorageSpace, g_nativeActivity, param); + return env->CallStaticLongMethod(g_classContentUri, filePathGetFreeStorageSpace, g_nativeActivity, param); } int64_t Android_ComputeRecursiveDirectorySize(const std::string &uri) { @@ -334,7 +334,7 @@ int64_t Android_ComputeRecursiveDirectorySize(const std::string &uri) { jstring param = env->NewStringUTF(uri.c_str()); double start = time_now_d(); - int64_t size = env->CallStaticLongMethod(g_classActivity, computeRecursiveDirectorySize, g_nativeActivity, param); + int64_t size = env->CallStaticLongMethod(g_classContentUri, computeRecursiveDirectorySize, g_nativeActivity, param); double elapsed = time_now_d() - start; INFO_LOG(Log::IO, "ComputeRecursiveDirectorySize(%s) in %0.3f s", uri.c_str(), elapsed); @@ -347,7 +347,7 @@ bool Android_IsExternalStoragePreservedLegacy() { } auto env = getEnv(); // Note: No activity param - return env->CallStaticBooleanMethod(g_classActivity, isExternalStoragePreservedLegacy); + return env->CallStaticBooleanMethod(g_classContentUri, isExternalStoragePreservedLegacy); } const char *Android_ErrorToString(StorageError error) { diff --git a/android/src/org/ppsspp/ppsspp/ContentUri.java b/android/src/org/ppsspp/ppsspp/ContentUri.java new file mode 100644 index 0000000000..c0e557c446 --- /dev/null +++ b/android/src/org/ppsspp/ppsspp/ContentUri.java @@ -0,0 +1,431 @@ +package org.ppsspp.ppsspp; + +import android.app.Activity; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.storage.StorageManager; +import android.provider.DocumentsContract; +import android.system.Os; +import android.system.StructStatVfs; +import android.util.Log; + +import androidx.annotation.Keep; +import androidx.annotation.RequiresApi; +import androidx.documentfile.provider.DocumentFile; + +import java.io.File; +import java.util.ArrayList; +import java.util.UUID; + +public class ContentUri { + private static final String TAG = "PpssppActivity"; // don't want to add yet another tag + + // Matches the enum in AndroidStorage.h. + private static final int STORAGE_ERROR_SUCCESS = 0; + private static final int STORAGE_ERROR_UNKNOWN = -1; + private static final int STORAGE_ERROR_NOT_FOUND = -2; + private static final int STORAGE_ERROR_DISK_FULL = -3; + private static final int STORAGE_ERROR_ALREADY_EXISTS = -4; + + @Keep + @SuppressWarnings("unused") + public static int openContentUri(Activity activity, String uriString, String mode) { + try { + Uri uri = Uri.parse(uriString); + try (ParcelFileDescriptor filePfd = activity.getContentResolver().openFileDescriptor(uri, mode)) { + if (filePfd == null) { + // I'd expect an exception to happen before we get here, so this is probably + // never reached. + Log.e(TAG, "Failed to get file descriptor for " + uriString); + return -1; + } + return filePfd.detachFd(); // Take ownership of the fd. + } + } catch (java.lang.IllegalArgumentException e) { + // This exception is long and ugly and really just means file not found. + // We don't log anything (the caller can log). + return -1; + } catch (Exception e) { + // Don't know when this might happen. Let's log. Still, the result is just a + // failure that the caller may additionally log. + Log.e(TAG, "Unexpected openContentUri exception: " + e); + return -1; + } + } + + private static final String[] columns = new String[] { + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR + DocumentsContract.Document.COLUMN_LAST_MODIFIED + }; + + private static String cursorToString(Cursor c) { + final int flags = c.getInt(2); + // Filter out any virtual or partial nonsense. + // There's a bunch of potentially-interesting flags here btw, + // to figure out how to set access flags better, etc. + // Like FLAG_SUPPORTS_WRITE etc. + if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) { + return null; + } + final String mimeType = c.getString(3); + final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); + final String documentName = c.getString(0); + final long size = isDirectory ? 0 : c.getLong(1); + final long lastModified = c.getLong(4); + + String str = "F|"; + if (isDirectory) { + str = "D|"; + } + return str + size + "|" + documentName + "|" + lastModified; + } + + private static long directorySizeRecursion(Activity activity, Uri uri) { + Cursor c = null; + try { + // Log.i(TAG, "recursing into " + uri.toString()); + final String[] columns = new String[]{ + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR + }; + final ContentResolver resolver = activity.getContentResolver(); + final String documentId = DocumentsContract.getDocumentId(uri); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId); + c = resolver.query(childrenUri, columns, null, null, null); + long sizeSum = 0; + + // Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework + // to handle more than one... + ArrayList childDirs = new ArrayList<>(); + + if (c != null) { + while (c.moveToNext()) { + final String mimeType = c.getString(2); + final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); + if (isDirectory) { + final String childDocumentId = c.getString(0); + final Uri childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childDocumentId); + childDirs.add(childUri); + } else { + final long fileSize = c.getLong(1); + sizeSum += fileSize; + } + } + c.close(); + } + c = null; + + for (Uri childUri : childDirs) { + long dirSize = directorySizeRecursion(activity, childUri); + if (dirSize >= 0) { + sizeSum += dirSize; + } else { + return dirSize; + } + } + return sizeSum; + } catch (Exception e) { + return -1; + } finally { + if (c != null) { + c.close(); + } + } + } + + @Keep + @SuppressWarnings("unused") + public static long computeRecursiveDirectorySize(Activity activity, String uriString) { + try { + Uri uri = Uri.parse(uriString); + return directorySizeRecursion(activity, uri); + } + catch (Exception e) { + Log.e(TAG, "computeRecursiveSize exception: " + e); + return -1; + } + } + + // TODO: Maybe add a cheaper version that doesn't extract all the file information? + // TODO: Replace with a proper query: + // * https://stackoverflow.com/q + // uestions/42186820/documentfile-is-very-slow + @Keep + @SuppressWarnings("unused") + public static String[] listContentUriDir(Activity activity, String uriString) { + try { + Uri uri = Uri.parse(uriString); + final ContentResolver resolver = activity.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + uri, DocumentsContract.getDocumentId(uri)); + final ArrayList listing = new ArrayList<>(); + + String[] projection = { + DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0 + DocumentsContract.Document.COLUMN_SIZE, // index 1 + DocumentsContract.Document.COLUMN_FLAGS, // index 2 + DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3 + DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 + }; + + try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) { + if (c == null) { + return new String[]{"X"}; + } + while (c.moveToNext()) { + String str = cursorToString(c); + if (str != null) { + listing.add(str); + } + } + } + + return listing.toArray(new String[0]); + } catch (IllegalArgumentException e) { + return new String[]{"X"}; + } catch (Exception e) { + Log.e(TAG, "listContentUriDir exception: " + e); + return new String[]{"X"}; + } + } + + @Keep + @SuppressWarnings("unused") + public static int contentUriCreateDirectory(Activity activity, String rootTreeUri, String dirName) { + try { + Uri uri = Uri.parse(rootTreeUri); + DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri); + if (documentFile != null) { + DocumentFile createdDir = documentFile.createDirectory(dirName); + return createdDir != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; + } else { + Log.e(TAG, "contentUriCreateDirectory: fromTreeUri returned null"); + return STORAGE_ERROR_UNKNOWN; + } + } catch (Exception e) { + Log.e(TAG, "contentUriCreateDirectory exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + @Keep + @SuppressWarnings("unused") + public static int contentUriCreateFile(Activity activity, String rootTreeUri, String fileName) { + try { + Uri uri = Uri.parse(rootTreeUri); + DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri); + if (documentFile != null) { + // TODO: Check the file extension and choose MIME type appropriately. + // Or actually, let's not bother. + DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName); + return createdFile != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; + } else { + Log.e(TAG, "contentUriCreateFile: fromTreeUrisv returned null"); + return STORAGE_ERROR_UNKNOWN; + } + } catch (Exception e) { + Log.e(TAG, "contentUriCreateFile exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + @Keep + public static int contentUriRemoveFile(Activity activity, String fileName) { + try { + Uri uri = Uri.parse(fileName); + DocumentFile documentFile = DocumentFile.fromSingleUri(activity, uri); + if (documentFile != null) { + return documentFile.delete() ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; + } else { + // This can return null on old Android versions (that we no longer supports). + return STORAGE_ERROR_UNKNOWN; + } + } catch (Exception e) { + Log.e(TAG, "contentUriRemoveFile exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + // NOTE: The destination is the parent directory! This means that contentUriCopyFile + // cannot rename things as part of the operation. + @RequiresApi(Build.VERSION_CODES.N) + @Keep + @SuppressWarnings("unused") + public static int contentUriCopyFile(Activity activity, String srcFileUri, String dstParentDirUri) { + try { + Uri srcUri = Uri.parse(srcFileUri); + Uri dstParentUri = Uri.parse(dstParentDirUri); + return DocumentsContract.copyDocument(activity.getContentResolver(), srcUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; + } catch (Exception e) { + Log.e(TAG, "contentUriCopyFile exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + // NOTE: The destination is the parent directory! This means that contentUriCopyFile + // cannot rename things as part of the operation. + @RequiresApi(Build.VERSION_CODES.N_MR1) + @Keep + @SuppressWarnings("unused") + public static int contentUriMoveFile(Activity activity, String srcFileUri, String srcParentDirUri, String dstParentDirUri) { + try { + Uri srcUri = Uri.parse(srcFileUri); + Uri srcParentUri = Uri.parse(srcParentDirUri); + Uri dstParentUri = Uri.parse(dstParentDirUri); + Log.i(TAG, "DocumentsContract.moveDocument"); + int result = DocumentsContract.moveDocument(activity.getContentResolver(), srcUri, srcParentUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; + Log.i(TAG, "DocumentsContract.moveDocument done"); + return result; + } catch (Exception e) { + Log.e(TAG, "contentUriMoveFile exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + @Keep + @SuppressWarnings("unused") + public static int contentUriRenameFileTo(Activity activity, 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(activity.getContentResolver(), uri, newName); + // NOTE: we don't use the returned newUri for anything right now. + return STORAGE_ERROR_SUCCESS; + } catch (Exception e) { + // TODO: More detailed exception processing. + Log.e(TAG, "contentUriRenameFile exception: " + e); + return STORAGE_ERROR_UNKNOWN; + } + } + + 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... + @Keep + @SuppressWarnings("unused") + public static boolean contentUriFileExists(Activity activity, String fileUri) { + Cursor c = null; + try { + Uri uri = Uri.parse(fileUri); + c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + if (c != null) { + return c.getCount() > 0; + } else { + return false; + } + } catch (Exception e) { + // Log.w(TAG, "Failed query: " + e); + return false; + } finally { + closeQuietly(c); + } + } + + @Keep + @SuppressWarnings("unused") + public static String contentUriGetFileInfo(Activity activity, String fileName) { + String[] projection = { + DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0 + DocumentsContract.Document.COLUMN_SIZE, // index 1 + DocumentsContract.Document.COLUMN_FLAGS, // index 2 + DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3 + DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 + }; + + try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) { + if (c != null && c.moveToFirst()) { + return cursorToString(c); + } else { + return null; + } + } catch (Exception e) { + Log.e(TAG, "contentUriGetFileInfo exception: " + e); + return null; + } + } + + // The example in Android documentation uses this.getFilesDir for path. + // There's also a way to beg the OS for more space, which might clear caches, but + // let's just not bother with that for now. + // NOTE: This is really super slow! + @RequiresApi(Build.VERSION_CODES.M) + @Keep + @SuppressWarnings("unused") + public static long contentUriGetFreeStorageSpaceSlow(Activity activity, Uri uri) { + try { + ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "r"); + if (pfd == null) { + Log.w(TAG, "Failed to get free storage space from URI: " + uri); + return -1; + } + StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor()); + long freeSpace = stats.f_bavail * stats.f_bsize; + pfd.close(); + return freeSpace; + } catch (Exception e) { + // FileNotFoundException | ErrnoException e + // Log.getStackTraceString(e) + Log.e(TAG, "contentUriGetFreeStorageSpace exception: " + e); + return -1; + } + } + + @Keep + @SuppressWarnings("unused") + public static long contentUriGetFreeStorageSpace(Activity activity, String str) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Uri uri = Uri.parse(str); + return contentUriGetFreeStorageSpaceSlow(activity, uri); + } + + // Too early Android version + return -1; + } + + @RequiresApi(Build.VERSION_CODES.O) + @Keep + @SuppressWarnings("unused") + public static long filePathGetFreeStorageSpace(Activity activity, String filePath) { + try { + StorageManager storageManager = activity.getApplicationContext().getSystemService(StorageManager.class); + File file = new File(filePath); + UUID volumeUUID = storageManager.getUuidForPath(file); + return storageManager.getAllocatableBytes(volumeUUID); + } catch (Exception e) { + Log.e(TAG, "filePathGetFreeStorageSpace exception: " + e); + return -1; + } + } + + @Keep + @SuppressWarnings("unused") + public static boolean isExternalStoragePreservedLegacy() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // In 29 and later, we can check whether we got preserved storage legacy. + return Environment.isExternalStorageLegacy(); + } else { + // In 28 and earlier, we won't call this - we'll still request an exception. + return false; + } + } +} diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 5d14a2727a..c3eeb7fa1d 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -37,13 +37,6 @@ public class PpssppActivity extends NativeActivity { public static boolean libraryLoaded = false; - // Matches the enum in AndroidStorage.h. - private static final int STORAGE_ERROR_SUCCESS = 0; - private static final int STORAGE_ERROR_UNKNOWN = -1; - private static final int STORAGE_ERROR_NOT_FOUND = -2; - private static final int STORAGE_ERROR_DISK_FULL = -3; - private static final int STORAGE_ERROR_ALREADY_EXISTS = -4; - public static void CheckABIAndLoadLibrary() { try { System.loadLibrary("ppsspp_jni"); @@ -156,402 +149,4 @@ public class PpssppActivity extends NativeActivity { return "bad debug string: " + str; } } - - @Keep - @SuppressWarnings("unused") - public static int openContentUri(Activity activity, String uriString, String mode) { - try { - Uri uri = Uri.parse(uriString); - try (ParcelFileDescriptor filePfd = activity.getContentResolver().openFileDescriptor(uri, mode)) { - if (filePfd == null) { - // I'd expect an exception to happen before we get here, so this is probably - // never reached. - Log.e(TAG, "Failed to get file descriptor for " + uriString); - return -1; - } - return filePfd.detachFd(); // Take ownership of the fd. - } - } catch (java.lang.IllegalArgumentException e) { - // This exception is long and ugly and really just means file not found. - // We don't log anything (the caller can log). - return -1; - } catch (Exception e) { - // Don't know when this might happen. Let's log. Still, the result is just a - // failure that the caller may additionally log. - Log.e(TAG, "Unexpected openContentUri exception: " + e); - return -1; - } - } - - private static final String[] columns = new String[] { - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_FLAGS, - DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR - DocumentsContract.Document.COLUMN_LAST_MODIFIED - }; - - private static String cursorToString(Cursor c) { - final int flags = c.getInt(2); - // Filter out any virtual or partial nonsense. - // There's a bunch of potentially-interesting flags here btw, - // to figure out how to set access flags better, etc. - // Like FLAG_SUPPORTS_WRITE etc. - if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) { - return null; - } - final String mimeType = c.getString(3); - final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); - final String documentName = c.getString(0); - final long size = isDirectory ? 0 : c.getLong(1); - final long lastModified = c.getLong(4); - - String str = "F|"; - if (isDirectory) { - str = "D|"; - } - return str + size + "|" + documentName + "|" + lastModified; - } - - private static long directorySizeRecursion(Activity activity, Uri uri) { - Cursor c = null; - try { - // Log.i(TAG, "recursing into " + uri.toString()); - final String[] columns = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR - }; - final ContentResolver resolver = activity.getContentResolver(); - final String documentId = DocumentsContract.getDocumentId(uri); - final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId); - c = resolver.query(childrenUri, columns, null, null, null); - long sizeSum = 0; - - // Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework - // to handle more than one... - ArrayList childDirs = new ArrayList<>(); - - if (c != null) { - while (c.moveToNext()) { - final String mimeType = c.getString(2); - final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); - if (isDirectory) { - final String childDocumentId = c.getString(0); - final Uri childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childDocumentId); - childDirs.add(childUri); - } else { - final long fileSize = c.getLong(1); - sizeSum += fileSize; - } - } - c.close(); - } - c = null; - - for (Uri childUri : childDirs) { - long dirSize = directorySizeRecursion(activity, childUri); - if (dirSize >= 0) { - sizeSum += dirSize; - } else { - return dirSize; - } - } - return sizeSum; - } catch (Exception e) { - return -1; - } finally { - if (c != null) { - c.close(); - } - } - } - - @Keep - @SuppressWarnings("unused") - public static long computeRecursiveDirectorySize(Activity activity, String uriString) { - try { - Uri uri = Uri.parse(uriString); - return directorySizeRecursion(activity, uri); - } - catch (Exception e) { - Log.e(TAG, "computeRecursiveSize exception: " + e); - return -1; - } - } - - // TODO: Maybe add a cheaper version that doesn't extract all the file information? - // TODO: Replace with a proper query: - // * https://stackoverflow.com/q - // uestions/42186820/documentfile-is-very-slow - @Keep - @SuppressWarnings("unused") - public static String[] listContentUriDir(Activity activity, String uriString) { - try { - Uri uri = Uri.parse(uriString); - final ContentResolver resolver = activity.getContentResolver(); - final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - uri, DocumentsContract.getDocumentId(uri)); - final ArrayList listing = new ArrayList<>(); - - String[] projection = { - DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0 - DocumentsContract.Document.COLUMN_SIZE, // index 1 - DocumentsContract.Document.COLUMN_FLAGS, // index 2 - DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3 - DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 - }; - - try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) { - if (c == null) { - return new String[]{"X"}; - } - while (c.moveToNext()) { - String str = cursorToString(c); - if (str != null) { - listing.add(str); - } - } - } - - return listing.toArray(new String[0]); - } catch (IllegalArgumentException e) { - return new String[]{"X"}; - } catch (Exception e) { - Log.e(TAG, "listContentUriDir exception: " + e); - return new String[]{"X"}; - } - } - - @Keep - @SuppressWarnings("unused") - public static int contentUriCreateDirectory(Activity activity, String rootTreeUri, String dirName) { - try { - Uri uri = Uri.parse(rootTreeUri); - DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri); - if (documentFile != null) { - DocumentFile createdDir = documentFile.createDirectory(dirName); - return createdDir != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; - } else { - Log.e(TAG, "contentUriCreateDirectory: fromTreeUri returned null"); - return STORAGE_ERROR_UNKNOWN; - } - } catch (Exception e) { - Log.e(TAG, "contentUriCreateDirectory exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - @Keep - @SuppressWarnings("unused") - public static int contentUriCreateFile(Activity activity, String rootTreeUri, String fileName) { - try { - Uri uri = Uri.parse(rootTreeUri); - DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri); - if (documentFile != null) { - // TODO: Check the file extension and choose MIME type appropriately. - // Or actually, let's not bother. - DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName); - return createdFile != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; - } else { - Log.e(TAG, "contentUriCreateFile: fromTreeUrisv returned null"); - return STORAGE_ERROR_UNKNOWN; - } - } catch (Exception e) { - Log.e(TAG, "contentUriCreateFile exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - @Keep - public static int contentUriRemoveFile(Activity activity, String fileName) { - try { - Uri uri = Uri.parse(fileName); - DocumentFile documentFile = DocumentFile.fromSingleUri(activity, uri); - if (documentFile != null) { - return documentFile.delete() ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; - } else { - // This can return null on old Android versions (that we no longer supports). - return STORAGE_ERROR_UNKNOWN; - } - } catch (Exception e) { - Log.e(TAG, "contentUriRemoveFile exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - // NOTE: The destination is the parent directory! This means that contentUriCopyFile - // cannot rename things as part of the operation. - @RequiresApi(Build.VERSION_CODES.N) - @Keep - @SuppressWarnings("unused") - public static int contentUriCopyFile(Activity activity, String srcFileUri, String dstParentDirUri) { - try { - Uri srcUri = Uri.parse(srcFileUri); - Uri dstParentUri = Uri.parse(dstParentDirUri); - return DocumentsContract.copyDocument(activity.getContentResolver(), srcUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; - } catch (Exception e) { - Log.e(TAG, "contentUriCopyFile exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - // NOTE: The destination is the parent directory! This means that contentUriCopyFile - // cannot rename things as part of the operation. - @RequiresApi(Build.VERSION_CODES.N_MR1) - @Keep - @SuppressWarnings("unused") - public static int contentUriMoveFile(Activity activity, String srcFileUri, String srcParentDirUri, String dstParentDirUri) { - try { - Uri srcUri = Uri.parse(srcFileUri); - Uri srcParentUri = Uri.parse(srcParentDirUri); - Uri dstParentUri = Uri.parse(dstParentDirUri); - Log.i(TAG, "DocumentsContract.moveDocument"); - int result = DocumentsContract.moveDocument(activity.getContentResolver(), srcUri, srcParentUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN; - Log.i(TAG, "DocumentsContract.moveDocument done"); - return result; - } catch (Exception e) { - Log.e(TAG, "contentUriMoveFile exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - @Keep - @SuppressWarnings("unused") - public static int contentUriRenameFileTo(Activity activity, 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(activity.getContentResolver(), uri, newName); - // NOTE: we don't use the returned newUri for anything right now. - return STORAGE_ERROR_SUCCESS; - } catch (Exception e) { - // TODO: More detailed exception processing. - Log.e(TAG, "contentUriRenameFile exception: " + e); - return STORAGE_ERROR_UNKNOWN; - } - } - - 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... - @Keep - @SuppressWarnings("unused") - public static boolean contentUriFileExists(Activity activity, String fileUri) { - Cursor c = null; - try { - Uri uri = Uri.parse(fileUri); - c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); - if (c != null) { - return c.getCount() > 0; - } else { - return false; - } - } catch (Exception e) { - // Log.w(TAG, "Failed query: " + e); - return false; - } finally { - closeQuietly(c); - } - } - - @Keep - @SuppressWarnings("unused") - public static String contentUriGetFileInfo(Activity activity, String fileName) { - String[] projection = { - DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0 - DocumentsContract.Document.COLUMN_SIZE, // index 1 - DocumentsContract.Document.COLUMN_FLAGS, // index 2 - DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3 - DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 - }; - - try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) { - if (c != null && c.moveToFirst()) { - return cursorToString(c); - } else { - return null; - } - } catch (Exception e) { - Log.e(TAG, "contentUriGetFileInfo exception: " + e); - return null; - } - } - - // The example in Android documentation uses this.getFilesDir for path. - // There's also a way to beg the OS for more space, which might clear caches, but - // let's just not bother with that for now. - // NOTE: This is really super slow! - @RequiresApi(Build.VERSION_CODES.M) - @Keep - @SuppressWarnings("unused") - public static long contentUriGetFreeStorageSpaceSlow(Activity activity, Uri uri) { - try { - ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "r"); - if (pfd == null) { - Log.w(TAG, "Failed to get free storage space from URI: " + uri); - return -1; - } - StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor()); - long freeSpace = stats.f_bavail * stats.f_bsize; - pfd.close(); - return freeSpace; - } catch (Exception e) { - // FileNotFoundException | ErrnoException e - // Log.getStackTraceString(e) - Log.e(TAG, "contentUriGetFreeStorageSpace exception: " + e); - return -1; - } - } - - @Keep - @SuppressWarnings("unused") - public static long contentUriGetFreeStorageSpace(Activity activity, String str) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Uri uri = Uri.parse(str); - return contentUriGetFreeStorageSpaceSlow(activity, uri); - } - - // Too early Android version - return -1; - } - - @RequiresApi(Build.VERSION_CODES.O) - @Keep - @SuppressWarnings("unused") - public static long filePathGetFreeStorageSpace(Activity activity, String filePath) { - try { - StorageManager storageManager = activity.getApplicationContext().getSystemService(StorageManager.class); - File file = new File(filePath); - UUID volumeUUID = storageManager.getUuidForPath(file); - return storageManager.getAllocatableBytes(volumeUUID); - } catch (Exception e) { - Log.e(TAG, "filePathGetFreeStorageSpace exception: " + e); - return -1; - } - } - - @Keep - @SuppressWarnings("unused") - public static boolean isExternalStoragePreservedLegacy() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - // In 29 and later, we can check whether we got preserved storage legacy. - return Environment.isExternalStorageLegacy(); - } else { - // In 28 and earlier, we won't call this - we'll still request an exception. - return false; - } - } }