From ef607a5fe25f12ed324511c6439b6fbd3f8ecee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 19 Apr 2021 00:34:10 +0200 Subject: [PATCH] Implement a "FileSystem" that wraps Android content storage. So far unused. --- Common/File/DirListing.h | 4 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 8 +- Core/FileSystems/AndroidStorageFileSystem.cpp | 539 ++++++++++++++++++ Core/FileSystems/AndroidStorageFileSystem.h | 105 ++++ android/jni/AndroidContentURI.h | 6 + android/jni/app-android.cpp | 58 +- android/jni/app-android.h | 6 +- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 82 ++- 9 files changed, 793 insertions(+), 17 deletions(-) create mode 100644 Core/FileSystems/AndroidStorageFileSystem.cpp create mode 100644 Core/FileSystems/AndroidStorageFileSystem.h diff --git a/Common/File/DirListing.h b/Common/File/DirListing.h index fff053923a..974f5e13a1 100644 --- a/Common/File/DirListing.h +++ b/Common/File/DirListing.h @@ -24,6 +24,10 @@ struct FileInfo { uint64_t ctime; uint32_t access; // st_mode & 0x1ff + // Currently only supported for Android storage files. + // Other places use different methods to get this. + uint64_t lastModified = 0; + bool operator <(const FileInfo &other) const; }; diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index ececc51b5f..8e6c9ff709 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -529,6 +529,7 @@ + @@ -1085,6 +1086,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 43a36fa022..e54ac5b2ea 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -962,6 +962,9 @@ Debugger\WebSocket + + FileSystems + Ext\libzip @@ -1874,6 +1877,9 @@ Debugger\WebSocket + + FileSystems + Ext\libzip @@ -1918,4 +1924,4 @@ Ext\libzip - \ No newline at end of file + diff --git a/Core/FileSystems/AndroidStorageFileSystem.cpp b/Core/FileSystems/AndroidStorageFileSystem.cpp new file mode 100644 index 0000000000..60e2663696 --- /dev/null +++ b/Core/FileSystems/AndroidStorageFileSystem.cpp @@ -0,0 +1,539 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include "ppsspp_config.h" + +#if PPSSPP_PLATFORM(ANDROID) +#include "android/jni/app-android.h" +#endif + +#include +#include +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/Data/Encoding/Utf8.h" +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Common/StringUtils.h" +#include "Common/File/DirListing.h" +#include "Core/FileSystems/AndroidStorageFileSystem.h" +#include "Core/HLE/sceKernel.h" +#include "Core/HW/MemoryStick.h" +#include "Core/CoreTiming.h" +#include "Core/System.h" +#include "Core/Host.h" +#include "Core/Replay.h" +#include "Core/Reporting.h" + +AndroidStorageFileSystem::AndroidStorageFileSystem(IHandleAllocator *_hAlloc, std::string _basePath, FileSystemFlags _flags) : basePath(_basePath), flags(_flags) { + // File::CreateFullPath(basePath); + hAlloc = _hAlloc; +} + +AndroidStorageFileSystem::~AndroidStorageFileSystem() { + CloseAll(); +} + +bool AndroidDirectoryFileHandle::Open(const std::string &basePath, std::string &fileName, FileAccess access, u32 &error) { + error = 0; + + // On the PSP, truncating doesn't lose data. If you seek later, you'll recover it. + // This is abnormal, so we deviate from the PSP's behavior and truncate on write/close. + // This means it's incorrectly not truncated before the write. + if (access & FILEACCESS_TRUNCATE) { + needsTrunc_ = 0; + } + + //TODO: tests, should append seek to end of file? seeking in a file opened for append? + + int flags = 0; + if (access & FILEACCESS_APPEND) { + flags |= O_APPEND; + } + if ((access & FILEACCESS_READ) && (access & FILEACCESS_WRITE)) { + flags |= O_RDWR; + } else if (access & FILEACCESS_READ) { + flags |= O_RDONLY; + } else if (access & FILEACCESS_WRITE) { + flags |= O_WRONLY; + } + if (access & FILEACCESS_CREATE) { + flags |= O_CREAT; + } + if (access & FILEACCESS_EXCL) { + flags |= O_EXCL; + } + + hFile = open(fullName.c_str(), flags, 0666); + bool success = hFile != -1; + +#if HOST_IS_CASE_SENSITIVE + if (!success && !(access & FILEACCESS_CREATE)) { + if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) { + error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND; + return false; + } + fullName = GetLocalPath(basePath, fileName); + const char *fullNameC = fullName.c_str(); + + DEBUG_LOG(FILESYS, "Case may have been incorrect, second try opening %s (%s)", fullNameC, fileName.c_str()); + + // And try again with the correct case this time +#ifdef _WIN32 + hFile = CreateFile(fullNameC, desired, sharemode, 0, openmode, 0, 0); + success = hFile != INVALID_HANDLE_VALUE; +#else + hFile = open(fullNameC, flags, 0666); + success = hFile != -1; +#endif + } +#endif + +#ifndef _WIN32 + if (success) { + // Reject directories, even if we succeed in opening them. + // TODO: Might want to do this stat first... + struct stat st; + if (fstat(hFile, &st) == 0 && S_ISDIR(st.st_mode)) { + close(hFile); + errno = EISDIR; + success = false; + } + } else if (errno == ENOSPC) { + // This is returned when the disk is full. + auto err = GetI18NCategory("Error"); + host->NotifyUserMessage(err->T("Disk full while writing data")); + error = SCE_KERNEL_ERROR_ERRNO_NO_PERM; + } else { + error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND; + } +#endif + + // Try to detect reads/writes to PSP/GAME to avoid them in replays. + if (fullName.find("/PSP/GAME/") != fullName.npos || fullName.find("\\PSP\\GAME\\") != fullName.npos) { + inGameDir_ = true; + } + + return success; +} + +size_t AndroidDirectoryFileHandle::Read(u8* pointer, s64 size) +{ + size_t bytesRead = 0; + if (needsTrunc_ != -1) { + // If the file was marked to be truncated, pretend there's nothing. + // On a PSP. it actually is truncated, but the data wasn't erased. + off_t off = (off_t)Seek(0, FILEMOVE_CURRENT); + if (needsTrunc_ <= off) { + return replay_ ? ReplayApplyDiskRead(pointer, 0, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : 0; + } + if (needsTrunc_ < off + size) { + size = needsTrunc_ - off; + } + } + bytesRead = read(hFile, pointer, size); + return replay_ ? ReplayApplyDiskRead(pointer, (uint32_t)bytesRead, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : bytesRead; +} + +size_t AndroidDirectoryFileHandle::Write(const u8* pointer, s64 size) +{ + size_t bytesWritten = 0; + bool diskFull = false; + + bytesWritten = write(hFile, pointer, size); + if (bytesWritten == (size_t)-1) { + diskFull = errno == ENOSPC; + } + if (needsTrunc_ != -1) { + off_t off = (off_t)Seek(0, FILEMOVE_CURRENT); + if (needsTrunc_ < off) { + needsTrunc_ = off; + } + } + + if (replay_) { + bytesWritten = ReplayApplyDiskWrite(pointer, (uint64_t)bytesWritten, (uint64_t)size, &diskFull, inGameDir_, CoreTiming::GetGlobalTimeUs()); + } + + if (diskFull) { + ERROR_LOG(FILESYS, "Disk full"); + auto err = GetI18NCategory("Error"); + host->NotifyUserMessage(err->T("Disk full while writing data")); + // We only return an error when the disk is actually full. + // When writing this would cause the disk to be full, so it wasn't written, we return 0. + if (MemoryStick_FreeSpace() == 0) { + // Sign extend on 64-bit. + return (size_t)(s64)(s32)SCE_KERNEL_ERROR_ERRNO_DEVICE_NO_FREE_SPACE; + } + } + + return bytesWritten; +} + +size_t AndroidDirectoryFileHandle::Seek(s32 position, FileMove type) +{ + if (needsTrunc_ != -1) { + // If the file is "currently truncated" move to the end based on that position. + // The actual, underlying file hasn't been truncated (yet.) + if (type == FILEMOVE_END) { + type = FILEMOVE_BEGIN; + position = needsTrunc_ + position; + } + } + + size_t result; + + int moveMethod = 0; + switch (type) { + case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break; + case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break; + case FILEMOVE_END: moveMethod = SEEK_END; break; + } + result = lseek(hFile, position, moveMethod); + + return replay_ ? (size_t)ReplayApplyDisk64(ReplayAction::FILE_SEEK, result, CoreTiming::GetGlobalTimeUs()) : result; +} + +void AndroidDirectoryFileHandle::Close() +{ + if (needsTrunc_ != -1) { + // Note: it's not great that Switch cannot truncate appropriately... + if (ftruncate(hFile, (off_t)needsTrunc_) != 0) { + ERROR_LOG_REPORT(FILESYS, "Failed to truncate file."); + } + } + if (hFile != -1) + close(hFile); +} + +void AndroidStorageFileSystem::CloseAll() { + for (auto iter = entries.begin(); iter != entries.end(); ++iter) { + INFO_LOG(FILESYS, "DirectoryFileSystem::CloseAll(): Force closing %d (%s)", (int)iter->first, iter->second.guestFilename.c_str()); + iter->second.hFile.Close(); + } + entries.clear(); +} + +std::string AndroidStorageFileSystem::GetLocalPath(std::string localpath) { + if (localpath.empty()) { + return baseContentUri.ToString(); + } + + if (localpath[0] == '/') + localpath.erase(0, 1); + + return baseContentUri.WithFilePath(localpath).ToString(); +} + +bool AndroidStorageFileSystem::MkDir(const std::string &dirname) { + ERROR_LOG(FILESYS, "MkDir operation not yet supported."); + // TODO: Figure out a way to create directories in Storage... + // TODO: Use Android_CreateDirectory. Not sure how deep it can go... + bool result = false; + // bool result = File::CreateFullPath(GetLocalPath(dirname)); + return ReplayApplyDisk(ReplayAction::MKDIR, result, CoreTiming::GetGlobalTimeUs()) != 0; +} + +bool AndroidStorageFileSystem::RmDir(const std::string &dirname) { + + ERROR_LOG(FILESYS, "RmDir operation not yet supported."); + + return false; // ReplayApplyDisk(ReplayAction::RMDIR, result, CoreTiming::GetGlobalTimeUs()) != 0; +} + +int AndroidStorageFileSystem::RenameFile(const std::string &from, const std::string &to) { + std::string fullTo = to; + + // Rename ignores the path (even if specified) on to. + size_t chop_at = to.find_last_of('/'); + if (chop_at != to.npos) + fullTo = to.substr(chop_at + 1); + + // Now put it in the same directory as from. + size_t dirname_end = from.find_last_of('/'); + if (dirname_end != from.npos) + fullTo = from.substr(0, dirname_end + 1) + fullTo; + + // At this point, we should check if the paths match and give an already exists error. + if (from == fullTo) + return ReplayApplyDisk(ReplayAction::FILE_RENAME, SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS, CoreTiming::GetGlobalTimeUs()); + + std::string fullFrom = GetLocalPath(from); + + fullTo = GetLocalPath(fullTo); + const char * fullToC = fullTo.c_str(); + + bool retValue = (0 == rename(fullFrom.c_str(), fullToC)); + + // TODO: Better error codes. + int result = retValue ? 0 : (int)SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS; + return ReplayApplyDisk(ReplayAction::FILE_RENAME, result, CoreTiming::GetGlobalTimeUs()); +} + +bool AndroidStorageFileSystem::RemoveFile(const std::string &filename) { + std::string fullName = GetLocalPath(filename); + bool retValue = (0 == unlink(fullName.c_str())); + return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0; +} + +int AndroidStorageFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) { + OpenFileEntry entry; + u32 err = 0; + bool success = entry.hFile.Open(basePath, filename, access, err); + if (err == 0 && !success) { + err = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND; + } + + err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs()); + if (err != 0) { + ERROR_LOG(FILESYS, "DirectoryFileSystem::OpenFile: FAILED, %i - access = %i", errno, (int)access); + return err; + } else { + if (access & FILEACCESS_APPEND) + entry.hFile.Seek(0, FILEMOVE_END); + + u32 newHandle = hAlloc->GetNewHandle(); + + entry.guestFilename = filename; + entry.access = access; + + entries[newHandle] = entry; + + return newHandle; + } +} + +void AndroidStorageFileSystem::CloseFile(u32 handle) { + EntryMap::iterator iter = entries.find(handle); + if (iter != entries.end()) { + hAlloc->FreeHandle(handle); + iter->second.hFile.Close(); + entries.erase(iter); + } else { + // This shouldn't happen... + ERROR_LOG(FILESYS, "Cannot close file that hasn't been opened: %08x", handle); + } +} + +bool AndroidStorageFileSystem::OwnsHandle(u32 handle) { + EntryMap::iterator iter = entries.find(handle); + return (iter != entries.end()); +} + +int AndroidStorageFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) { + return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED; +} + +PSPDevType AndroidStorageFileSystem::DevType(u32 handle) { + return PSPDevType::FILE; +} + +size_t AndroidStorageFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) { + int ignored; + return ReadFile(handle, pointer, size, ignored); +} + +size_t AndroidStorageFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) { + EntryMap::iterator iter = entries.find(handle); + if (iter != entries.end()) { + if (size < 0) { + ERROR_LOG_REPORT(FILESYS, "Invalid read for %lld bytes from disk %s", size, iter->second.guestFilename.c_str()); + return 0; + } + + size_t bytesRead = iter->second.hFile.Read(pointer, size); + return bytesRead; + } else { + //This shouldn't happen... + ERROR_LOG(FILESYS, "Cannot read file that hasn't been opened: %08x", handle); + return 0; + } +} + +size_t AndroidStorageFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) { + int ignored; + return WriteFile(handle, pointer, size, ignored); +} + +size_t AndroidStorageFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) { + EntryMap::iterator iter = entries.find(handle); + if (iter != entries.end()) + { + size_t bytesWritten = iter->second.hFile.Write(pointer, size); + return bytesWritten; + } else { + //This shouldn't happen... + ERROR_LOG(FILESYS, "Cannot write to file that hasn't been opened: %08x", handle); + return 0; + } +} + +size_t AndroidStorageFileSystem::SeekFile(u32 handle, s32 position, FileMove type) { + EntryMap::iterator iter = entries.find(handle); + if (iter != entries.end()) { + return iter->second.hFile.Seek(position, type); + } else { + //This shouldn't happen... + ERROR_LOG(FILESYS, "Cannot seek in file that hasn't been opened: %08x", handle); + return 0; + } +} + +PSPFileInfo AndroidStorageFileSystem::GetFileInfo(std::string filename) { + PSPFileInfo x; + x.name = filename; + + std::string uri = GetLocalPath(filename); + + FileInfo info; + if (!Android_GetFileInfo(uri, &info)) { + return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs()); + } + + x.type = info.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL; + x.exists = true; + + if (x.type != FILETYPE_DIRECTORY) { + x.size = info.size; + x.access = info.isWritable ? 0777 : 0666; + + // The only time value we get from the storage API. + int64_t lastModified = info.lastModified / 1000; + + time_t atime = lastModified; + time_t ctime = lastModified; + time_t mtime = lastModified; + + localtime_r((time_t*)&atime, &x.atime); + localtime_r((time_t*)&ctime, &x.ctime); + localtime_r((time_t*)&mtime, &x.mtime); + } + + return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs()); +} + +bool AndroidStorageFileSystem::GetHostPath(const std::string &inpath, std::string &outpath) { + outpath = GetLocalPath(inpath); + return true; +} + +// See comment in DirectoryFileSystem. +extern std::string SimulateVFATBug(std::string filename); + +std::vector AndroidStorageFileSystem::GetDirListing(std::string path) { + std::vector myVector; + bool listingRoot = path == "/" || path == "\\"; + + std::string uri = GetLocalPath(path); + + std::vector fileInfo = Android_ListContentUri(uri); + + bool hideISOFiles = PSP_CoreParameter().compat.flags().HideISOFiles; + for (auto &info : fileInfo) { + PSPFileInfo entry; + if (info.isDirectory) + entry.type = FILETYPE_DIRECTORY; + else + entry.type = FILETYPE_NORMAL; + entry.access = info.isWritable ? 0777 : 0666; + entry.name = info.name; + if (Flags() & FileSystemFlags::SIMULATE_FAT32) + entry.name = SimulateVFATBug(entry.name); + entry.size = info.size; + + bool hideFile = false; + if (hideISOFiles && (endsWithNoCase(entry.name, ".cso") || endsWithNoCase(entry.name, ".iso"))) { + // Workaround for DJ Max Portable, see compat.ini. + hideFile = true; + } + int64_t lastModified = info.lastModified / 1000; + + time_t atime = lastModified; + time_t ctime = lastModified; + time_t mtime = lastModified; + + localtime_r((time_t*)&s.st_atime, &entry.atime); + localtime_r((time_t*)&s.st_ctime, &entry.ctime); + localtime_r((time_t*)&s.st_mtime, &entry.mtime); + if (!hideFile && (!listingRoot || (strcmp(info.name.c_str(), "..") && strcmp(info.name.c_str(), ".")))) + myVector.push_back(entry); + } + + return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs()); +} + +u64 AndroidStorageFileSystem::FreeSpace(const std::string &path) { + // Can't get this, I think. + + return ReplayApplyDisk64(ReplayAction::FREESPACE, std::numeric_limits::max(), CoreTiming::GetGlobalTimeUs()); +} + +// WARNING! This must be kept compatible with DirectoryFileSystem for save state portability. +void AndroidStorageFileSystem::DoState(PointerWrap &p) { + auto s = p.Section("DirectoryFileSystem", 0, 2); + if (!s) + return; + + // Savestate layout: + // u32: number of entries + // per-entry: + // u32: handle number + // std::string filename (in guest's terms, untranslated) + // enum FileAccess file access mode + // u32 seek position + // s64 current truncate position (v2+ only) + + u32 num = (u32)entries.size(); + Do(p, num); + + if (p.mode == p.MODE_READ) { + CloseAll(); + u32 key; + OpenFileEntry entry; + for (u32 i = 0; i < num; i++) { + Do(p, key); + Do(p, entry.guestFilename); + Do(p, entry.access); + u32 err; + if (!entry.hFile.Open(basePath, entry.guestFilename, entry.access, err)) { + ERROR_LOG(FILESYS, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str()); + continue; + } + u32 position; + Do(p, position); + if (position != entry.hFile.Seek(position, FILEMOVE_BEGIN)) { + ERROR_LOG(FILESYS, "Failed to restore seek position while loading state: %s", entry.guestFilename.c_str()); + continue; + } + if (s >= 2) { + Do(p, entry.hFile.needsTrunc_); + } + entries[key] = entry; + } + } else { + for (auto iter = entries.begin(); iter != entries.end(); ++iter) { + u32 key = iter->first; + Do(p, key); + Do(p, iter->second.guestFilename); + Do(p, iter->second.access); + u32 position = (u32)iter->second.hFile.Seek(0, FILEMOVE_CURRENT); + Do(p, position); + Do(p, iter->second.hFile.needsTrunc_); + } + } +} diff --git a/Core/FileSystems/AndroidStorageFileSystem.h b/Core/FileSystems/AndroidStorageFileSystem.h new file mode 100644 index 0000000000..25fceff1dd --- /dev/null +++ b/Core/FileSystems/AndroidStorageFileSystem.h @@ -0,0 +1,105 @@ +#pragma once + +// Like DirectoryFileSystem, but uses the Android Storage Access Framework +// to access a folder tree as if it was a PSP file system. +// Unfortunately we cannot implement all the semantics like this, let's see +// how good we can get it though. + +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include "ppsspp_config.h" + +#include + +#include "android/jni/AndroidContentURI.h" + +#include "Core/FileSystems/FileSystem.h" +#include "Core/FileSystems/AndroidStorageFileSystem.h" + +struct AndroidDirectoryFileHandle { + enum Flags { + NORMAL, + SKIP_REPLAY, + }; + + int hFile = -1; + s64 needsTrunc_ = -1; + bool replay_ = true; + bool inGameDir_ = false; + + AndroidDirectoryFileHandle(Flags flags) : replay_(flags != SKIP_REPLAY) { } + + // std::string GetLocalPath(const std::string &basePath, std::string localpath); + bool Open(const std::string &basePath, std::string &fileName, FileAccess access, u32 &err); + size_t Read(u8* pointer, s64 size); + size_t Write(const u8* pointer, s64 size); + size_t Seek(s32 position, FileMove type); + void Close(); +}; + +class AndroidStorageFileSystem : public IFileSystem { +public: + AndroidStorageFileSystem(IHandleAllocator *_hAlloc, std::string basePath, FileSystemFlags _flags = FileSystemFlags::NONE); + ~AndroidStorageFileSystem(); + + void CloseAll(); + + void DoState(PointerWrap &p) override; + std::vector GetDirListing(std::string path) override; + int OpenFile(std::string filename, FileAccess access, const char *devicename = nullptr) override; + void CloseFile(u32 handle) override; + size_t ReadFile(u32 handle, u8 *pointer, s64 size) override; + size_t ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) override; + size_t WriteFile(u32 handle, const u8 *pointer, s64 size) override; + size_t WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) override; + size_t SeekFile(u32 handle, s32 position, FileMove type) override; + PSPFileInfo GetFileInfo(std::string filename) override; + bool OwnsHandle(u32 handle) override; + int Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) override; + PSPDevType DevType(u32 handle) override; + + bool MkDir(const std::string &dirname) override; + bool RmDir(const std::string &dirname) override; + int RenameFile(const std::string &from, const std::string &to) override; + bool RemoveFile(const std::string &filename) override; + bool GetHostPath(const std::string &inpath, std::string &outpath) override; + FileSystemFlags Flags() override { return flags; } + u64 FreeSpace(const std::string &path) override; + +private: + struct OpenFileEntry { + std::string guestFilename; + FileAccess access; + AndroidDirectoryFileHandle hFile = AndroidDirectoryFileHandle::NORMAL; + }; + + typedef std::map EntryMap; + EntryMap entries; + + AndroidStorageContentURI baseContentUri; + + std::string basePath; + IHandleAllocator *hAlloc; + FileSystemFlags flags; + + // Actually, builds a content URI. + std::string GetLocalPath(std::string localpath); +}; + diff --git a/android/jni/AndroidContentURI.h b/android/jni/AndroidContentURI.h index 9bc0e73d77..8c81a9fd02 100644 --- a/android/jni/AndroidContentURI.h +++ b/android/jni/AndroidContentURI.h @@ -58,6 +58,12 @@ public: } } + AndroidStorageContentURI WithFilePath(const std::string &filePath) { + AndroidStorageContentURI uri = *this; + uri.file = uri.root + "/" + filePath; + return uri; + } + bool IsTreeURI() const { return file.empty(); } diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 1cef504747..6498c348f4 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -173,7 +173,10 @@ static jmethodID postCommand; static jmethodID openContentUri; static jmethodID listContentUriDir; -static jmethodID closeContentUri; +static jmethodID contentUriCreateFile; +static jmethodID contentUriCreateDirectory; +static jmethodID contentUriRemoveFile; +static jmethodID contentUriGetFileInfo; static jobject nativeActivity; static volatile bool exitRenderLoop; @@ -254,11 +257,42 @@ int Android_OpenContentUriFd(const std::string &filename) { return fd; } -// Empty string means no parent -std::string Android_GetContentUriParent(const std::string &uri) { - // Might attempt to implement this with path manipulation later, but that's - // not reliable. - return ""; +bool Android_CreateDirectory(const std::string &rootTreeUri, const std::string &dirName) { + if (!nativeActivity) { + return -1; + } + auto env = getEnv(); + jstring paramRoot = env->NewStringUTF(rootTreeUri.c_str()); + jstring paramDirName = env->NewStringUTF(dirName.c_str()); + return env->CallBooleanMethod(nativeActivity, contentUriCreateDirectory, paramRoot, paramDirName); +} + +bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) { + if (!nativeActivity) { + return -1; + } + auto env = getEnv(); + jstring paramRoot = env->NewStringUTF(parentTreeUri.c_str()); + jstring paramFileName = env->NewStringUTF(fileName.c_str()); + return env->CallBooleanMethod(nativeActivity, contentUriCreateFile, paramRoot, paramFileName); +} + +bool Android_RemoveFile(const std::string &fileUri) { + if (!nativeActivity) { + return -1; + } + auto env = getEnv(); + jstring paramFileName = env->NewStringUTF(fileUri.c_str()); + return env->CallBooleanMethod(nativeActivity, contentUriRemoveFile, paramFileName); +} + +bool Android_GetFileInfo(const std::string &fileUri, FileInfo *info) { + if (!nativeActivity) { + return -1; + } + auto env = getEnv(); + jstring paramFileUri = env->NewStringUTF(fileUri.c_str()); + return env->CallObjectMethod(nativeActivity, contentUriGetFileInfo, paramFileUri); } std::vector Android_ListContentUri(const std::string &path) { @@ -280,7 +314,7 @@ std::vector Android_ListContentUri(const std::string &path) { INFO_LOG(FILESYS, "!! %s", file.c_str()); std::vector parts; SplitString(file, '|', parts); - if (parts.size() != 4) { + if (parts.size() != 5) { continue; } File::FileInfo info; @@ -290,6 +324,7 @@ std::vector Android_ListContentUri(const std::string &path) { sscanf(parts[1].c_str(), "%ld", &info.size); info.fullName = Path(parts[3]); info.isWritable = false; // We don't yet request write access + sscanf(parts[4].c_str(), "%ld", &info.lastModified); items.push_back(info); } env->ReleaseStringUTFChars(str, charArray); @@ -571,10 +606,19 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv * nativeActivity = env->NewGlobalRef(obj); postCommand = env->GetMethodID(env->GetObjectClass(obj), "postCommand", "(Ljava/lang/String;Ljava/lang/String;)V"); _dbg_assert_(postCommand); + openContentUri = env->GetMethodID(env->GetObjectClass(obj), "openContentUri", "(Ljava/lang/String;)I"); _dbg_assert_(openContentUri); listContentUriDir = env->GetMethodID(env->GetObjectClass(obj), "listContentUriDir", "(Ljava/lang/String;)[Ljava/lang/String;"); _dbg_assert_(listContentUriDir); + contentUriCreateDirectory = env->GetMethodID(env->GetObjectClass(obj), "contentUriCreateDirectory", "(Ljava/lang/String;Ljava/lang/String;)Z"); + _dbg_assert_(contentUriCreateDirectory); + contentUriCreateFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriCreateFile", "(Ljava/lang/String;Ljava/lang/String;)Z"); + _dbg_assert_(contentUriCreateFile); + contentUriRemoveFile = env->GetMethodID(env->GetObjectClass(obj), "contentUriRemoveFile", "(Ljava/lang/String;)Z"); + _dbg_assert_(contentUriRemoveFile); + contentUriGetFileInfo = env->GetMethodID(env->GetObjectClass(obj), "contentUriGetFileInfo", "(Ljava/lang/String;)Ljava/lang/String;"); + _dbg_assert_(contentUriGetFileInfo); } extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) { diff --git a/android/jni/app-android.h b/android/jni/app-android.h index cd0a565f42..d24cc6e011 100644 --- a/android/jni/app-android.h +++ b/android/jni/app-android.h @@ -26,8 +26,10 @@ extern std::string g_extFilesDir; bool Android_IsContentUri(const std::string &uri); int Android_OpenContentUriFd(const std::string &uri); -std::string Android_GetContentUriParent(const std::string &uri); // Empty string means no parent - +bool Android_CreateDirectory(const std::string &parentTreeUri, const std::string &dirName); +bool Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName); +bool Android_RemoveFile(const std::string &fileUri); +bool Android_GetFileInfo(const std::string &fileUri, FileInfo *info); std::vector Android_ListContentUri(const std::string &uri); diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 94b77b141a..e7e95abcca 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -131,6 +131,16 @@ public class PpssppActivity extends NativeActivity { } } + private static String fileInfoToString(DocumentFile file) { + String str = "F|"; + if (file.isDirectory()) { + str = "D|"; + } + // TODO: Should we do something with child.isVirtual()?. + str += file.length() + "|" + file.getName() + "|" + file.getUri() + "|" + file.lastModified(); + return str; + } + public String[] listContentUriDir(String uriString) { try { Uri uri = Uri.parse(uriString); @@ -139,13 +149,8 @@ public class PpssppActivity extends NativeActivity { ArrayList listing = new ArrayList(); // Encode entries into strings for JNI simplicity. for (DocumentFile file : children) { - String typeStr = "F|"; - if (file.isDirectory()) { - typeStr = "D|"; - } - // TODO: Should we do something with child.isVirtual()?. - typeStr += file.length() + "|" + file.getName() + "|" + file.getUri(); - listing.add(typeStr); + String str = fileInfoToString(file); + listing.add(str); } // Is ArrayList weird or what? String[] strings = new String[listing.size()]; @@ -155,4 +160,67 @@ public class PpssppActivity extends NativeActivity { return new String[]{}; } } + + public boolean contentUriCreateDirectory(String rootTreeUri, String dirName) { + try { + Uri uri = Uri.parse(rootTreeUri); + DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri); + if (documentFile != null) { + DocumentFile createdDir = documentFile.createDirectory(dirName); + return createdDir != null; + } else { + return false; + } + } catch (Exception e) { + Log.e(TAG, "Exception opening content uri: " + e.toString()); + return false; + } + } + + public boolean contentUriCreateFile(String rootTreeUri, String fileName) { + try { + Uri uri = Uri.parse(rootTreeUri); + DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri); + if (documentFile != null) { + DocumentFile createdFile = documentFile.createFile("application/arbitrary", fileName); + return createdFile != null; + } else { + return false; + } + } catch (Exception e) { + Log.e(TAG, "Exception opening content uri: " + e.toString()); + return false; + } + } + + public boolean contentUriRemoveFile(String fileName) { + try { + Uri uri = Uri.parse(fileName); + DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri); + if (documentFile != null) { + return documentFile.delete(); + } else { + return false; + } + } catch (Exception e) { + Log.e(TAG, "Exception opening content uri: " + e.toString()); + return false; + } + } + + public String contentUriGetFileInfo(String fileName) { + try { + Uri uri = Uri.parse(fileName); + DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri); + if (documentFile != null) { + String str = fileInfoToString(documentFile); + return str; + } else { + return null; + } + } catch (Exception e) { + Log.e(TAG, "Exception opening content uri: " + e.toString()); + return null; + } + } }