Files
ppsspp/Common/File/AndroidStorage.cpp
T
Henrik RydgårdandClaude Opus 5.5 56580581e0 Android: Check for Java exceptions after content URI calls
ContentUri catches Exception, but an Error got through, and none of the
native wrappers checked. A thrown int call returned 0, which reads as
SUCCESS, and the exception stayed pending, so the next JNI call aborted.
copyDocument/moveDocument are API 24/25 while the folder browser is
offered from 21, so those threw NoSuchMethodError there - now they return
a failure so the callers fall back.

Also convert non-URI names to Java's modified UTF-8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:19 +02:00

502 lines
18 KiB
C++

#include <inttypes.h>
#include <mutex>
#include "Common/File/AndroidStorage.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/System/System.h"
#include "android/jni/app-android.h"
#include "Common/Thread/ThreadUtil.h"
#if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__)
static jmethodID openContentUri;
static jmethodID listContentUriDir;
static jmethodID contentUriCreateFile;
static jmethodID contentUriCreateDirectory;
static jmethodID contentUriCopyFile;
static jmethodID contentUriMoveFile;
static jmethodID contentUriRemoveFile;
static jmethodID contentUriRenameFileTo;
static jmethodID contentUriGetFileInfo;
static jmethodID contentUriFileExists;
static jmethodID contentUriGetFreeStorageSpace;
static jmethodID filePathGetFreeStorageSpace;
static jmethodID isExternalStoragePreservedLegacy;
static jmethodID computeRecursiveDirectorySize;
// A global ref owned by whoever calls Android_StorageSetActivity, who may replace it and delete the old
// one at any time. So we only look at it under the mutex, and call with a local ref of our own.
static jobject g_nativeActivity;
static std::mutex g_nativeActivityLock;
static jclass g_classContentUri;
void Android_StorageSetActivity(jobject nativeActivity) {
std::lock_guard<std::mutex> guard(g_nativeActivityLock);
g_nativeActivity = nativeActivity;
}
// Only for the one call that doesn't pass the activity to Java. Anything that does must use
// AcquireActivity and check the ref it got - between a check here and the call, the UI thread
// can swap the activity out from under us.
static bool HasActivity() {
std::lock_guard<std::mutex> guard(g_nativeActivityLock);
return g_nativeActivity != nullptr;
}
// Returns a local ref, to be freed by the caller's JNILocalFrame. Can be null if we just lost the activity.
static jobject AcquireActivity(JNIEnv *env) {
std::lock_guard<std::mutex> guard(g_nativeActivityLock);
return g_nativeActivity ? env->NewLocalRef(g_nativeActivity) : nullptr;
}
// Returns true (and clears it) if the last Java call threw. ContentUri catches Exception, but an Error
// (NoSuchMethodError, OutOfMemoryError) gets through, and a pending exception makes the next JNI call abort.
// The call's return value is garbage then - for the int calls, 0 would read as SUCCESS.
static bool CheckException(JNIEnv *env, const char *func) {
if (!env->ExceptionCheck()) {
return false;
}
ERROR_LOG(Log::IO, "%s: Java exception", func);
env->ExceptionDescribe();
env->ExceptionClear();
return true;
}
// For names that aren't URIs (those are percent-encoded already), which can contain anything.
static jstring NewJavaString(JNIEnv *env, const std::string &str) {
std::string modified;
ConvertUTF8ToJavaModifiedUTF8(&modified, str);
return env->NewStringUTF(modified.c_str());
}
void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) {
if (g_classContentUri) {
// Already done. This gets called for each new activity, but the class and the method IDs stay
// valid, and other threads may be using them right now.
return;
}
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");
_dbg_assert_(openContentUri);
listContentUriDir = env->GetStaticMethodID(localClass, "listContentUriDir", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;");
_dbg_assert_(listContentUriDir);
contentUriCreateDirectory = env->GetStaticMethodID(localClass, "contentUriCreateDirectory", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
_dbg_assert_(contentUriCreateDirectory);
contentUriCreateFile = env->GetStaticMethodID(localClass, "contentUriCreateFile", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
_dbg_assert_(contentUriCreateFile);
contentUriCopyFile = env->GetStaticMethodID(localClass, "contentUriCopyFile", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
_dbg_assert_(contentUriCopyFile);
contentUriRemoveFile = env->GetStaticMethodID(localClass, "contentUriRemoveFile", "(Landroid/app/Activity;Ljava/lang/String;)I");
_dbg_assert_(contentUriRemoveFile);
contentUriMoveFile = env->GetStaticMethodID(localClass, "contentUriMoveFile", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I");
_dbg_assert_(contentUriMoveFile);
contentUriRenameFileTo = env->GetStaticMethodID(localClass, "contentUriRenameFileTo", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
_dbg_assert_(contentUriRenameFileTo);
contentUriGetFileInfo = env->GetStaticMethodID(localClass, "contentUriGetFileInfo", "(Landroid/app/Activity;Ljava/lang/String;)Ljava/lang/String;");
_dbg_assert_(contentUriGetFileInfo);
contentUriFileExists = env->GetStaticMethodID(localClass, "contentUriFileExists", "(Landroid/app/Activity;Ljava/lang/String;)Z");
_dbg_assert_(contentUriFileExists);
contentUriGetFreeStorageSpace = env->GetStaticMethodID(localClass, "contentUriGetFreeStorageSpace", "(Landroid/app/Activity;Ljava/lang/String;)J");
_dbg_assert_(contentUriGetFreeStorageSpace);
filePathGetFreeStorageSpace = env->GetStaticMethodID(localClass, "filePathGetFreeStorageSpace", "(Landroid/app/Activity;Ljava/lang/String;)J");
_dbg_assert_(filePathGetFreeStorageSpace);
isExternalStoragePreservedLegacy = env->GetStaticMethodID(localClass, "isExternalStoragePreservedLegacy", "()Z"); // doesn't need an activity
_dbg_assert_(isExternalStoragePreservedLegacy);
computeRecursiveDirectorySize = env->GetStaticMethodID(localClass, "computeRecursiveDirectorySize", "(Landroid/app/Activity;Ljava/lang/String;)J");
_dbg_assert_(computeRecursiveDirectorySize);
g_classContentUri = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
env->DeleteLocalRef(localClass); // cleanup local ref
}
void Android_UnregisterStorageCallbacks(JNIEnv * env) {
if (g_classContentUri) {
env->DeleteGlobalRef(g_classContentUri);
g_classContentUri = nullptr;
}
Android_StorageSetActivity(nullptr);
openContentUri = nullptr;
listContentUriDir = nullptr;
contentUriCreateFile = nullptr;
contentUriCreateDirectory = nullptr;
contentUriCopyFile = nullptr;
contentUriMoveFile = nullptr;
contentUriRemoveFile = nullptr;
contentUriRenameFileTo = nullptr;
contentUriGetFileInfo = nullptr;
contentUriFileExists = nullptr;
contentUriGetFreeStorageSpace = nullptr;
filePathGetFreeStorageSpace = nullptr;
isExternalStoragePreservedLegacy = nullptr;
computeRecursiveDirectorySize = nullptr;
}
bool Android_IsContentUri(std::string_view filename) {
return startsWith(filename, "content://");
}
int Android_OpenContentUriFd(std::string_view filename, Android_OpenContentUriMode mode) {
/*
// Should breakpoint here to try to find and move as many of these off the EmuThread as possible
if (!strcmp(GetCurrentThreadName(), "EmuThread")) {
WARN_LOG(Log::IO, "Content URI opened on EmuThread: %.*s", (int)filename.size(), filename.data());
}
*/
std::string fname(filename);
// PPSSPP adds an ending slash to directories before looking them up.
// TODO: Fix that in the caller (or don't call this for directories).
if (!fname.empty() && fname.back() == '/')
fname.pop_back();
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
// Hit this in shortcut creation.
ERROR_LOG(Log::IO, "Android_OpenContentUriFd: No native activity");
return -1;
}
const char *modeStr = "";
switch (mode) {
case Android_OpenContentUriMode::READ: modeStr = "r"; break;
case Android_OpenContentUriMode::READ_WRITE: modeStr = "rw"; break;
case Android_OpenContentUriMode::READ_WRITE_TRUNCATE: modeStr = "rwt"; break;
}
jstring j_filename = env->NewStringUTF(fname.c_str());
jstring j_mode = env->NewStringUTF(modeStr);
int fd = env->CallStaticIntMethod(g_classContentUri, openContentUri, activity, j_filename, j_mode);
if (CheckException(env, __FUNCTION__)) {
return -1;
}
return fd;
}
StorageError Android_CreateDirectory(const std::string &rootTreeUri, const std::string &dirName) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramRoot = env->NewStringUTF(rootTreeUri.c_str());
jstring paramDirName = NewJavaString(env, dirName);
int result = env->CallStaticIntMethod(g_classContentUri, contentUriCreateDirectory, activity, paramRoot, paramDirName);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
StorageError Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramRoot = env->NewStringUTF(parentTreeUri.c_str());
jstring paramFileName = NewJavaString(env, fileName);
int result = env->CallStaticIntMethod(g_classContentUri, contentUriCreateFile, activity, paramRoot, paramFileName);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
StorageError Android_CopyFile(const std::string &fileUri, const std::string &destParentUri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str());
int result = env->CallStaticIntMethod(g_classContentUri, contentUriCopyFile, activity, paramFileName, paramDestParentUri);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
StorageError Android_MoveFile(const std::string &fileUri, const std::string &srcParentUri, const std::string &destParentUri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
jstring paramSrcParentUri = env->NewStringUTF(srcParentUri.c_str());
jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str());
int result = env->CallStaticIntMethod(g_classContentUri, contentUriMoveFile, activity, paramFileName, paramSrcParentUri, paramDestParentUri);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
StorageError Android_RemoveFile(const std::string &fileUri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
int result = env->CallStaticIntMethod(g_classContentUri, contentUriRemoveFile, activity, paramFileName);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
StorageError Android_RenameFileTo(const std::string &fileUri, const std::string &newName) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return StorageError::UNKNOWN;
}
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
jstring paramNewName = NewJavaString(env, newName);
int result = env->CallStaticIntMethod(g_classContentUri, contentUriRenameFileTo, activity, paramFileUri, paramNewName);
if (CheckException(env, __FUNCTION__)) {
return StorageError::UNKNOWN;
}
return StorageErrorFromInt(result);
}
// NOTE: Does not set fullName - you're supposed to already know it.
static bool ParseFileInfo(std::string_view line, File::FileInfo *fileInfo) {
std::vector<std::string> parts;
SplitString(line, '|', parts);
if (parts.size() != 4) {
ERROR_LOG(Log::FileSystem, "Bad format (1): %.*s", (int)line.size(), line.data());
return false;
}
fileInfo->name = parts[2];
fileInfo->isDirectory = parts[0][0] == 'D';
fileInfo->exists = true;
if (1 != sscanf(parts[1].c_str(), "%" PRIu64, &fileInfo->size)) {
ERROR_LOG(Log::FileSystem, "Bad format (2): %.*s", (int)line.size(), line.data());
return false;
}
fileInfo->isWritable = true; // TODO: Should be passed as part of the string.
// TODO: For read-only mappings, reflect that here, similarly as with isWritable.
// Directories are normally executable (0111) which means they're traversable.
fileInfo->access = fileInfo->isDirectory ? 0777 : 0666;
uint64_t lastModifiedMs = 0;
if (1 != sscanf(parts[3].c_str(), "%" PRIu64, &lastModifiedMs)) {
ERROR_LOG(Log::FileSystem, "Bad format (3): %.*s", (int)line.size(), line.data());
return false;
}
// Convert from milliseconds
uint32_t lastModified = lastModifiedMs / 1000;
// We don't have better information, so let's just spam lastModified into all the date/time fields.
fileInfo->mtime = lastModified;
fileInfo->ctime = lastModified;
fileInfo->atime = lastModified;
return true;
}
bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *fileInfo) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return false;
}
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
jstring str = (jstring)env->CallStaticObjectMethod(g_classContentUri, contentUriGetFileInfo, activity, paramFileUri);
if (CheckException(env, __FUNCTION__) || !str) {
return false;
}
const char *charArray = env->GetStringUTFChars(str, 0);
if (!charArray) {
return false;
}
bool retval = ParseFileInfo(charArray, fileInfo);
env->ReleaseStringUTFChars(str, charArray);
fileInfo->fullName = Path(fileUri);
return retval && fileInfo->exists;
}
bool Android_FileExists(const std::string &fileUri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return false;
}
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
bool exists = env->CallStaticBooleanMethod(g_classContentUri, contentUriFileExists, activity, paramFileUri);
if (CheckException(env, __FUNCTION__)) {
return false;
}
return exists;
}
std::vector<File::FileInfo> Android_ListContentUri(const std::string &uri, const std::string &prefix, bool *exists) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
*exists = false;
return {};
}
*exists = true;
double start = time_now_d();
jstring param = env->NewStringUTF(uri.c_str());
jstring filenamePrefix = prefix.empty() ? nullptr : NewJavaString(env, prefix);
jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, activity, param, filenamePrefix);
if (CheckException(env, __FUNCTION__)) {
*exists = false;
return {};
}
jobjectArray fileList = (jobjectArray)retval;
std::vector<File::FileInfo> items;
if (!fileList) {
*exists = false;
return items;
}
int size = env->GetArrayLength(fileList);
for (int i = 0; i < size; i++) {
jstring str = (jstring)env->GetObjectArrayElement(fileList, i);
if (!str) {
continue;
}
const char *charArray = env->GetStringUTFChars(str, 0);
if (charArray) { // paranoia
std::string line = charArray;
File::FileInfo info{};
if (line == "X") {
// Indicates an exception thrown, uri doesn't exist.
*exists = false;
} else if (ParseFileInfo(line, &info)) {
// We can just reconstruct the URI.
info.fullName = Path(uri) / info.name;
// INFO_LOG(Log::IO, "%s", info.name.c_str());
items.push_back(info);
}
env->ReleaseStringUTFChars(str, charArray);
}
// Don't leave these to the frame, there can be thousands.
env->DeleteLocalRef(str);
}
double elapsed = time_now_d() - start;
double threshold = 0.1;
if (elapsed >= threshold) {
INFO_LOG(Log::IO, "Listing directory on content URI '%s' took %0.3f s (%d files, log threshold = %0.3f)", uri.c_str(), elapsed, (int)items.size(), threshold);
}
return items;
}
int64_t Android_GetFreeSpaceByContentUri(const std::string &uri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return false;
}
jstring param = env->NewStringUTF(uri.c_str());
int64_t space = env->CallStaticLongMethod(g_classContentUri, contentUriGetFreeStorageSpace, activity, param);
if (CheckException(env, __FUNCTION__)) {
return -1;
}
return space;
}
// Hm, this is never used? We use statvfs instead.
int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return false;
}
if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) < 26) {
// This is available from Android O.
return -1;
}
jstring param = env->NewStringUTF(filePath.c_str());
int64_t space = env->CallStaticLongMethod(g_classContentUri, filePathGetFreeStorageSpace, activity, param);
if (CheckException(env, __FUNCTION__)) {
return -1;
}
return space;
}
int64_t Android_ComputeRecursiveDirectorySize(const std::string &uri) {
auto env = getEnv();
JNILocalFrame frame(env);
jobject activity = AcquireActivity(env);
if (!activity) {
return false;
}
jstring param = env->NewStringUTF(uri.c_str());
double start = time_now_d();
int64_t size = env->CallStaticLongMethod(g_classContentUri, computeRecursiveDirectorySize, activity, param);
if (CheckException(env, __FUNCTION__)) {
return 0;
}
double elapsed = time_now_d() - start;
INFO_LOG(Log::IO, "ComputeRecursiveDirectorySize(%s) in %0.3f s", uri.c_str(), elapsed);
return size;
}
bool Android_IsExternalStoragePreservedLegacy() {
if (!HasActivity()) {
return false;
}
auto env = getEnv();
JNILocalFrame frame(env);
// Note: No activity param
bool preserved = env->CallStaticBooleanMethod(g_classContentUri, isExternalStoragePreservedLegacy);
if (CheckException(env, __FUNCTION__)) {
return false;
}
return preserved;
}
const char *Android_ErrorToString(StorageError error) {
switch (error) {
case StorageError::SUCCESS: return "SUCCESS";
case StorageError::UNKNOWN: return "UNKNOWN";
case StorageError::NOT_FOUND: return "NOT_FOUND";
case StorageError::DISK_FULL: return "DISK_FULL";
case StorageError::ALREADY_EXISTS: return "ALREADY_EXISTS";
default: return "(UNKNOWN)";
}
}
#else
// These strings should never appear except on Android.
// Very hacky.
std::string g_extFilesDir = "(IF YOU SEE THIS THERE'S A BUG)";
std::string g_externalDir = "(IF YOU SEE THIS THERE'S A BUG (2))";
#endif