#define _LARGEFILE64_SOURCE #include "GpFileSystem_Android.h" #include "GpIOStream.h" #include "IGpDirectoryCursor.h" #include "IGpSystemServices.h" #include "IGpMutex.h" #include "IGpThreadRelay.h" #include "VirtualDirectory.h" #include "PLDrivers.h" #include "SDL.h" #include "SDL_rwops.h" #include #include #include #include #include #include #include #include "UTF8.h" JNIEXPORT void JNICALL nativePostSourceExportRequest(JNIEnv *env, jclass cls, jboolean cancelled, jint fd, jobject pfd); static JNINativeMethod GpFileSystemAPI_tab[] = { { "nativePostSourceExportRequest", "(ZILjava/lang/Object;)V", reinterpret_cast(nativePostSourceExportRequest) }, }; class GpFileStream_PFD final : public GpIOStream { public: GpFileStream_PFD(GpFileSystem_Android *fs, int fd, jobject pfd, bool readOnly, bool writeOnly); ~GpFileStream_PFD(); size_t Read(void *bytesOut, size_t size) override; size_t Write(const void *bytes, size_t size) override; bool IsSeekable() const override; bool IsReadOnly() const override; bool IsWriteOnly() const override; bool SeekStart(GpUFilePos_t loc) override; bool SeekCurrent(GpFilePos_t loc) override; bool SeekEnd(GpUFilePos_t loc) override; GpUFilePos_t Size() const override; GpUFilePos_t Tell() const override; void Close() override; void Flush() override; private: GpFileSystem_Android *m_fs; int m_fd; jobject m_pfd; bool m_readOnly; bool m_writeOnly; }; class GpFileStream_SDLRWops final : public GpIOStream { public: GpFileStream_SDLRWops(SDL_RWops *f, bool readOnly, bool writeOnly); ~GpFileStream_SDLRWops(); size_t Read(void *bytesOut, size_t size) override; size_t Write(const void *bytes, size_t size) override; bool IsSeekable() const override; bool IsReadOnly() const override; bool IsWriteOnly() const override; bool SeekStart(GpUFilePos_t loc) override; bool SeekCurrent(GpFilePos_t loc) override; bool SeekEnd(GpUFilePos_t loc) override; GpUFilePos_t Size() const override; GpUFilePos_t Tell() const override; void Close() override; void Flush() override; private: SDL_RWops *m_rw; bool m_isReadOnly; bool m_isWriteOnly; }; class GpFileStream_Android_File final : public GpIOStream { public: GpFileStream_Android_File(FILE *f, int fd, bool readOnly, bool writeOnly); ~GpFileStream_Android_File(); size_t Read(void *bytesOut, size_t size) override; size_t Write(const void *bytes, size_t size) override; bool IsSeekable() const override; bool IsReadOnly() const override; bool IsWriteOnly() const override; bool SeekStart(GpUFilePos_t loc) override; bool SeekCurrent(GpFilePos_t loc) override; bool SeekEnd(GpUFilePos_t loc) override; GpUFilePos_t Size() const override; GpUFilePos_t Tell() const override; void Close() override; void Flush() override; private: FILE *m_f; int m_fd; bool m_seekable; bool m_isReadOnly; bool m_isWriteOnly; }; GpFileStream_PFD::GpFileStream_PFD(GpFileSystem_Android *fs, int fd, jobject pfd, bool readOnly, bool writeOnly) : m_fs(fs) , m_fd(fd) , m_readOnly(readOnly) , m_writeOnly(writeOnly) , m_pfd(pfd) { } GpFileStream_PFD::~GpFileStream_PFD() { m_fs->ClosePFD(m_pfd); } size_t GpFileStream_PFD::Read(void *bytesOut, size_t size) { if (m_writeOnly) return 0; return read(m_fd, bytesOut, size); } size_t GpFileStream_PFD::Write(const void *bytes, size_t size) { if (m_readOnly) return 0; return write(m_fd, bytes, size); } bool GpFileStream_PFD::IsSeekable() const { return true; } bool GpFileStream_PFD::IsReadOnly() const { return m_readOnly; } bool GpFileStream_PFD::IsWriteOnly() const { return m_writeOnly; } bool GpFileStream_PFD::SeekStart(GpUFilePos_t loc) { return lseek64(m_fd, loc, SEEK_SET) >= 0; } bool GpFileStream_PFD::SeekCurrent(GpFilePos_t loc) { return lseek64(m_fd, loc, SEEK_CUR) >= 0; } bool GpFileStream_PFD::SeekEnd(GpUFilePos_t loc) { return lseek64(m_fd, loc, SEEK_END) >= 0; } GpUFilePos_t GpFileStream_PFD::Size() const { struct stat64 s; if (fstat64(m_fd, &s) < 0) return 0; return static_cast(s.st_size); } GpUFilePos_t GpFileStream_PFD::Tell() const { return lseek64(m_fd, 0, SEEK_CUR); } void GpFileStream_PFD::Close() { this->~GpFileStream_PFD(); free(this); } void GpFileStream_PFD::Flush() { } GpFileStream_SDLRWops::GpFileStream_SDLRWops(SDL_RWops *f, bool readOnly, bool writeOnly) : m_rw(f) , m_isReadOnly(readOnly) , m_isWriteOnly(writeOnly) { } GpFileStream_SDLRWops::~GpFileStream_SDLRWops() { m_rw->close(m_rw); } size_t GpFileStream_SDLRWops::Read(void *bytesOut, size_t size) { return m_rw->read(m_rw, bytesOut, 1, size); } size_t GpFileStream_SDLRWops::Write(const void *bytes, size_t size) { return m_rw->write(m_rw, bytes, 1, size); } bool GpFileStream_SDLRWops::IsSeekable() const { return true; } bool GpFileStream_SDLRWops::IsReadOnly() const { return m_isReadOnly; } bool GpFileStream_SDLRWops::IsWriteOnly() const { return m_isWriteOnly; } bool GpFileStream_SDLRWops::SeekStart(GpUFilePos_t loc) { return m_rw->seek(m_rw, static_cast(loc), RW_SEEK_SET) >= 0; } bool GpFileStream_SDLRWops::SeekCurrent(GpFilePos_t loc) { return m_rw->seek(m_rw, static_cast(loc), RW_SEEK_CUR) >= 0; } bool GpFileStream_SDLRWops::SeekEnd(GpUFilePos_t loc) { return m_rw->seek(m_rw, -static_cast(loc), RW_SEEK_END) >= 0; } GpUFilePos_t GpFileStream_SDLRWops::Size() const { return m_rw->size(m_rw); } GpUFilePos_t GpFileStream_SDLRWops::GpFileStream_SDLRWops::Tell() const { return SDL_RWtell(m_rw); } void GpFileStream_SDLRWops::Close() { this->~GpFileStream_SDLRWops(); free(this); } void GpFileStream_SDLRWops::Flush() { } GpFileStream_Android_File::GpFileStream_Android_File(FILE *f, int fd, bool readOnly, bool writeOnly) : m_f(f) , m_fd(fd) , m_isReadOnly(readOnly) , m_isWriteOnly(writeOnly) { m_seekable = (fseek(m_f, 0, SEEK_CUR) == 0); } GpFileStream_Android_File::~GpFileStream_Android_File() { fclose(m_f); } size_t GpFileStream_Android_File::Read(void *bytesOut, size_t size) { if (m_isWriteOnly) return 0; return fread(bytesOut, 1, size, m_f); } size_t GpFileStream_Android_File::Write(const void *bytes, size_t size) { if (m_isReadOnly) return 0; return fwrite(bytes, 1, size, m_f); } bool GpFileStream_Android_File::IsSeekable() const { return m_seekable; } bool GpFileStream_Android_File::IsReadOnly() const { return m_isReadOnly; } bool GpFileStream_Android_File::IsWriteOnly() const { return m_isWriteOnly; } bool GpFileStream_Android_File::SeekStart(GpUFilePos_t loc) { if (!m_seekable) return false; fflush(m_f); return lseek64(m_fd, static_cast(loc), SEEK_SET) >= 0; } bool GpFileStream_Android_File::SeekCurrent(GpFilePos_t loc) { if (!m_seekable) return false; fflush(m_f); return lseek64(m_fd, static_cast(loc), SEEK_CUR) >= 0; } bool GpFileStream_Android_File::SeekEnd(GpUFilePos_t loc) { if (!m_seekable) return false; fflush(m_f); return lseek64(m_fd, -static_cast(loc), SEEK_END) >= 0; } GpUFilePos_t GpFileStream_Android_File::Size() const { fflush(m_f); struct stat64 s; if (fstat64(m_fd, &s) < 0) return 0; return static_cast(s.st_size); } GpUFilePos_t GpFileStream_Android_File::Tell() const { return static_cast(ftell(m_f)); } void GpFileStream_Android_File::Close() { this->~GpFileStream_Android_File(); free(this); } void GpFileStream_Android_File::Flush() { fflush(m_f); } bool GpFileSystem_Android::OpenSourceExportFD(PortabilityLayer::VirtualDirectory_t virtualDirectory, const char *const *paths, size_t numPaths, int &fd, jobject &pfd) { if (!m_sourceExportMutex) m_sourceExportMutex = PLDrivers::GetSystemServices()->CreateMutex(); m_sourceExportWaiting = true; m_sourceExportCancelled = false; JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jstring fname = jni->NewStringUTF(paths[0]); jni->CallVoidMethod(m_activity, this->m_selectSourceExportPathMID, fname); jni->DeleteLocalRef(fname); for (;;) { m_sourceExportMutex->Lock(); const bool isWaiting = m_sourceExportWaiting; m_sourceExportMutex->Unlock(); if (!isWaiting) break; m_delayCallback(5); } fd = m_sourceExportFD; pfd = m_sourceExportPFD; return !m_sourceExportCancelled; } bool GpFileSystem_Android::ResolvePath(PortabilityLayer::VirtualDirectory_t virtualDirectory, char const* const* paths, size_t numPaths, std::string &resolution, bool &isAsset) { const char *prefsAppend = nullptr; isAsset = false; switch (virtualDirectory) { case PortabilityLayer::VirtualDirectories::kApplicationData: resolution = std::string("Packaged") ; isAsset = true; break; case PortabilityLayer::VirtualDirectories::kGameData: resolution = std::string("Packaged/Houses"); isAsset = true; break; case PortabilityLayer::VirtualDirectories::kFonts: resolution = std::string("Resources"); isAsset = true; break; case PortabilityLayer::VirtualDirectories::kHighScores: prefsAppend = "HighScores"; break; case PortabilityLayer::VirtualDirectories::kUserData: prefsAppend = "Houses"; break; case PortabilityLayer::VirtualDirectories::kUserSaves: prefsAppend = "SavedGames"; break; case PortabilityLayer::VirtualDirectories::kPrefs: prefsAppend = "Prefs"; break; case PortabilityLayer::VirtualDirectories::kFontCache: prefsAppend = "FontCache"; break; default: return false; }; if (prefsAppend) { char *prefsDir = SDL_GetPrefPath("aerofoil", "aerofoil"); resolution = prefsDir; SDL_free(prefsDir); resolution += prefsAppend; } for (size_t i = 0; i < numPaths; i++) { resolution += "/"; resolution += paths[i]; } return true; } GpFileSystem_Android::GpFileSystem_Android() : m_activity(nullptr) , m_delayCallback(nullptr) , m_sourceExportMutex(nullptr) , m_sourceExportFD(0) , m_sourceExportWaiting(false) , m_sourceExportCancelled(false) , m_sourceExportPFD(nullptr) { } GpFileSystem_Android::~GpFileSystem_Android() { } void GpFileSystem_Android::InitJNI() { JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jclass fileSystemAPIClass = jni->FindClass("org/thecodedeposit/aerofoil/GpFileSystemAPI"); int registerStatus = jni->RegisterNatives(fileSystemAPIClass, GpFileSystemAPI_tab, sizeof(GpFileSystemAPI_tab) / sizeof(GpFileSystemAPI_tab[0])); jni->DeleteLocalRef(fileSystemAPIClass); jobject activityLR = static_cast(SDL_AndroidGetActivity()); jclass activityClassLR = static_cast(jni->GetObjectClass(activityLR)); m_scanAssetDirectoryMID = jni->GetMethodID(activityClassLR, "scanAssetDirectory", "(Ljava/lang/String;)[Ljava/lang/String;"); m_selectSourceExportPathMID = jni->GetMethodID(activityClassLR, "selectSourceExportPath", "(Ljava/lang/String;)V"); m_closeSourceExportPFDMID = jni->GetMethodID(activityClassLR, "closeSourceExportPFD", "(Ljava/lang/Object;)V"); m_activity = jni->NewGlobalRef(activityLR); jni->DeleteLocalRef(activityLR); jni->DeleteLocalRef(activityClassLR); char *prefsDir = SDL_GetPrefPath("aerofoil", "aerofoil"); size_t prefsDirLen = strlen(prefsDir); for (size_t i = 0; i < prefsDirLen; i++) { if (prefsDir[i] == '/') { prefsDir[i] = '\0'; int created = mkdir(prefsDir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); prefsDir[i] = '/'; } } const char *extensions[] = { "HighScores", "Houses", "SavedGames", "Prefs", "FontCache" }; for (size_t i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) { std::string prefsPath = std::string(prefsDir) + extensions[i]; int created = mkdir(prefsPath.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); int n = 0; } SDL_free(prefsDir); } void GpFileSystem_Android::ShutdownJNI() { JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jni->DeleteGlobalRef(m_activity); m_activity = nullptr; } bool GpFileSystem_Android::FileExists(PortabilityLayer::VirtualDirectory_t virtualDirectory, const char *path) { std::string resolvedPath; bool isAsset; if (!ResolvePath(virtualDirectory, &path, 1, resolvedPath, isAsset)) return false; if (isAsset) { SDL_RWops *rw = SDL_RWFromFile(resolvedPath.c_str(), "rb"); if (!rw) return false; SDL_RWclose(rw); return true; } struct stat s; return stat(resolvedPath.c_str(), &s) == 0; } bool GpFileSystem_Android::FileLocked(PortabilityLayer::VirtualDirectory_t virtualDirectory, const char *path, bool &exists) { std::string resolvedPath; bool isAsset; if (!ResolvePath(virtualDirectory, &path, 1, resolvedPath, isAsset)) { if (exists) exists = false; return false; } if (isAsset) { if (exists) exists = this->FileExists(virtualDirectory, path); return true; } int permissions = access(resolvedPath.c_str(), W_OK | F_OK); exists = ((permissions & F_OK) != 0); return ((permissions & W_OK) != 0); } GpIOStream *GpFileSystem_Android::OpenFileNested(PortabilityLayer::VirtualDirectory_t virtualDirectory, char const* const* subPaths, size_t numSubPaths, bool writeAccess, GpFileCreationDisposition_t createDisposition) { const char *mode = nullptr; bool canWrite = false; bool needResetPosition = false; switch (createDisposition) { case GpFileCreationDispositions::kCreateOrOverwrite: mode = "w+b"; break; case GpFileCreationDispositions::kCreateNew: mode = "x+b"; break; case GpFileCreationDispositions::kCreateOrOpen: mode = "a+b"; needResetPosition = true; break; case GpFileCreationDispositions::kOpenExisting: mode = writeAccess ? "r+b" : "rb"; break; case GpFileCreationDispositions::kOverwriteExisting: mode = "r+b"; break; default: return nullptr; }; if (virtualDirectory == PortabilityLayer::VirtualDirectories::kSourceExport) { void *objStorage = malloc(sizeof(GpFileStream_PFD)); if (!objStorage) return nullptr; int fd = 0; jobject pfd = nullptr; const bool resolved = OpenSourceExportFD(virtualDirectory, subPaths, numSubPaths, fd, pfd); if (!resolved) { free(objStorage); return nullptr; } return new (objStorage) GpFileStream_PFD(this, fd, pfd, false, true); } std::string resolvedPath; bool isAsset; if (!ResolvePath(virtualDirectory, subPaths, numSubPaths, resolvedPath, isAsset)) return nullptr; if (isAsset) { if (createDisposition == GpFileCreationDispositions::kOverwriteExisting || writeAccess) return nullptr; void *objStorage = malloc(sizeof(GpFileStream_SDLRWops)); if (!objStorage) return nullptr; SDL_RWops *rw = SDL_RWFromFile(resolvedPath.c_str(), mode); if (!rw) { free(objStorage); return nullptr; } return new (objStorage) GpFileStream_SDLRWops(rw, true, false); } else { void *objStorage = malloc(sizeof(GpFileStream_Android_File)); if (!objStorage) return nullptr; FILE *f = fopen(resolvedPath.c_str(), mode); if (!f) { free(objStorage); return nullptr; } if (needResetPosition) fseek(f, 0, SEEK_SET); int fd = fileno(f); if (createDisposition == GpFileCreationDispositions::kOverwriteExisting) { if (ftruncate64(fd, 0) < 0) { free(objStorage); fclose(f); return nullptr; } } return new (objStorage) GpFileStream_Android_File(f, fd, !writeAccess, false); } } bool GpFileSystem_Android::DeleteFile(PortabilityLayer::VirtualDirectory_t virtualDirectory, const char *path, bool &existed) { std::string resolvedPath; bool isAsset; if (!ResolvePath(virtualDirectory, &path, 1, resolvedPath, isAsset)) { existed = false; return false; } if (isAsset) return false; if (unlink(resolvedPath.c_str()) < 0) { existed = (errno != ENOENT); return false; } existed = true; return true; } IGpDirectoryCursor *GpFileSystem_Android::ScanDirectoryNested(PortabilityLayer::VirtualDirectory_t virtualDirectory, const char *const *paths, size_t numPaths) { if (virtualDirectory == PortabilityLayer::VirtualDirectories::kGameData || virtualDirectory == PortabilityLayer::VirtualDirectories::kApplicationData) return ScanAssetDirectory(virtualDirectory, paths, numPaths); return ScanStorageDirectory(virtualDirectory, paths, numPaths); } bool GpFileSystem_Android::ValidateFilePath(const char *path, size_t length) const { for (size_t i = 0; i < length; i++) { const char c = path[i]; if (c >= '0' && c <= '9') continue; if (c == '_' || c == '.' || c == '\'' || c == '!') continue; if (c == ' ' && i != 0 && i != length - 1) continue; if (c >= 'a' && c <= 'z') continue; if (c >= 'A' && c <= 'Z') continue; return false; } return true; } bool GpFileSystem_Android::ValidateFilePathUnicodeChar(uint32_t c) const { if (c >= '0' && c <= '9') return true; if (c == '_' || c == '\'') return true; if (c == ' ') return true; if (c >= 'a' && c <= 'z') return true; if (c >= 'A' && c <= 'Z') return true; return false; } void GpFileSystem_Android::SetDelayCallback(DelayCallback_t delayCallback) { m_delayCallback = delayCallback; } void GpFileSystem_Android::PostSourceExportRequest(bool cancelled, int fd, jobject pfd) { JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jobject globalRef = jni->NewGlobalRef(pfd); m_sourceExportMutex->Lock(); m_sourceExportWaiting = false; m_sourceExportCancelled = cancelled; m_sourceExportFD = fd; m_sourceExportPFD = globalRef; m_sourceExportMutex->Unlock(); } void GpFileSystem_Android::ClosePFD(jobject pfd) { JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jni->CallVoidMethod(m_activity, m_closeSourceExportPFDMID, pfd); jni->DeleteGlobalRef(pfd); } GpFileSystem_Android *GpFileSystem_Android::GetInstance() { return &ms_instance; } class GpDirectoryCursor_StringList final : public IGpDirectoryCursor { public: explicit GpDirectoryCursor_StringList(std::vector &paths); ~GpDirectoryCursor_StringList(); bool GetNext(const char *&outFileName) override; void Destroy() override; private: std::vector m_paths; size_t m_index; }; GpDirectoryCursor_StringList::GpDirectoryCursor_StringList(std::vector &paths) : m_index(0) { std::swap(paths, m_paths); } GpDirectoryCursor_StringList::~GpDirectoryCursor_StringList() { } bool GpDirectoryCursor_StringList::GetNext(const char *&outFileName) { if (m_index == m_paths.size()) return false; outFileName = m_paths[m_index].c_str(); m_index++; return true; } void GpDirectoryCursor_StringList::Destroy() { delete this; } class GpDirectoryCursor_POSIX final : public IGpDirectoryCursor { public: explicit GpDirectoryCursor_POSIX(DIR *dir); ~GpDirectoryCursor_POSIX(); bool GetNext(const char *&outFileName) override; void Destroy() override; private: DIR *m_dir; }; GpDirectoryCursor_POSIX::GpDirectoryCursor_POSIX(DIR *dir) : m_dir(dir) { } GpDirectoryCursor_POSIX::~GpDirectoryCursor_POSIX() { closedir(m_dir); } bool GpDirectoryCursor_POSIX::GetNext(const char *&outFileName) { struct dirent *dir = readdir(m_dir); if (!dir) return false; outFileName = dir->d_name; return true; } void GpDirectoryCursor_POSIX::Destroy() { delete this; } IGpDirectoryCursor *GpFileSystem_Android::ScanAssetDirectory(PortabilityLayer::VirtualDirectory_t virtualDirectory, char const* const* paths, size_t numPaths) { std::string resolvedPath; std::vector subPaths; bool isAsset = true; if (!ResolvePath(virtualDirectory, paths, numPaths, resolvedPath, isAsset)) return nullptr; JNIEnv *jni = static_cast(SDL_AndroidGetJNIEnv()); jstring directory = jni->NewStringUTF(resolvedPath.c_str()); jobjectArray resultArray = static_cast(jni->CallObjectMethod(m_activity, m_scanAssetDirectoryMID, directory)); jni->DeleteLocalRef(directory); size_t arrayLength = jni->GetArrayLength(resultArray); subPaths.reserve(arrayLength); for (size_t i = 0; i < arrayLength; i++) { jstring pathJStr = static_cast(jni->GetObjectArrayElement(resultArray, i)); const char *pathStrChars = jni->GetStringUTFChars(pathJStr, nullptr); subPaths.push_back(std::string(pathStrChars, static_cast(jni->GetStringUTFLength(pathJStr)))); jni->ReleaseStringUTFChars(pathJStr, pathStrChars); jni->DeleteLocalRef(pathJStr); } jni->DeleteLocalRef(resultArray); return new GpDirectoryCursor_StringList(subPaths); } IGpDirectoryCursor *GpFileSystem_Android::ScanStorageDirectory(PortabilityLayer::VirtualDirectory_t virtualDirectory, char const* const* paths, size_t numPaths) { std::string resolvedPath; std::vector subPaths; bool isAsset = true; if (!ResolvePath(virtualDirectory, paths, numPaths, resolvedPath, isAsset)) return nullptr; DIR *d = opendir(resolvedPath.c_str()); if (!d) return nullptr; return new GpDirectoryCursor_POSIX(d); } JNIEXPORT void JNICALL nativePostSourceExportRequest(JNIEnv *env, jclass cls, jboolean cancelled, jint fd, jobject pfd) { GpFileSystem_Android::GetInstance()->PostSourceExportRequest(cancelled != JNI_FALSE, fd, pfd); } GpFileSystem_Android GpFileSystem_Android::ms_instance;