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());