mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
better SAF - texture pack support etc.
This commit is contained in:
@@ -95,6 +95,17 @@ static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Forward declaration from native-lib.cpp (Android JNI helper)
|
||||
#ifdef __ANDROID__
|
||||
// JNI helpers implemented in native-lib.cpp
|
||||
// Resolve a file path under the user-selected SAF data root, e.g. "textures/SLUS-12345/replacements/foo/bar.png"
|
||||
std::string ResolveSafPathUriJNI(const char* relative_path, bool create);
|
||||
// List files under a SAF-relative directory non-recursively: returns full relative paths (e.g., "textures/.../file.png").
|
||||
std::vector<std::string> SafListFilesFlatJNI(const char* relative_dir);
|
||||
// List files under a SAF-relative directory recursively: returns full relative paths.
|
||||
std::vector<std::string> SafListRecursiveFilesJNI(const char* relative_dir);
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
static inline void PathAppendString(std::string& dst, const T& src)
|
||||
{
|
||||
@@ -970,6 +981,29 @@ std::string Path::CreateFileURL(std::string_view path)
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool IsWriteMode(const char* mode)
|
||||
{
|
||||
if (!mode) return false;
|
||||
while (*mode)
|
||||
{
|
||||
if (*mode == 'w' || *mode == 'a' || *mode == '+') return true;
|
||||
mode++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ParseSafPath(const std::string& path, std::string* out_subdir, std::string* out_filename)
|
||||
{
|
||||
if (path.rfind("saf://", 0) != 0) return false;
|
||||
std::string rest = path.substr(6);
|
||||
// Expect format: subdir/filename
|
||||
size_t slash = rest.find('/');
|
||||
if (slash == std::string::npos) return false;
|
||||
*out_subdir = rest.substr(0, slash);
|
||||
*out_filename = rest.substr(slash + 1);
|
||||
return !out_subdir->empty() && !out_filename->empty();
|
||||
}
|
||||
|
||||
std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error* error)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
@@ -1003,8 +1037,26 @@ std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error*
|
||||
////
|
||||
std::string _filename(filename);
|
||||
if (_filename.rfind("content://", 0) == 0) {
|
||||
fp = fdopen(FileSystem::OpenFDFileContent(_filename.c_str()), "rb");
|
||||
} else {
|
||||
// Direct document uri
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(_filename.c_str(), cmode);
|
||||
fp = (fd >= 0) ? fdopen(fd, mode) : nullptr;
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
else if (_filename.rfind("saf://", 0) == 0) {
|
||||
const bool create = IsWriteMode(mode);
|
||||
std::string rel = _filename.substr(6);
|
||||
std::string doc = ResolveSafPathUriJNI(rel.c_str(), create);
|
||||
if (!doc.empty()) {
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(doc.c_str(), cmode);
|
||||
fp = (fd >= 0) ? fdopen(fd, mode) : nullptr;
|
||||
} else {
|
||||
fp = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fp = std::fopen(_filename.c_str(), mode);
|
||||
}
|
||||
////
|
||||
@@ -1017,18 +1069,33 @@ std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error*
|
||||
std::FILE* FileSystem::OpenCFileTryIgnoreCase(const char* filename, const char* mode, Error* error)
|
||||
{
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
return OpenCFile(filename, mode, error);
|
||||
return OpenCFile(filename, mode, error);
|
||||
#else
|
||||
std::FILE* fp;
|
||||
////
|
||||
std::FILE* fp = nullptr;
|
||||
std::string _filename(filename);
|
||||
if (_filename.rfind("content://", 0) == 0) {
|
||||
fp = fdopen(FileSystem::OpenFDFileContent(_filename.c_str()), "rb");
|
||||
} else {
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(_filename.c_str(), cmode);
|
||||
if (fd >= 0)
|
||||
fp = fdopen(fd, mode);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
else if (_filename.rfind("saf://", 0) == 0) {
|
||||
const bool create = IsWriteMode(mode);
|
||||
std::string rel = _filename.substr(6);
|
||||
std::string doc = ResolveSafPathUriJNI(rel.c_str(), create);
|
||||
if (!doc.empty()) {
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(doc.c_str(), cmode);
|
||||
if (fd >= 0)
|
||||
fp = fdopen(fd, mode);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fp = std::fopen(_filename.c_str(), mode);
|
||||
}
|
||||
////
|
||||
const auto cur_errno = errno;
|
||||
const auto cur_errno = errno;
|
||||
|
||||
if (!fp)
|
||||
{
|
||||
@@ -1042,8 +1109,25 @@ std::FILE* FileSystem::OpenCFileTryIgnoreCase(const char* filename, const char*
|
||||
{
|
||||
////
|
||||
if (file.FileName.rfind("content://", 0) == 0) {
|
||||
fp = fdopen(FileSystem::OpenFDFileContent(file.FileName.c_str()), "rb");
|
||||
} else {
|
||||
const char* cmode2 = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd2 = FileSystem::OpenFDFileContentWithMode(file.FileName.c_str(), cmode2);
|
||||
if (fd2 >= 0)
|
||||
fp = fdopen(fd2, mode);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
else if (file.FileName.rfind("saf://", 0) == 0) {
|
||||
const bool create2 = IsWriteMode(mode);
|
||||
std::string rel2 = file.FileName.substr(6);
|
||||
std::string doc2 = ResolveSafPathUriJNI(rel2.c_str(), create2);
|
||||
if (!doc2.empty()) {
|
||||
const char* cmode2 = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd2 = FileSystem::OpenFDFileContentWithMode(doc2.c_str(), cmode2);
|
||||
if (fd2 >= 0)
|
||||
fp = fdopen(fd2, mode);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fp = std::fopen(file.FileName.c_str(), mode);
|
||||
}
|
||||
////
|
||||
@@ -1073,8 +1157,23 @@ int FileSystem::OpenFDFile(const char* filename, int flags, int mode, Error* err
|
||||
////
|
||||
std::string _filename(filename);
|
||||
if (_filename.rfind("content://", 0) == 0) {
|
||||
fd = FileSystem::OpenFDFileContent(_filename.c_str());
|
||||
} else {
|
||||
const char* cmode = (flags & O_WRONLY) || (flags & O_RDWR) ? "rw" : "r";
|
||||
fd = FileSystem::OpenFDFileContentWithMode(_filename.c_str(), cmode);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
else if (_filename.rfind("saf://", 0) == 0) {
|
||||
const bool create = (flags & O_WRONLY) || (flags & O_RDWR) || (flags & O_CREAT);
|
||||
std::string rel = _filename.substr(6);
|
||||
std::string doc = ResolveSafPathUriJNI(rel.c_str(), create);
|
||||
if (!doc.empty()) {
|
||||
const char* cmode = (flags & O_WRONLY) || (flags & O_RDWR) ? "rw" : "r";
|
||||
fd = FileSystem::OpenFDFileContentWithMode(doc.c_str(), cmode);
|
||||
} else {
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fd = open(_filename.c_str(), flags, mode);
|
||||
}
|
||||
////
|
||||
@@ -1128,18 +1227,33 @@ std::FILE* FileSystem::OpenSharedCFile(const char* filename, const char* mode, F
|
||||
return nullptr;
|
||||
#else
|
||||
|
||||
std::FILE* fp;
|
||||
////
|
||||
std::FILE* fp = nullptr;
|
||||
std::string _filename(filename);
|
||||
if (_filename.rfind("content://", 0) == 0) {
|
||||
fp = fdopen(FileSystem::OpenFDFileContent(_filename.c_str()), "rb");
|
||||
} else {
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(_filename.c_str(), cmode);
|
||||
if (fd >= 0)
|
||||
fp = fdopen(fd, mode);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
else if (_filename.rfind("saf://", 0) == 0) {
|
||||
const bool create = IsWriteMode(mode);
|
||||
std::string rel = _filename.substr(6);
|
||||
std::string doc = ResolveSafPathUriJNI(rel.c_str(), create);
|
||||
if (!doc.empty()) {
|
||||
const char* cmode = IsWriteMode(mode) ? "rw" : "r";
|
||||
int fd = FileSystem::OpenFDFileContentWithMode(doc.c_str(), cmode);
|
||||
if (fd >= 0)
|
||||
fp = fdopen(fd, mode);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fp = std::fopen(_filename.c_str(), mode);
|
||||
}
|
||||
////
|
||||
if (!fp)
|
||||
Error::SetErrno(error, errno);
|
||||
return fp;
|
||||
if (!fp)
|
||||
Error::SetErrno(error, errno);
|
||||
return fp;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1546,13 +1660,44 @@ static u32 RecursiveFindFiles(const char* origin_path, const char* parent_path,
|
||||
|
||||
bool FileSystem::FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results, ProgressCallback* cancel)
|
||||
{
|
||||
// has a path
|
||||
if (path[0] == '\0')
|
||||
return false;
|
||||
// has a path
|
||||
if (path[0] == '\0')
|
||||
return false;
|
||||
|
||||
// clear result array
|
||||
if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY))
|
||||
results->clear();
|
||||
// clear result array
|
||||
if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY))
|
||||
results->clear();
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// SAF listing: path of the form saf://<relative_path_under_data_root>
|
||||
if (std::string(path).rfind("saf://", 0) == 0)
|
||||
{
|
||||
const std::string rel = std::string(path).substr(6);
|
||||
std::vector<std::string> files;
|
||||
if (flags & FILESYSTEM_FIND_RECURSIVE)
|
||||
files = SafListRecursiveFilesJNI(rel.c_str());
|
||||
else
|
||||
files = SafListFilesFlatJNI(rel.c_str());
|
||||
|
||||
const bool wildcard = (std::strpbrk(pattern, "*?") != nullptr);
|
||||
for (const std::string& relpath : files)
|
||||
{
|
||||
if (cancel && cancel->IsCancelled()) break;
|
||||
// Compare only the filename component against the pattern
|
||||
const std::string_view fname = Path::GetFileName(relpath);
|
||||
bool match = wildcard ? StringUtil::WildcardMatch(std::string(fname).c_str(), pattern) : (std::strcmp(std::string(fname).c_str(), pattern) == 0);
|
||||
if (!match) continue;
|
||||
FILESYSTEM_FIND_DATA out{};
|
||||
out.Attributes = 0; // files only for now
|
||||
out.FileName = std::string("saf://") + relpath;
|
||||
out.Size = -1;
|
||||
out.CreationTime = 0;
|
||||
out.ModificationTime = 0;
|
||||
results->push_back(out);
|
||||
}
|
||||
return !results->empty();
|
||||
}
|
||||
#endif
|
||||
|
||||
// add self if recursive, we don't want to visit it twice
|
||||
std::vector<std::string> visited;
|
||||
@@ -2685,3 +2830,7 @@ FileSystem::POSIXLock::~POSIXLock()
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ namespace FileSystem
|
||||
s64 FSize64(std::FILE* fp);
|
||||
|
||||
int OpenFDFileContent(const char* filename);
|
||||
int OpenFDFileContentWithMode(const char* filename, const char* mode);
|
||||
int OpenFDFile(const char* filename, int flags, int mode, Error* error = nullptr);
|
||||
|
||||
/// Sharing modes for OpenSharedCFile().
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "Console.h"
|
||||
|
||||
|
||||
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_managed(const char* filename, int flags, zip_error_t* ze)
|
||||
{
|
||||
zip_source_t* zs = zip_source_file_create(filename, 0, 0, ze);
|
||||
@@ -139,4 +140,4 @@ static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_t* zip, con
|
||||
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size = 4096)
|
||||
{
|
||||
return ReadFileInZipToContainer<std::vector<u8>>(file, chunk_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
#include "MTGS.h"
|
||||
#include "SDL3/SDL.h"
|
||||
#include <future>
|
||||
#ifdef __ANDROID__
|
||||
#include "SDL3/SDL.h"
|
||||
#endif
|
||||
|
||||
|
||||
bool s_execute_exit;
|
||||
@@ -490,6 +493,18 @@ Java_com_izzy2lost_psx2_NativeApp_setAsyncTextureLoading(JNIEnv *env, jclass cla
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_izzy2lost_psx2_NativeApp_setPrecacheTextureReplacements(JNIEnv *env, jclass clazz,
|
||||
jboolean p_enabled) {
|
||||
s_settings_interface.SetBoolValue("EmuCore/GS", "PrecacheTextureReplacements", p_enabled);
|
||||
|
||||
// Apply the settings immediately if emulation is running
|
||||
if (VMManager::HasValidVM()) {
|
||||
VMManager::ApplySettings();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_izzy2lost_psx2_NativeApp_setShadeBoost(JNIEnv *env, jclass clazz,
|
||||
@@ -673,7 +688,7 @@ Java_com_izzy2lost_psx2_NativeApp_saveGameSettingsToPath(JNIEnv *env, jclass cla
|
||||
// Check if file actually exists and has content
|
||||
if (FileSystem::FileExists(settings_path.c_str())) {
|
||||
s64 file_size = FileSystem::GetPathFileSize(settings_path.c_str());
|
||||
printf("PCSX2: File exists with size: %lld bytes\n", file_size);
|
||||
printf("PCSX2: File exists with size: %lld bytes\n", static_cast<long long>(file_size));
|
||||
} else {
|
||||
printf("PCSX2: File does not exist after save attempt\n");
|
||||
}
|
||||
@@ -983,6 +998,139 @@ int FileSystem::OpenFDFileContent(const char* filename)
|
||||
return fd;
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// Helpers callable from core for SAF bridging
|
||||
static jclass GetNativeAppClass(JNIEnv* env)
|
||||
{
|
||||
return env->FindClass("com/izzy2lost/psx2/NativeApp");
|
||||
}
|
||||
|
||||
std::string ResolveSafPathUriJNI(const char* relative_path, bool create)
|
||||
{
|
||||
JNIEnv* env = reinterpret_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (!env)
|
||||
return {};
|
||||
jclass cls = GetNativeAppClass(env);
|
||||
if (!cls)
|
||||
return {};
|
||||
jmethodID mid = env->GetStaticMethodID(cls, "resolveSafPathUri", "(Ljava/lang/String;Z)Ljava/lang/String;");
|
||||
if (!mid)
|
||||
return {};
|
||||
jstring jrel = env->NewStringUTF(relative_path);
|
||||
jobject juri = env->CallStaticObjectMethod(cls, mid, jrel, (jboolean)create);
|
||||
env->DeleteLocalRef(jrel);
|
||||
if (!juri)
|
||||
return {};
|
||||
const char* cstr = env->GetStringUTFChars((jstring)juri, nullptr);
|
||||
std::string out = cstr ? std::string(cstr) : std::string();
|
||||
if (cstr)
|
||||
env->ReleaseStringUTFChars((jstring)juri, cstr);
|
||||
env->DeleteLocalRef(juri);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> SafListRecursiveFilesJNI(const char* relative_dir)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
JNIEnv* env = reinterpret_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (!env)
|
||||
return out;
|
||||
jclass cls = GetNativeAppClass(env);
|
||||
if (!cls)
|
||||
return out;
|
||||
jmethodID mid = env->GetStaticMethodID(cls, "listSafRecursiveFiles", "(Ljava/lang/String;)[Ljava/lang/String;");
|
||||
if (!mid)
|
||||
return out;
|
||||
jstring jarg = env->NewStringUTF(relative_dir);
|
||||
jobjectArray arr = (jobjectArray)env->CallStaticObjectMethod(cls, mid, jarg);
|
||||
env->DeleteLocalRef(jarg);
|
||||
if (!arr)
|
||||
return out;
|
||||
jsize len = env->GetArrayLength(arr);
|
||||
out.reserve((size_t)len);
|
||||
for (jsize i = 0; i < len; i++)
|
||||
{
|
||||
jstring js = (jstring)env->GetObjectArrayElement(arr, i);
|
||||
if (!js) continue;
|
||||
const char* c = env->GetStringUTFChars(js, nullptr);
|
||||
if (c)
|
||||
{
|
||||
out.emplace_back(c);
|
||||
env->ReleaseStringUTFChars(js, c);
|
||||
}
|
||||
env->DeleteLocalRef(js);
|
||||
}
|
||||
env->DeleteLocalRef(arr);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> SafListFilesFlatJNI(const char* relative_dir)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
JNIEnv* env = reinterpret_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (!env)
|
||||
return out;
|
||||
jclass cls = GetNativeAppClass(env);
|
||||
if (!cls)
|
||||
return out;
|
||||
jmethodID mid = env->GetStaticMethodID(cls, "listSafFilesFlat", "(Ljava/lang/String;)[Ljava/lang/String;");
|
||||
if (!mid)
|
||||
return out;
|
||||
jstring jarg = env->NewStringUTF(relative_dir);
|
||||
jobjectArray arr = (jobjectArray)env->CallStaticObjectMethod(cls, mid, jarg);
|
||||
env->DeleteLocalRef(jarg);
|
||||
if (!arr)
|
||||
return out;
|
||||
jsize len = env->GetArrayLength(arr);
|
||||
out.reserve((size_t)len);
|
||||
for (jsize i = 0; i < len; i++)
|
||||
{
|
||||
jstring js = (jstring)env->GetObjectArrayElement(arr, i);
|
||||
if (!js) continue;
|
||||
const char* c = env->GetStringUTFChars(js, nullptr);
|
||||
if (c)
|
||||
{
|
||||
out.emplace_back(c);
|
||||
env->ReleaseStringUTFChars(js, c);
|
||||
}
|
||||
env->DeleteLocalRef(js);
|
||||
}
|
||||
env->DeleteLocalRef(arr);
|
||||
return out;
|
||||
}
|
||||
#endif // __ANDROID__
|
||||
|
||||
int FileSystem::OpenFDFileContentWithMode(const char* filename, const char* mode)
|
||||
{
|
||||
auto *env = static_cast<JNIEnv *>(SDL_GetAndroidJNIEnv());
|
||||
if(env == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
jclass NativeApp = env->FindClass("com/izzy2lost/psx2/NativeApp");
|
||||
jmethodID openContentUriMode = env->GetStaticMethodID(NativeApp, "openContentUriMode", "(Ljava/lang/String;Ljava/lang/String;)I");
|
||||
jstring j_filename = env->NewStringUTF(filename);
|
||||
jstring j_mode = env->NewStringUTF(mode);
|
||||
int fd = env->CallStaticIntMethod(NativeApp, openContentUriMode, j_filename, j_mode);
|
||||
return fd;
|
||||
}
|
||||
|
||||
std::string ResolveSafChildUriJNI(const char* subdir, const char* filename, bool create)
|
||||
{
|
||||
auto *env = static_cast<JNIEnv *>(SDL_GetAndroidJNIEnv());
|
||||
if(env == nullptr) return {};
|
||||
jclass NativeApp = env->FindClass("com/izzy2lost/psx2/NativeApp");
|
||||
jmethodID resolve = env->GetStaticMethodID(NativeApp, "resolveSafChildUri", "(Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;");
|
||||
jstring j_sub = env->NewStringUTF(subdir);
|
||||
jstring j_file = env->NewStringUTF(filename);
|
||||
jstring j_uri = (jstring)env->CallStaticObjectMethod(NativeApp, resolve, j_sub, j_file, (jboolean)create);
|
||||
if (!j_uri) return {};
|
||||
const char* cstr = env->GetStringUTFChars(j_uri, nullptr);
|
||||
std::string ret(cstr);
|
||||
env->ReleaseStringUTFChars(j_uri, cstr);
|
||||
env->DeleteLocalRef(j_uri);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@@ -1445,3 +1593,42 @@ bool Host::InNoGUIMode()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// JNI: report if a SAF Data Root is configured
|
||||
bool HasSafDataRootJNI()
|
||||
{
|
||||
JNIEnv* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (!env) return false;
|
||||
jclass cls = env->FindClass("com/izzy2lost/psx2/NativeApp");
|
||||
if (!cls) return false;
|
||||
jmethodID mid = env->GetStaticMethodID(cls, "hasSafDataRoot", "()Z");
|
||||
if (!mid) return false;
|
||||
jboolean res = env->CallStaticBooleanMethod(cls, mid);
|
||||
return (res == JNI_TRUE);
|
||||
}
|
||||
static std::vector<std::string> SafListFilesJNI(const char* subdir)
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
JNIEnv* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (!env) return ret;
|
||||
jclass cls = env->FindClass("com/izzy2lost/psx2/NativeApp");
|
||||
if (!cls) return ret;
|
||||
jmethodID mid = env->GetStaticMethodID(cls, "listSafFilenames", "(Ljava/lang/String;)[Ljava/lang/String;");
|
||||
if (!mid) return ret;
|
||||
jstring j_sub = env->NewStringUTF(subdir);
|
||||
jobjectArray arr = (jobjectArray)env->CallStaticObjectMethod(cls, mid, j_sub);
|
||||
env->DeleteLocalRef(j_sub);
|
||||
if (!arr) return ret;
|
||||
jsize n = env->GetArrayLength(arr);
|
||||
ret.reserve(n);
|
||||
for (jsize i = 0; i < n; i++) {
|
||||
jstring s = (jstring)env->GetObjectArrayElement(arr, i);
|
||||
if (!s) continue;
|
||||
const char* cs = env->GetStringUTFChars(s, nullptr);
|
||||
if (cs) ret.emplace_back(cs);
|
||||
env->ReleaseStringUTFChars(s, cs);
|
||||
env->DeleteLocalRef(s);
|
||||
}
|
||||
env->DeleteLocalRef(arr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@ INISettingsInterface::~INISettingsInterface()
|
||||
|
||||
bool INISettingsInterface::Load()
|
||||
{
|
||||
if (m_filename.empty())
|
||||
return false;
|
||||
if (m_filename.empty())
|
||||
return false;
|
||||
|
||||
std::unique_lock lock(s_ini_load_save_mutex);
|
||||
SI_Error err = SI_FAIL;
|
||||
auto fp = FileSystem::OpenManagedCFile(m_filename.c_str(), "rb");
|
||||
if (fp)
|
||||
err = m_ini.LoadFile(fp.get());
|
||||
std::unique_lock lock(s_ini_load_save_mutex);
|
||||
SI_Error err = SI_FAIL;
|
||||
auto fp = FileSystem::OpenManagedCFile(m_filename.c_str(), "rb");
|
||||
if (fp)
|
||||
err = m_ini.LoadFile(fp.get());
|
||||
|
||||
return (err == SI_OK);
|
||||
return (err == SI_OK);
|
||||
}
|
||||
|
||||
bool INISettingsInterface::Save(Error* error)
|
||||
@@ -95,29 +95,37 @@ bool INISettingsInterface::Save(Error* error)
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_lock lock(s_ini_load_save_mutex);
|
||||
std::string temp_filename;
|
||||
std::FILE* fp = GetTemporaryFile(&temp_filename, m_filename, "wb", error);
|
||||
SI_Error err = SI_FAIL;
|
||||
if (fp)
|
||||
{
|
||||
err = m_ini.SaveFile(fp, false);
|
||||
std::fclose(fp);
|
||||
std::unique_lock lock(s_ini_load_save_mutex);
|
||||
SI_Error err = SI_FAIL;
|
||||
if (m_filename.rfind("saf://", 0) == 0)
|
||||
{
|
||||
// Direct write to SAF file; no temp/rename
|
||||
auto fp = FileSystem::OpenManagedCFile(m_filename.c_str(), "wb", error);
|
||||
if (fp)
|
||||
err = m_ini.SaveFile(fp.get(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string temp_filename;
|
||||
std::FILE* fp = GetTemporaryFile(&temp_filename, m_filename, "wb", error);
|
||||
if (fp)
|
||||
{
|
||||
err = m_ini.SaveFile(fp, false);
|
||||
std::fclose(fp);
|
||||
|
||||
if (err != SI_OK)
|
||||
{
|
||||
Error::SetStringFmt(error, "INI SaveFile() failed: {}", static_cast<int>(err));
|
||||
|
||||
// remove temporary file
|
||||
FileSystem::DeleteFilePath(temp_filename.c_str());
|
||||
}
|
||||
else if (!FileSystem::RenamePath(temp_filename.c_str(), m_filename.c_str(), error))
|
||||
{
|
||||
Console.Error("Failed to rename '%s' to '%s'", temp_filename.c_str(), m_filename.c_str());
|
||||
FileSystem::DeleteFilePath(temp_filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (err != SI_OK)
|
||||
{
|
||||
Error::SetStringFmt(error, "INI SaveFile() failed: {}", static_cast<int>(err));
|
||||
FileSystem::DeleteFilePath(temp_filename.c_str());
|
||||
}
|
||||
else if (!FileSystem::RenamePath(temp_filename.c_str(), m_filename.c_str(), error))
|
||||
{
|
||||
Console.Error("Failed to rename '%s' to '%s'", temp_filename.c_str(), m_filename.c_str());
|
||||
FileSystem::DeleteFilePath(temp_filename.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (err != SI_OK)
|
||||
{
|
||||
|
||||
@@ -2259,7 +2259,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
|
||||
Cheats = LoadPathFromSettings(si, DataRoot, "Cheats", "cheats");
|
||||
Patches = LoadPathFromSettings(si, DataRoot, "Patches", "patches");
|
||||
Covers = LoadPathFromSettings(si, DataRoot, "Covers", "covers");
|
||||
GameSettings = LoadPathFromSettings(si, DataRoot, "GameSettings", "gamesettings");
|
||||
GameSettings = LoadPathFromSettings(si, DataRoot, "GameSettings", "gamesettings");
|
||||
UserResources = LoadPathFromSettings(si, DataRoot, "UserResources", "resources");
|
||||
Cache = LoadPathFromSettings(si, DataRoot, "Cache", "cache");
|
||||
Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures");
|
||||
@@ -2276,7 +2276,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
|
||||
Console.WriteLn("Cheats Directory: %s", Cheats.c_str());
|
||||
Console.WriteLn("Patches Directory: %s", Patches.c_str());
|
||||
Console.WriteLn("Covers Directory: %s", Covers.c_str());
|
||||
Console.WriteLn("Game Settings Directory: %s", GameSettings.c_str());
|
||||
Console.WriteLn("Game Settings Directory: %s", GameSettings.c_str());
|
||||
Console.WriteLn("Resources Directory: %s", Resources.c_str());
|
||||
Console.WriteLn("User Resources Directory: %s", UserResources.c_str());
|
||||
Console.WriteLn("Cache Directory: %s", Cache.c_str());
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
|
||||
#include <csetjmp>
|
||||
#include <png.h>
|
||||
#if defined(__ANDROID__)
|
||||
// includes previously used for fd-based zip sinks; left guarded for future use
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
using namespace R5900;
|
||||
|
||||
@@ -1040,29 +1045,29 @@ static bool SaveState_AddToZip(zip_t* zf, ArchiveEntryList* srclist, SaveStateSc
|
||||
|
||||
bool SaveState_ZipToDisk(std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot, const char* filename)
|
||||
{
|
||||
zip_error_t ze = {};
|
||||
zip_source_t* zs = zip_source_file_create(filename, 0, 0, &ze);
|
||||
zip_t* zf = nullptr;
|
||||
if (zs && !(zf = zip_open_from_source(zs, ZIP_CREATE | ZIP_TRUNCATE, &ze)))
|
||||
{
|
||||
Console.Error("Failed to open zip file '%s' for save state: %s", filename, zip_error_strerror(&ze));
|
||||
zip_error_t ze = {};
|
||||
zip_source_t* zs = zip_source_file_create(filename, 0, 0, &ze);
|
||||
zip_t* zf = nullptr;
|
||||
if (zs && !(zf = zip_open_from_source(zs, ZIP_CREATE | ZIP_TRUNCATE, &ze)))
|
||||
{
|
||||
Console.Error("Failed to open zip file '%s' for save state: %s", filename, zip_error_strerror(&ze));
|
||||
|
||||
// have to clean up source
|
||||
zip_source_free(zs);
|
||||
return false;
|
||||
}
|
||||
// have to clean up source
|
||||
zip_source_free(zs);
|
||||
return false;
|
||||
}
|
||||
|
||||
// discard zip file if we fail saving something
|
||||
if (!SaveState_AddToZip(zf, srclist.get(), screenshot.get()))
|
||||
{
|
||||
Console.Error("Failed to save state to zip file '%s'", filename);
|
||||
zip_discard(zf);
|
||||
return false;
|
||||
}
|
||||
// discard zip file if we fail saving something
|
||||
if (!SaveState_AddToZip(zf, srclist.get(), screenshot.get()))
|
||||
{
|
||||
Console.Error("Failed to save state to zip file '%s'", filename);
|
||||
zip_discard(zf);
|
||||
return false;
|
||||
}
|
||||
|
||||
// force the zip to close, this is the expensive part with libzip.
|
||||
zip_close(zf);
|
||||
return true;
|
||||
// force the zip to close, this is the expensive part with libzip.
|
||||
zip_close(zf);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SaveState_ReadScreenshot(const std::string& filename, u32* out_width, u32* out_height, std::vector<u32>* out_pixels)
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
#include "common/Darwin/DarwinMisc.h"
|
||||
#endif
|
||||
|
||||
#ifdef __ANDROID__
|
||||
// JNI helper implemented in native-lib.cpp to query SAF Data Root presence
|
||||
bool HasSafDataRootJNI();
|
||||
#endif
|
||||
|
||||
namespace VMManager
|
||||
{
|
||||
static void SetDefaultLoggingSettings(SettingsInterface& si);
|
||||
@@ -543,8 +548,19 @@ bool VMManager::Internal::CheckSettingsVersion()
|
||||
void VMManager::Internal::LoadStartupSettings()
|
||||
{
|
||||
SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer();
|
||||
EmuFolders::LoadConfig(*bsi);
|
||||
EmuFolders::EnsureFoldersExist();
|
||||
EmuFolders::LoadConfig(*bsi);
|
||||
EmuFolders::EnsureFoldersExist();
|
||||
#ifdef __ANDROID__
|
||||
// Redirect cheats and patches to SAF Data Folder if available
|
||||
if (HasSafDataRootJNI())
|
||||
{
|
||||
EmuFolders::Cheats = "saf://cheats";
|
||||
EmuFolders::Patches = "saf://patches";
|
||||
// Route textures and screenshots to SAF; savestates remain on internal due to libzip constraints
|
||||
EmuFolders::Textures = "saf://textures";
|
||||
EmuFolders::Snapshots = "saf://snaps";
|
||||
}
|
||||
#endif
|
||||
|
||||
// We need to create the console window early, otherwise it appears behind the main window.
|
||||
UpdateLoggingSettings(*bsi);
|
||||
@@ -767,11 +783,17 @@ bool VMManager::ReloadGameSettings()
|
||||
|
||||
std::string VMManager::GetGameSettingsPath(const std::string_view game_serial, u32 game_crc)
|
||||
{
|
||||
std::string sanitized_serial(Path::SanitizeFileName(game_serial));
|
||||
std::string sanitized_serial(Path::SanitizeFileName(game_serial));
|
||||
|
||||
return game_serial.empty() ?
|
||||
Path::Combine(EmuFolders::GameSettings, fmt::format("{:08X}.ini", game_crc)) :
|
||||
Path::Combine(EmuFolders::GameSettings, fmt::format("{}_{:08X}.ini", sanitized_serial, game_crc));
|
||||
std::string base = EmuFolders::GameSettings;
|
||||
#ifdef __ANDROID__
|
||||
// If the app has a SAF Data Root, direct per-game settings into it via saf:// scheme
|
||||
if (HasSafDataRootJNI())
|
||||
base = "saf://gamesettings";
|
||||
#endif
|
||||
return game_serial.empty() ?
|
||||
Path::Combine(base, fmt::format("{:08X}.ini", game_crc)) :
|
||||
Path::Combine(base, fmt::format("{}_{:08X}.ini", sanitized_serial, game_crc));
|
||||
}
|
||||
|
||||
std::string VMManager::GetDiscOverrideFromGameSettings(const std::string& elf_path)
|
||||
@@ -3694,3 +3716,4 @@ void VMManager::PollDiscordPresence()
|
||||
|
||||
Discord_RunCallbacks();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class CheatsDialogFragment extends DialogFragment {
|
||||
|
||||
private ArrayAdapter<String> adapter;
|
||||
private ArrayList<String> items = new ArrayList<>();
|
||||
|
||||
private static final int REQ_IMPORT_PNACH = 1001;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Context ctx = requireContext();
|
||||
View view = LayoutInflater.from(ctx).inflate(R.layout.simple_list, null, false);
|
||||
ListView lv = view.findViewById(android.R.id.list);
|
||||
adapter = new ArrayAdapter<>(ctx, android.R.layout.simple_list_item_1, items);
|
||||
lv.setAdapter(adapter);
|
||||
lv.setOnItemLongClickListener((parent, v, position, id) -> {
|
||||
String name = items.get(position);
|
||||
deleteCheat(name);
|
||||
return true;
|
||||
});
|
||||
|
||||
refreshList();
|
||||
|
||||
return new MaterialAlertDialogBuilder(ctx, com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setCustomTitle(UiUtils.centeredDialogTitle(ctx, "Manage Cheats"))
|
||||
.setView(view)
|
||||
.setNegativeButton("Close", null)
|
||||
.setPositiveButton("Import For Game", (d, w) -> startImport())
|
||||
.create();
|
||||
}
|
||||
|
||||
private void refreshList() {
|
||||
items.clear();
|
||||
String[] names = NativeApp.listSafFilenames("cheats");
|
||||
if (names != null && names.length > 0) {
|
||||
for (String n : names) if (n != null && n.endsWith(".pnach")) items.add(n);
|
||||
} else {
|
||||
java.io.File dir = new java.io.File(requireContext().getExternalFilesDir(null), "cheats");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
java.io.File[] arr = dir.listFiles((f, n) -> n != null && n.endsWith(".pnach"));
|
||||
if (arr != null) for (java.io.File f : arr) items.add(f.getName());
|
||||
}
|
||||
if (adapter != null) adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void startImport() {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQ_IMPORT_PNACH);
|
||||
} catch (Throwable t) {
|
||||
Toast.makeText(requireContext(), "No file picker available", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQ_IMPORT_PNACH && resultCode == android.app.Activity.RESULT_OK && data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri == null) return;
|
||||
String serial = null;
|
||||
try { serial = NativeApp.getCurrentGameSerial(); } catch (Throwable ignored) {}
|
||||
if (serial == null || serial.isEmpty()) {
|
||||
Toast.makeText(requireContext(), "Unknown game serial", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
String outName = serial + ".pnach";
|
||||
android.net.Uri dataRoot = SafManager.getDataRootUri(requireContext());
|
||||
if (dataRoot != null) {
|
||||
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(requireContext(), new String[]{"cheats"}, outName, "application/octet-stream");
|
||||
if (target != null) {
|
||||
try (InputStream in = requireContext().getContentResolver().openInputStream(uri)) {
|
||||
if (in != null && SafManager.copyFromStream(requireContext(), in, target.getUri())) {
|
||||
Toast.makeText(requireContext(), "Imported to Data Folder", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Import failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(requireContext(), "Import failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
java.io.File dir = new java.io.File(requireContext().getExternalFilesDir(null), "cheats");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
java.io.File out = new java.io.File(dir, outName);
|
||||
try (InputStream in = requireContext().getContentResolver().openInputStream(uri);
|
||||
java.io.FileOutputStream os = new java.io.FileOutputStream(out)) {
|
||||
if (in != null) {
|
||||
byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) != -1) os.write(buf, 0, n);
|
||||
os.flush();
|
||||
Toast.makeText(requireContext(), "Imported", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(requireContext(), "Import failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
refreshList();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteCheat(String name) {
|
||||
android.net.Uri dataRoot = SafManager.getDataRootUri(requireContext());
|
||||
boolean ok = false;
|
||||
if (dataRoot != null) {
|
||||
androidx.documentfile.provider.DocumentFile f = SafManager.getChild(requireContext(), new String[]{"cheats"}, name);
|
||||
if (f != null) ok = f.delete();
|
||||
} else {
|
||||
java.io.File dir = new java.io.File(requireContext().getExternalFilesDir(null), "cheats");
|
||||
java.io.File f = new java.io.File(dir, name);
|
||||
ok = f.delete();
|
||||
}
|
||||
if (ok) {
|
||||
Toast.makeText(requireContext(), "Deleted", Toast.LENGTH_SHORT).show();
|
||||
refreshList();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Delete failed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class GameSettingsDialogFragment extends DialogFragment {
|
||||
android.net.Uri dataRoot = SafManager.getDataRootUri(ctx);
|
||||
if (dataRoot != null) {
|
||||
String subdir = mImportAsCheats ? "cheats" : "patches";
|
||||
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(ctx, new String[]{subdir}, gameSerial + ".pnach", "text/plain");
|
||||
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(ctx, new String[]{subdir}, gameSerial + ".pnach", "application/octet-stream");
|
||||
if (target != null) {
|
||||
try (java.io.InputStream in2 = cr.openInputStream(android.net.Uri.fromFile(outFile))) {
|
||||
SafManager.copyFromStream(ctx, in2, target.getUri());
|
||||
@@ -428,7 +428,7 @@ public class GameSettingsDialogFragment extends DialogFragment {
|
||||
android.net.Uri dataRoot = SafManager.getDataRootUri(ctx);
|
||||
if (dataRoot != null) {
|
||||
try {
|
||||
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(ctx, new String[]{"gamesettings"}, fileName, "text/plain");
|
||||
androidx.documentfile.provider.DocumentFile target = SafManager.createChild(ctx, new String[]{"gamesettings"}, fileName, "application/octet-stream");
|
||||
if (target != null) {
|
||||
byte[] data = sb.toString().getBytes("UTF-8");
|
||||
SafManager.writeBytes(ctx, target.getUri(), data);
|
||||
|
||||
@@ -521,6 +521,13 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
SettingsDialogFragment dialog = new SettingsDialogFragment();
|
||||
dialog.show(fm, "settings_dialog");
|
||||
});
|
||||
// Long-press to open Cheats manager
|
||||
btn_settings.setOnLongClickListener(v -> {
|
||||
FragmentManager fm2 = getSupportFragmentManager();
|
||||
CheatsDialogFragment cd = new CheatsDialogFragment();
|
||||
cd.show(fm2, "cheats_dialog");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle all UI visibility (including controls)
|
||||
@@ -1742,3 +1749,4 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class NativeApp {
|
||||
initialize(externalFilesDir.getAbsolutePath(), android.os.Build.VERSION.SDK_INT);
|
||||
}
|
||||
|
||||
public static native void initialize(String path, int apiVer);
|
||||
public static native void initialize(String path, int apiVer);
|
||||
public static native String getGameTitle(String path);
|
||||
public static native String getGameTitleFromUri(String gameUri);
|
||||
public static native String getGameSerial();
|
||||
@@ -69,6 +69,7 @@ public class NativeApp {
|
||||
// Texture loading options for texture packs
|
||||
public static native void setLoadTextures(boolean enabled);
|
||||
public static native void setAsyncTextureLoading(boolean enabled);
|
||||
public static native void setPrecacheTextureReplacements(boolean enabled);
|
||||
public static native void setBlendingAccuracy(int level);
|
||||
|
||||
// Shade Boost (brightness/contrast/saturation)
|
||||
@@ -154,7 +155,7 @@ public class NativeApp {
|
||||
public static native void onNativeSurfaceChanged(Surface surface, int w, int h);
|
||||
public static native void onNativeSurfaceDestroyed();
|
||||
|
||||
public static native boolean runVMThread(String path);
|
||||
public static native boolean runVMThread(String path);
|
||||
|
||||
public static native void pause();
|
||||
public static native void resume();
|
||||
@@ -166,17 +167,149 @@ public class NativeApp {
|
||||
public static native byte[] getImageSlot(int slot);
|
||||
|
||||
// Call jni
|
||||
public static int openContentUri(String uriString) {
|
||||
Context _context = getContext();
|
||||
if(_context != null) {
|
||||
ContentResolver _contentResolver = _context.getContentResolver();
|
||||
try {
|
||||
ParcelFileDescriptor filePfd = _contentResolver.openFileDescriptor(Uri.parse(uriString), "r");
|
||||
if (filePfd != null) {
|
||||
return filePfd.detachFd(); // Take ownership of the fd.
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
public static int openContentUri(String uriString) {
|
||||
Context _context = getContext();
|
||||
if(_context != null) {
|
||||
ContentResolver _contentResolver = _context.getContentResolver();
|
||||
try {
|
||||
ParcelFileDescriptor filePfd = _contentResolver.openFileDescriptor(Uri.parse(uriString), "r");
|
||||
if (filePfd != null) {
|
||||
return filePfd.detachFd(); // Take ownership of the fd.
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Indicates whether a SAF Data Root has been selected by the user.
|
||||
public static boolean hasSafDataRoot() {
|
||||
return SafManager.getDataRootUri(getContext()) != null;
|
||||
}
|
||||
|
||||
// Open a SAF content Uri with the requested mode ("r", "w", or "rw"). Returns a detached FD or -1.
|
||||
public static int openContentUriMode(String uriString, String mode) {
|
||||
Context _context = getContext();
|
||||
if(_context != null) {
|
||||
ContentResolver _contentResolver = _context.getContentResolver();
|
||||
try {
|
||||
ParcelFileDescriptor filePfd = _contentResolver.openFileDescriptor(Uri.parse(uriString), mode);
|
||||
if (filePfd != null) {
|
||||
return filePfd.detachFd();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Resolve a child document Uri within the SAF Data Root.
|
||||
// subdir: e.g., "gamesettings", filename: e.g., "SLUS-12345.ini". If create is true, creates file.
|
||||
public static String resolveSafChildUri(String subdir, String filename, boolean create) {
|
||||
Uri root = SafManager.getDataRootUri(getContext());
|
||||
if (root == null) return null;
|
||||
try {
|
||||
androidx.documentfile.provider.DocumentFile df;
|
||||
if (create) {
|
||||
df = SafManager.createChild(getContext(), new String[]{subdir}, filename, "application/octet-stream");
|
||||
} else {
|
||||
df = SafManager.getChild(getContext(), new String[]{subdir}, filename);
|
||||
}
|
||||
return (df != null) ? df.getUri().toString() : null;
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve a file path relative to the SAF Data Root. Accepts nested paths like
|
||||
// "textures/SLUS-12345/replacements/subdir/file.png". If create is true, creates the file.
|
||||
public static String resolveSafPathUri(String relativePath, boolean create) {
|
||||
if (relativePath == null) return null;
|
||||
Uri root = SafManager.getDataRootUri(getContext());
|
||||
if (root == null) return null;
|
||||
try {
|
||||
String[] parts = relativePath.split("/");
|
||||
if (parts.length == 0) return null;
|
||||
String[] dirSegs;
|
||||
String filename;
|
||||
if (parts.length == 1) {
|
||||
dirSegs = new String[]{};
|
||||
filename = parts[0];
|
||||
} else {
|
||||
dirSegs = new String[parts.length - 1];
|
||||
System.arraycopy(parts, 0, dirSegs, 0, parts.length - 1);
|
||||
filename = parts[parts.length - 1];
|
||||
}
|
||||
androidx.documentfile.provider.DocumentFile df;
|
||||
if (create) {
|
||||
df = SafManager.createChild(getContext(), dirSegs, filename, "application/octet-stream");
|
||||
} else {
|
||||
df = SafManager.getChild(getContext(), dirSegs, filename);
|
||||
}
|
||||
return (df != null) ? df.getUri().toString() : null;
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
// List files under a relative SAF directory recursively. Returns full relative paths from the root.
|
||||
public static String[] listSafRecursiveFiles(String relativeDir) {
|
||||
java.util.ArrayList<String> out = new java.util.ArrayList<>();
|
||||
try {
|
||||
androidx.documentfile.provider.DocumentFile base = SafManager.getOrCreateDir(getContext(), relativeDir.split("/"));
|
||||
if (base == null || !base.exists()) return new String[0];
|
||||
walkDirRecursive(base, relativeDir, out);
|
||||
} catch (Throwable ignored) { }
|
||||
return out.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static void walkDirRecursive(androidx.documentfile.provider.DocumentFile dir, String relPrefix, java.util.ArrayList<String> out) {
|
||||
androidx.documentfile.provider.DocumentFile[] arr = dir.listFiles();
|
||||
if (arr == null) return;
|
||||
for (androidx.documentfile.provider.DocumentFile f : arr) {
|
||||
if (f == null) continue;
|
||||
String name = f.getName();
|
||||
if (name == null || name.isEmpty()) continue;
|
||||
if (f.isDirectory()) {
|
||||
walkDirRecursive(f, relPrefix + "/" + name, out);
|
||||
} else if (f.isFile()) {
|
||||
out.add(relPrefix + "/" + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List files directly under a relative SAF directory (non-recursive). Returns full relative paths.
|
||||
public static String[] listSafFilesFlat(String relativeDir) {
|
||||
java.util.ArrayList<String> out = new java.util.ArrayList<>();
|
||||
try {
|
||||
androidx.documentfile.provider.DocumentFile dir = SafManager.getOrCreateDir(getContext(), relativeDir.split("/"));
|
||||
if (dir == null || !dir.isDirectory()) return new String[0];
|
||||
androidx.documentfile.provider.DocumentFile[] arr = dir.listFiles();
|
||||
if (arr != null) {
|
||||
for (androidx.documentfile.provider.DocumentFile f : arr) {
|
||||
if (f != null && f.isFile()) {
|
||||
String name = f.getName();
|
||||
if (name != null && !name.isEmpty()) out.add(relativeDir + "/" + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return out.toArray(new String[0]);
|
||||
}
|
||||
|
||||
// List filenames (files only) under a SAF subdirectory (e.g., "cheats", "patches").
|
||||
public static String[] listSafFilenames(String subdir) {
|
||||
try {
|
||||
androidx.documentfile.provider.DocumentFile dir = SafManager.getOrCreateDir(getContext(), subdir);
|
||||
if (dir == null || !dir.isDirectory()) return new String[0];
|
||||
androidx.documentfile.provider.DocumentFile[] arr = dir.listFiles();
|
||||
java.util.ArrayList<String> out = new java.util.ArrayList<>();
|
||||
if (arr != null) {
|
||||
for (androidx.documentfile.provider.DocumentFile f : arr) {
|
||||
if (f != null && f.isFile()) {
|
||||
String name = f.getName();
|
||||
if (name != null && !name.isEmpty()) out.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.toArray(new String[0]);
|
||||
} catch (Throwable ignored) { }
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
boolean noInterlacingPatches = prefs.getBoolean("no_interlacing_patches", true);
|
||||
boolean loadTextures = prefs.getBoolean("load_textures", false);
|
||||
boolean asyncTextureLoading = prefs.getBoolean("async_texture_loading", true);
|
||||
boolean precacheTextures = prefs.getBoolean("precache_textures", false);
|
||||
boolean hudVisible = prefs.getBoolean("hud_visible", false);
|
||||
|
||||
// Debug logging
|
||||
@@ -58,6 +59,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
NativeApp.setNoInterlacingPatches(noInterlacingPatches);
|
||||
NativeApp.setLoadTextures(loadTextures);
|
||||
NativeApp.setAsyncTextureLoading(asyncTextureLoading);
|
||||
NativeApp.setPrecacheTextureReplacements(precacheTextures);
|
||||
NativeApp.setHudVisible(hudVisible);
|
||||
|
||||
// Set brighter default brightness (60 instead of 50)
|
||||
@@ -85,6 +87,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
MaterialSwitch swNoInterlacing = view.findViewById(R.id.sw_no_interlacing);
|
||||
MaterialSwitch swLoadTextures = view.findViewById(R.id.sw_load_textures);
|
||||
MaterialSwitch swAsyncTextureLoading = view.findViewById(R.id.sw_async_texture_loading);
|
||||
MaterialSwitch swPrecacheTextures = view.findViewById(R.id.sw_precache_textures);
|
||||
MaterialSwitch swDevHud = view.findViewById(R.id.sw_dev_hud);
|
||||
View btnPower = view.findViewById(R.id.btn_power);
|
||||
View btnReboot = view.findViewById(R.id.btn_reboot);
|
||||
@@ -187,6 +190,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
boolean savedNoInterlacing = prefs.getBoolean("no_interlacing_patches", true);
|
||||
boolean savedLoadTextures = prefs.getBoolean("load_textures", false);
|
||||
boolean savedAsyncTextureLoading = prefs.getBoolean("async_texture_loading", true);
|
||||
boolean savedPrecacheTextures = prefs.getBoolean("precache_textures", false);
|
||||
boolean savedHud = prefs.getBoolean("hud_visible", false);
|
||||
boolean savedCheatsGlobal = prefs.getBoolean("enable_cheats", false);
|
||||
int savedBlending = prefs.getInt("blending_accuracy", 1);
|
||||
@@ -213,6 +217,7 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
swNoInterlacing.setChecked(savedNoInterlacing);
|
||||
swLoadTextures.setChecked(savedLoadTextures);
|
||||
swAsyncTextureLoading.setChecked(savedAsyncTextureLoading);
|
||||
if (swPrecacheTextures != null) swPrecacheTextures.setChecked(savedPrecacheTextures);
|
||||
if (swDevHud != null) swDevHud.setChecked(savedHud);
|
||||
MaterialSwitch swCheatsGlobal = view.findViewById(R.id.sw_enable_cheats_global);
|
||||
if (swCheatsGlobal != null) swCheatsGlobal.setChecked(savedCheatsGlobal);
|
||||
@@ -235,7 +240,8 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
boolean widescreenPatches = swWidescreen.isChecked();
|
||||
boolean noInterlacingPatches = swNoInterlacing.isChecked();
|
||||
boolean loadTextures = swLoadTextures.isChecked();
|
||||
boolean asyncTextureLoading = swAsyncTextureLoading.isChecked();
|
||||
boolean asyncTextureLoading = swAsyncTextureLoading.isChecked();
|
||||
boolean precacheTextureReplacements = swPrecacheTextures != null && swPrecacheTextures.isChecked();
|
||||
boolean hudVisible = (swDevHud != null && swDevHud.isChecked());
|
||||
boolean enableCheatsGlobal = swCheatsGlobal != null && swCheatsGlobal.isChecked();
|
||||
|
||||
@@ -250,17 +256,20 @@ public class SettingsDialogFragment extends DialogFragment {
|
||||
.putBoolean("no_interlacing_patches", noInterlacingPatches)
|
||||
.putBoolean("load_textures", loadTextures)
|
||||
.putBoolean("async_texture_loading", asyncTextureLoading)
|
||||
.putBoolean("precache_textures", precacheTextureReplacements)
|
||||
.putBoolean("hud_visible", hudVisible)
|
||||
.putBoolean("enable_cheats", enableCheatsGlobal)
|
||||
.apply();
|
||||
|
||||
// Apply in one batch to avoid repeated ApplySettings calls
|
||||
try {
|
||||
NativeApp.applyGlobalSettingsBatch(renderer, scale, aspectRatio, blendingLevel,
|
||||
widescreenPatches, noInterlacingPatches, loadTextures, asyncTextureLoading, hudVisible);
|
||||
} catch (Throwable t) {
|
||||
android.util.Log.e("SettingsDialog", "Batch apply failed: " + t.getMessage());
|
||||
}
|
||||
try {
|
||||
NativeApp.applyGlobalSettingsBatch(renderer, scale, aspectRatio, blendingLevel,
|
||||
widescreenPatches, noInterlacingPatches, loadTextures, asyncTextureLoading, hudVisible);
|
||||
// Apply precache separately (not part of the batch JNI)
|
||||
NativeApp.setPrecacheTextureReplacements(precacheTextureReplacements);
|
||||
} catch (Throwable t) {
|
||||
android.util.Log.e("SettingsDialog", "Apply failed: " + t.getMessage());
|
||||
}
|
||||
|
||||
// Refresh quick UI (renderer label) if hosting activity is MainActivity
|
||||
try {
|
||||
|
||||
@@ -208,6 +208,14 @@
|
||||
android:text="Async Texture Loading"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.Material3.CompoundButton.MaterialSwitch"/>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/sw_precache_textures"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Precache Texture Replacements"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.Material3.CompoundButton.MaterialSwitch"/>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
<ListView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:dividerHeight="8dp"/>
|
||||
</FrameLayout>
|
||||
Reference in New Issue
Block a user