mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
registerCallbacks took a new global ref to the activity on every onCreate without releasing the previous one, and unregisterCallbacks only runs on a real shutdown. Since rotation and resizing recreate the activity, each of those leaked the old one, view hierarchy and all. Releasing it means other threads can no longer use the raw global ref, which had been copied into AndroidStorage and TextDrawerAndroid and was read from the emu thread and IO threads. They now take a local ref under a mutex (Android_GetActivity, and the equivalent inside AndroidStorage for the shortcut activity's sake), so a call in flight keeps its activity alive while the UI thread swaps in the new one. The "no activity" bail is a null check on that local ref rather than a separate HasActivity() test beforehand. Between such a test and the call the UI thread can swap the activity out, and we'd hand null to a Java method that wants a Context. The one remaining HasActivity() caller never passes the activity on, so it has nothing to race with. The ContentUri class ref and method IDs are now only looked up once, instead of leaking a class ref per activity. Headless and the unit test executable link Common but not app-android.cpp, so they get a stub for this the same way they already do for getEnv and findClass. Without it the Android CI build fails at link time, where a normal gradle build is perfectly happy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
429 lines
16 KiB
C++
429 lines
16 KiB
C++
#include <inttypes.h>
|
|
#include <mutex>
|
|
|
|
#include "Common/File/AndroidStorage.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;
|
|
}
|
|
|
|
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.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);
|
|
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 = env->NewStringUTF(dirName.c_str());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateDirectory, activity, paramRoot, paramDirName));
|
|
}
|
|
|
|
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 = env->NewStringUTF(fileName.c_str());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateFile, activity, paramRoot, paramFileName));
|
|
}
|
|
|
|
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());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCopyFile, activity, paramFileName, paramDestParentUri));
|
|
}
|
|
|
|
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());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriMoveFile, activity, paramFileName, paramSrcParentUri, paramDestParentUri));
|
|
}
|
|
|
|
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());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRemoveFile, activity, paramFileName));
|
|
}
|
|
|
|
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 = env->NewStringUTF(newName.c_str());
|
|
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRenameFileTo, activity, paramFileUri, paramNewName));
|
|
}
|
|
|
|
// 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 (!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);
|
|
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 : env->NewStringUTF(prefix.c_str());
|
|
jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, activity, param, filenamePrefix);
|
|
|
|
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);
|
|
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());
|
|
return env->CallStaticLongMethod(g_classContentUri, contentUriGetFreeStorageSpace, activity, param);
|
|
}
|
|
|
|
// 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());
|
|
return env->CallStaticLongMethod(g_classContentUri, filePathGetFreeStorageSpace, activity, param);
|
|
}
|
|
|
|
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);
|
|
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
|
|
return env->CallStaticBooleanMethod(g_classContentUri, isExternalStoragePreservedLegacy);
|
|
}
|
|
|
|
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
|