Files
ppsspp/Common/File/AndroidStorage.cpp
T
Henrik RydgårdandClaude Opus 5.5 d6985d81eb Android: Simplify joystickAxis and queryGameInfo, drop dead JNI calls
joystickAxis copies the axes in batches on the stack instead of allocating
per event, and bounds the count by the arrays it got. queryGameInfo's cache
is a local, in a helper so it's gone before the thread manager is torn down.

Removes Android_GetFreeSpaceByFilePath (never called) with its Java side, and
isLandscape, which only existed to call NativeGetAppInfo early.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 01:36:35 +02:00

420 lines
16 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 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;
}
// Fixed for the life of the process (it's decided at install), so asked once at registration.
static bool g_externalStoragePreservedLegacy;
// 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;
}
// ContentUri catches Exception, but an Error (NoSuchMethodError, OutOfMemoryError) gets through, so every
// call is followed by CheckJavaException. For the int calls, the garbage result would read as SUCCESS.
// 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) {
_dbg_assert_(!g_classContentUri);
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);
jmethodID 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
g_externalStoragePreservedLegacy = env->CallStaticBooleanMethod(g_classContentUri, isExternalStoragePreservedLegacy);
if (CheckJavaException(env, __FUNCTION__)) {
g_externalStoragePreservedLegacy = false;
}
}
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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(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 (CheckJavaException(env, __FUNCTION__) || !str) {
return false;
}
bool retval = ParseFileInfo(GetJavaString(env, str), fileInfo);
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 (CheckJavaException(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 (CheckJavaException(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;
}
std::string line = GetJavaString(env, str);
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;
items.push_back(info);
}
// 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 -1;
}
jstring param = env->NewStringUTF(uri.c_str());
int64_t space = env->CallStaticLongMethod(g_classContentUri, contentUriGetFreeStorageSpace, activity, param);
if (CheckJavaException(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 0;
}
jstring param = env->NewStringUTF(uri.c_str());
double start = time_now_d();
int64_t size = env->CallStaticLongMethod(g_classContentUri, computeRecursiveDirectorySize, activity, param);
if (CheckJavaException(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() {
return g_externalStoragePreservedLegacy;
}
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