#include #include #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 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 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(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 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 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 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