Android refactor: Move all contentUri utilities to the new class ContentUri

This commit is contained in:
Henrik Rydgård
2025-11-06 09:03:44 +01:00
parent 57d64eaf60
commit cfe807baed
3 changed files with 451 additions and 425 deletions
+20 -20
View File
@@ -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<jclass>(env->NewGlobalRef(localClass));
g_classContentUri = reinterpret_cast<jclass>(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<File::FileInfo> 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<File::FileInfo> 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) {
@@ -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<Uri> 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<String> 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;
}
}
}
@@ -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<Uri> 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<String> 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;
}
}
}