From 1f018d33c5a94454fdcb067697c26e41eadb0a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Apr 2021 23:34:20 +0200 Subject: [PATCH] Start work on supporting Android Content URIs in the FileUtil API. Add tentative Android Content URI support to much of FileUtil.h. Buildfix Fix some issues with the file browser in storage URIs. --- Common/File/FileUtil.cpp | 144 ++++++++++++++++++++++++++++++-- Common/File/FileUtil.h | 9 +- Common/File/Path.cpp | 4 + Common/File/Path.h | 2 +- UI/GameSettingsScreen.cpp | 8 +- UI/NativeApp.cpp | 4 +- android/jni/AndroidContentURI.h | 25 ++++++ unittest/UnitTest.cpp | 6 ++ 8 files changed, 187 insertions(+), 15 deletions(-) diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp index bc252ebeaf..6bfbe956b6 100644 --- a/Common/File/FileUtil.cpp +++ b/Common/File/FileUtil.cpp @@ -25,6 +25,7 @@ #include "ppsspp_config.h" #include "android/jni/app-android.h" +#include "android/jni/AndroidContentURI.h" #ifdef __MINGW32__ #include @@ -99,6 +100,19 @@ FILE *OpenCFile(const Path &path, const char *mode) { switch (path.Type()) { case PathType::NATIVE: break; + case PathType::CONTENT_URI: + if (!strcmp(mode, "r") || !strcmp(mode, "rb")) { + // Read, let's support this - easy one. + int descriptor = Android_OpenContentUriFd(path.ToString(), Android_OpenContentUriMode::READ); + if (descriptor == -1) { + return nullptr; + } + return fdopen(descriptor, "rb"); + } else { + ERROR_LOG(COMMON, "OpenCFile(%s): Mode not yet supported: %s", path.c_str(), mode); + return nullptr; + } + break; default: ERROR_LOG(COMMON, "OpenCFile(%s): Not yet supported", path.c_str()); return nullptr; @@ -107,7 +121,7 @@ FILE *OpenCFile(const Path &path, const char *mode) { #if defined(_WIN32) && defined(UNICODE) return _wfopen(path.ToWString().c_str(), ConvertUTF8ToWString(mode).c_str()); #else - return fopen(path.ToString().c_str(), mode); + return fopen(path.c_str(), mode); #endif } @@ -148,7 +162,13 @@ std::string ResolvePath(const std::string &path) { if (startsWith(path, "http://") || startsWith(path, "https://")) { return path; } -#if PPSSPP_PLATFORM(WINDOWS) + + if (Android_IsContentUri(path)) { + // Nothing to do? + return path; + } + +#ifdef _WIN32 static const int BUF_SIZE = 32768; wchar_t *buf = new wchar_t[BUF_SIZE] {}; @@ -210,9 +230,23 @@ static void StripTailDirSlashes(std::string &fname) { // Returns true if file filename exists. Will return true on directories. bool ExistsInDir(const Path &path, const std::string &filename) { return Exists(path / filename); + if (Android_IsContentUri(path.ToString())) { + AndroidStorageContentURI uri(path.ToString()); + return Exists(Path(uri.WithFilePath(filename).ToString())); + } else { + return Exists(path / filename); + } } bool Exists(const std::string &filename) { + if (Android_IsContentUri(filename)) { + FileInfo info; + if (!Android_GetFileInfo(filename, &info)) { + return false; + } + return info.exists; + } + std::string fn = filename; StripTailDirSlashes(fn); @@ -247,6 +281,14 @@ bool IsDirectory(const Path &filename) { switch (filename.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + { + FileInfo info; + if (!Android_GetFileInfo(filename.ToString(), &info)) { + return false; + } + return info.isDirectory; + } default: return false; } @@ -284,6 +326,11 @@ bool Delete(const Path &filename) { switch (filename.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + { + FileInfo info; + return Android_RemoveFile(filename.ToString()); + } default: return false; } @@ -324,11 +371,26 @@ bool CreateDir(const Path &path) { switch (path.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + { + // Convert it to a CreateDirIn call, if possible, since that's + // what we can do with the storage API. + AndroidStorageContentURI uri(path.ToString()); + std::string newDirName = uri.GetLastPart(); + if (uri.NavigateUp()) { + return Android_CreateDirectory(uri.ToString(), newDirName); + } else { + // Bad path - can't create this directory. + return false; + } + break; + } default: return false; } std::string fn = path.ToString(); + StripTailDirSlashes(fn); DEBUG_LOG(COMMON, "CreateDir('%s')", fn.c_str()); #ifdef _WIN32 if (::CreateDirectory(ConvertUTF8ToWString(fn).c_str(), NULL)) @@ -357,16 +419,54 @@ bool CreateDir(const Path &path) { #endif } +// Returns true if successful, or path already exists. +// Supports Android Content Storage URIs more reliably than CreateDir. +bool CreateDirIn(const std::string &parentDir, const std::string &newDirName) { + if (Android_IsContentUri(parentDir)) { + return Android_CreateDirectory(parentDir, newDirName); + } + std::string fn = parentDir; + StripTailDirSlashes(fn); + DEBUG_LOG(COMMON, "CreateDir('%s')", fn.c_str()); +#ifdef _WIN32 + std::string fullName = parentDir + "\\" + newDirName; + if (::CreateDirectory(ConvertUTF8ToWString(fullName).c_str(), NULL)) + return true; + DWORD error = GetLastError(); + if (error == ERROR_ALREADY_EXISTS) { + WARN_LOG(COMMON, "CreateDir: CreateDirectory failed on %s: already exists", fullName.c_str()); + return true; + } + ERROR_LOG(COMMON, "CreateDir: CreateDirectory failed on %s: %08x", fullName.c_str(), (uint32_t)error); + return false; +#else + std::string fullName = parentDir + "/" + newDirName; + if (mkdir(fullName.c_str(), 0755) == 0) + return true; + int err = errno; + if (err == EEXIST) { + WARN_LOG(COMMON, "CreateDir: mkdir failed on %s: already exists", fullName.c_str()); + return true; + } + ERROR_LOG(COMMON, "CreateDir: mkdir failed on %s: %s", fullName.c_str(), strerror(err)); + return false; +#endif +} + // Creates the full path of fullPath returns true on success bool CreateFullPath(const Path &path) { switch (path.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + ERROR_LOG(COMMON, "CreateFullPath(%s): Not supported", path.c_str()); + return false; default: return false; } std::string fullPath = path.ToString(); + StripTailDirSlashes(fullPath); int panicCounter = 100; VERBOSE_LOG(COMMON, "CreateFullPath: '%s'", fullPath.c_str()); @@ -413,6 +513,8 @@ bool DeleteDir(const Path &path) { switch (path.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + return Android_RemoveFile(path.ToString()); default: return false; } @@ -448,11 +550,15 @@ bool Rename(const Path &srcFilename, const Path &destFilename) switch (srcFilename.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + ERROR_LOG(COMMON, "Moving files by Android URI is not yet supported"); + return false; default: return false; } INFO_LOG(COMMON, "Rename: %s --> %s", srcFilename.c_str(), destFilename.c_str()); + #if defined(_WIN32) && defined(UNICODE) std::wstring srcw = srcFilename.ToWString(); std::wstring destw = destFilename.ToWString(); @@ -474,12 +580,18 @@ bool Copy(const Path &srcFilename, const Path &destFilename) switch (srcFilename.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + ERROR_LOG(COMMON, "Copying files by Android URI is not yet supported"); + break; default: return false; } switch (destFilename.Type()) { case PathType::NATIVE: break; // OK + case PathType::CONTENT_URI: + ERROR_LOG(COMMON, "Copying files by Android URI is not yet supported"); + return false; default: return false; } @@ -567,6 +679,15 @@ bool Move(const Path &srcFilename, const Path &destFilename) { // Returns the size of file (64bit) // TODO: Add a way to return an error. uint64_t GetFileSize(const Path &filename) { + if (Android_IsContentUri(filename.ToString())) { + FileInfo info; + if (Android_GetFileInfo(filename.ToString(), &info)) { + return info.size; + } else { + return 0; + } + } + #if defined(_WIN32) && defined(UNICODE) WIN32_FILE_ATTRIBUTE_DATA attr; if (!GetFileAttributesEx(filename.ToWString().c_str(), GetFileExInfoStandard, &attr)) @@ -595,8 +716,7 @@ uint64_t GetFileSize(const Path &filename) { #endif } -uint64_t GetFileSize(FILE *f) -{ +uint64_t GetFileSize(FILE *f) { // This will only support 64-bit when large file support is available. // That won't be the case on some versions of Android, at least. #if defined(__ANDROID__) || (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS < 64) @@ -636,7 +756,6 @@ uint64_t GetFileSize(FILE *f) // creates an empty file filename, returns true on success bool CreateEmptyFile(const Path &filename) { - INFO_LOG(COMMON, "CreateEmptyFile: %s", filename.c_str()); FILE *pFile = OpenCFile(filename, "wb"); if (!pFile) { @@ -649,6 +768,11 @@ bool CreateEmptyFile(const Path &filename) { // Deletes the given directory and anything under it. Returns true on success. bool DeleteDirRecursively(const Path &directory) { + if (Android_IsContentUri(directory.ToString())) { + ERROR_LOG(COMMON, "DeleteDirRecursively(%s) not yet supported on content URIs", directory.c_str()); + return false; + } + //Removed check, it prevents the UWP from deleting store downloads INFO_LOG(COMMON, "DeleteDirRecursively: %s", directory.c_str()); @@ -712,7 +836,14 @@ bool DeleteDirRecursively(const Path &directory) { return File::DeleteDir(directory); } -void OpenFileInEditor(const Path &fileName) { +bool OpenFileInEditor(const Path &fileName) { + if (Android_IsContentUri(fileName.ToString())) { + // TODO: This might even be supportable? + + ERROR_LOG(COMMON, "OpenFileInEditor(%s) not yet supported on content URIs", fileName.c_str()); + return false; + } + #if defined(_WIN32) #if PPSSPP_PLATFORM(UWP) // Do nothing. @@ -733,6 +864,7 @@ void OpenFileInEditor(const Path &fileName) { ERROR_LOG(COMMON, "Failed to launch ini file"); } #endif + return true; } const Path &GetExeDirectory() { diff --git a/Common/File/FileUtil.h b/Common/File/FileUtil.h index e74cd50557..9c731bef4c 100644 --- a/Common/File/FileUtil.h +++ b/Common/File/FileUtil.h @@ -51,6 +51,9 @@ bool Exists(const Path &path); // Returns true if file filename exists in directory path. bool ExistsInDir(const Path &path, const std::string &filename); +// Returns true if file filename exists in directory path. +bool ExistsInDir(const std::string &path, const std::string &filename); + // Returns true if filename exists, and is a directory // Supports Android content URIs. bool IsDirectory(const Path &filename); @@ -67,6 +70,10 @@ uint64_t GetFileSize(FILE *f); // Returns true if successful, or path already exists. bool CreateDir(const Path &filename); +// Returns true if successful, or path already exists. +// Supports Android Content Storage URIs, unlike CreateDir. +bool CreateDirIn(const std::string &parentDir, const std::string &newDirName); + // Creates the full path of fullPath returns true on success bool CreateFullPath(const Path &fullPath); @@ -99,7 +106,7 @@ bool CreateEmptyFile(const Path &filename); // Opens ini file (cheats, texture replacements etc.) // TODO: Belongs in System or something. -void OpenFileInEditor(const Path &fileName); +bool OpenFileInEditor(const Path &fileName); // TODO: Belongs in System or something. const Path &GetExeDirectory(); diff --git a/Common/File/Path.cpp b/Common/File/Path.cpp index 0f340dda06..a3678fa0db 100644 --- a/Common/File/Path.cpp +++ b/Common/File/Path.cpp @@ -6,11 +6,15 @@ #include "Common/Log.h" #include "Common/Data/Encoding/Utf8.h" +#include "android/jni/app-android.h" + Path::Path(const std::string &str) { if (str.empty()) { type_ = PathType::UNDEFINED; } else if (startsWith(str, "http://") || startsWith(str, "https://")) { type_ = PathType::HTTP; + } else if (Android_IsContentUri(str)) { + type_ = PathType::CONTENT_URI; } else { type_ = PathType::NATIVE; } diff --git a/Common/File/Path.h b/Common/File/Path.h index 258287f8cb..ded1064b3a 100644 --- a/Common/File/Path.h +++ b/Common/File/Path.h @@ -8,7 +8,7 @@ enum class PathType { UNDEFINED = 0, NATIVE = 1, // RELATIVE, // (do we need this?) - // CONTENT_URI = 2, // Android only + CONTENT_URI = 2, // Android only HTTP = 3, // http://, https:// }; diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 6880eaca95..90e1c0ac89 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -1170,12 +1170,8 @@ UI::EventReturn GameSettingsScreen::OnChangeBackground(UI::EventParams &e) { const Path bgJpg = GetSysDirectory(DIRECTORY_SYSTEM) / "background.jpg"; if (File::Exists(bgPng) || File::Exists(bgJpg)) { - if (File::Exists(bgPng)) { - File::Delete(bgPng); - } - if (File::Exists(bgJpg)) { - File::Delete(bgJpg); - } + File::Delete(bgPng); + File::Delete(bgJpg); NativeMessageReceived("bgImage_updated", ""); } else { diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 151667f04a..6861eb8379 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -493,7 +493,7 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_Config.defaultCurrentDirectory = Path("/"); g_Config.internalDataDirectory = Path(savegame_dir); -#if defined(__ANDROID__) +#if PPSSPP_PLATFORM(ANDROID) // TODO: This needs to change in Android 12. // @@ -515,6 +515,8 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch } else { ERROR_LOG(SYSTEM, "Couldn't read directory '%s' specified by memstick_dir.txt.", memstickDir.c_str()); } + } else { + INFO_LOG(SYSTEM, "No memstick directory file found. Using '%s'", memstickDirFile.c_str()); } #elif PPSSPP_PLATFORM(IOS) diff --git a/android/jni/AndroidContentURI.h b/android/jni/AndroidContentURI.h index 2cce0d1d9f..251909e24c 100644 --- a/android/jni/AndroidContentURI.h +++ b/android/jni/AndroidContentURI.h @@ -75,6 +75,26 @@ public: return file.size() > root.size(); } + std::string GetLastPart() const { + if (IsTreeURI()) { + // Can't do anything anyway. + return std::string(); + } + + if (!CanNavigateUp()) { + // Kinda useless to get the "primary:" volume. + return std::string(); + } + + size_t slash = file.rfind('/'); + if (slash == std::string::npos) { + return std::string(); + } + + std::string part = file.substr(slash + 1); + return part; + } + bool NavigateUp() { if (!CanNavigateUp()) { return false; @@ -106,6 +126,11 @@ public: } } + // Never store the output of this, only show it to the user. + std::string ToVisualString() const { + return file; + } + const std::string &FilePath() const { return file; } diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 8a23cf77af..9955009ddb 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -615,9 +615,15 @@ static bool TestAndroidContentURI() { AndroidStorageContentURI treeURI; EXPECT_TRUE(treeURI.Parse(std::string(treeURIString))); + AndroidStorageContentURI dirURI; + EXPECT_TRUE(dirURI.Parse(std::string(directoryURIString))); AndroidStorageContentURI fileURI; EXPECT_TRUE(fileURI.Parse(std::string(fileURIString))); + std::string lastPart = dirURI.GetLastPart(); + + EXPECT_EQ_STR(fileURI.GetLastPart(), std::string("Tekken 6.iso")); + EXPECT_TRUE(treeURI.TreeContains(fileURI)); EXPECT_TRUE(fileURI.CanNavigateUp());