From e3cf04bb3455850ee3976ebd8e2bd2f85b4692b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 14 Mar 2021 11:01:51 +0100 Subject: [PATCH] Implement file filters for Android in PathBrowser asdf Move Android file listing parsing logic into app-android.cpp. Add utility for parsing/writing Android Content Storage URIs. Fix some bugs. Allow upwards navigation in file browser from directories downstream from tree URIs. --- Common/File/PathBrowser.cpp | 48 +++++--- Windows/PPSSPP.vcxproj | 1 + Windows/PPSSPP.vcxproj.filters | 3 + android/jni/AndroidContentURI.h | 107 ++++++++++++++++++ android/jni/app-android.cpp | 29 ++++- android/jni/app-android.h | 10 +- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 2 - unittest/UnitTest.cpp | 25 ++++ 8 files changed, 198 insertions(+), 27 deletions(-) create mode 100644 android/jni/AndroidContentURI.h diff --git a/Common/File/PathBrowser.cpp b/Common/File/PathBrowser.cpp index cb616f0ec1..8edca75c25 100644 --- a/Common/File/PathBrowser.cpp +++ b/Common/File/PathBrowser.cpp @@ -19,6 +19,7 @@ #if PPSSPP_PLATFORM(ANDROID) #include "android/jni/app-android.h" +#include "android/jni/AndroidContentURI.h" #endif bool LoadRemoteFileList(const Path &url, bool *cancel, std::vector &files) { @@ -261,22 +262,25 @@ bool PathBrowser::GetListing(std::vector &fileInfo, const char * #if PPSSPP_PLATFORM(ANDROID) if (Android_IsContentUri(path_.ToString())) { - std::vector files = Android_ListContentUri(path_.ToString()); + std::vector files = Android_ListContentUri(path_.ToString()); fileInfo.clear(); - for (auto &file : files) { - ERROR_LOG(FILESYS, "!! %s", file.c_str()); - std::vector parts; - SplitString(file, '|', parts); - if (parts.size() != 4) { - continue; + + std::vector allowedExtensions; + SplitString(filter, ':', allowedExtensions); + + for (auto &info : files) { + if (!info.isDirectory && allowedExtensions.size()) { + bool found = false; + for (auto &ext : allowedExtensions) { + if (endsWithNoCase(info.name, "." + ext)) { + found = true; + break; + } + } + if (!found) { + continue; + } } - File::FileInfo info; - info.exists = true; - info.isDirectory = parts[0][0] == 'D'; - sscanf(parts[1].c_str(), "%ld", &info.size); - info.name = parts[2]; - info.fullName = Path(parts[3]); - info.isWritable = false; // We don't yet request write access fileInfo.push_back(info); } return true; @@ -303,10 +307,8 @@ bool PathBrowser::CanNavigateUp() { */ #if PPSSPP_PLATFORM(ANDROID) if (Android_IsContentUri(path_.ToString())) { - // Need to figure out how much we can navigate by parsing the URL. - // DocumentUri from seems to be split into two paths: The folder you have gotten permission to see, - // and the folder below it. - return false; + AndroidStorageContentURI uri(path_.ToString()); + return uri.CanNavigateUp(); } #endif @@ -314,6 +316,16 @@ bool PathBrowser::CanNavigateUp() { } void PathBrowser::NavigateUp() { +#if PPSSPP_PLATFORM(ANDROID) + if (Android_IsContentUri(path_.ToString())) { + // Manipulate the Uri to navigate upwards. + AndroidStorageContentURI uri(path_.ToString()); + if (uri.NavigateUp()) { + path_ = Path(uri.ToString()); + } + return; + } +#endif path_ = path_.NavigateUp(); } diff --git a/Windows/PPSSPP.vcxproj b/Windows/PPSSPP.vcxproj index e3b348add4..d9b881f0f5 100644 --- a/Windows/PPSSPP.vcxproj +++ b/Windows/PPSSPP.vcxproj @@ -1006,6 +1006,7 @@ true true + true true diff --git a/Windows/PPSSPP.vcxproj.filters b/Windows/PPSSPP.vcxproj.filters index 141d03a3ca..537972bafa 100644 --- a/Windows/PPSSPP.vcxproj.filters +++ b/Windows/PPSSPP.vcxproj.filters @@ -538,6 +538,9 @@ Other Platforms\Android + + Other Platforms\Android + diff --git a/android/jni/AndroidContentURI.h b/android/jni/AndroidContentURI.h new file mode 100644 index 0000000000..9bc0e73d77 --- /dev/null +++ b/android/jni/AndroidContentURI.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +#include "Common/StringUtils.h" +#include "Common/Net/URL.h" + +// Utility to deal with Android storage URIs of the forms: +// content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO +// content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO + +// I am not 100% sure it's OK to rely on the internal format of file content URIs. +// On the other hand, I'm sure tons of apps would break if these changed, so I think we can +// consider them pretty stable. +class AndroidStorageContentURI { +private: + std::string provider; + std::string root; + std::string file; +public: + AndroidStorageContentURI() {} + explicit AndroidStorageContentURI(const std::string &path) { + Parse(path); + } + + bool Parse(const std::string &path) { + const char *prefix = "content://"; + if (!startsWith(path, prefix)) { + return false; + } + + std::string components = path.substr(strlen(prefix)); + + std::vector parts; + SplitString(components, '/', parts); + if (parts.size() == 3) { + provider = parts[0]; + if (parts[1] != "tree") { + return false; + } + root = UriDecode(parts[2]); + return true; + } else if (parts.size() == 5) { + provider = parts[0]; + if (parts[1] != "tree") { + return false; + } + root = UriDecode(parts[2]); + if (parts[3] != "document") { + return false; + } + file = UriDecode(parts[4]); + // Sanity check file path. + return startsWith(file, root); + } else { + // Invalid Content URI + return false; + } + } + + bool IsTreeURI() const { + return file.empty(); + } + + bool CanNavigateUp() const { + return file.size() > root.size(); + } + + bool NavigateUp() { + if (!CanNavigateUp()) { + return false; + } + + size_t slash = file.rfind('/'); + if (slash == std::string::npos) { + return false; + } + + file = file.substr(0, slash); + return true; + } + + bool TreeContains(const AndroidStorageContentURI &fileURI) { + if (!IsTreeURI()) { + return false; + } + return startsWith(fileURI.file, root); + } + + std::string ToString() const { + if (file.empty()) { + // Tree URI + return StringFromFormat("content://%s/tree/%s", provider.c_str(), UriEncode(root).c_str()); + } else { + // File URI + return StringFromFormat("content://%s/tree/%s/document/%s", provider.c_str(), UriEncode(root).c_str(), UriEncode(file).c_str()); + } + } + + const std::string &FilePath() const { + return file; + } + + const std::string &RootPath() const { + return root; + } +}; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index dd64a893b4..1cef504747 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -254,22 +254,43 @@ int Android_OpenContentUriFd(const std::string &filename) { return fd; } -std::vector Android_ListContentUri(const std::string &path) { +// 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 ""; +} + +std::vector Android_ListContentUri(const std::string &path) { if (!nativeActivity) { - return std::vector(); + return std::vector(); } auto env = getEnv(); jstring param = env->NewStringUTF(path.c_str()); jobject retval = env->CallObjectMethod(nativeActivity, listContentUriDir, param); jobjectArray fileList = (jobjectArray)retval; - std::vector items; + std::vector items; int size = env->GetArrayLength(fileList); for (int i = 0; i < size; i++) { jstring str = (jstring) env->GetObjectArrayElement(fileList, i); const char *charArray = env->GetStringUTFChars(str, 0); if (charArray) { // paranoia - items.push_back(std::string(charArray)); + std::string file = charArray; + INFO_LOG(FILESYS, "!! %s", file.c_str()); + std::vector parts; + SplitString(file, '|', parts); + if (parts.size() != 4) { + continue; + } + File::FileInfo info; + info.name = parts[2]; + info.isDirectory = parts[0][0] == 'D'; + info.exists = true; + sscanf(parts[1].c_str(), "%ld", &info.size); + info.fullName = Path(parts[3]); + info.isWritable = false; // We don't yet request write access + items.push_back(info); } env->ReleaseStringUTFChars(str, charArray); env->DeleteLocalRef(str); diff --git a/android/jni/app-android.h b/android/jni/app-android.h index 24662ca210..cd0a565f42 100644 --- a/android/jni/app-android.h +++ b/android/jni/app-android.h @@ -6,6 +6,7 @@ #include #include "Common/LogManager.h" +#include "Common/File/DirListing.h" #if PPSSPP_PLATFORM(ANDROID) @@ -23,8 +24,11 @@ extern std::string g_extFilesDir; // Called from PathBrowser for example. -bool Android_IsContentUri(const std::string &filename); -int Android_OpenContentUriFd(const std::string &filename); -std::vector Android_ListContentUri(const std::string &filename); +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 + + +std::vector Android_ListContentUri(const std::string &uri); #endif diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index b4e74c70cc..94b77b141a 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -135,7 +135,6 @@ public class PpssppActivity extends NativeActivity { try { Uri uri = Uri.parse(uriString); DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri); - Log.i(TAG, "Listing content directory: " + documentFile.getUri()); DocumentFile[] children = documentFile.listFiles(); ArrayList listing = new ArrayList(); // Encode entries into strings for JNI simplicity. @@ -146,7 +145,6 @@ public class PpssppActivity extends NativeActivity { } // TODO: Should we do something with child.isVirtual()?. typeStr += file.length() + "|" + file.getName() + "|" + file.getUri(); - Log.i(TAG, "> " + typeStr); listing.add(typeStr); } // Is ArrayList weird or what? diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 47c48c1eda..8a23cf77af 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -58,6 +58,8 @@ #include "Core/MIPS/MIPSVFPUUtils.h" #include "GPU/Common/TextureDecoder.h" +#include "android/jni/AndroidContentURI.h" + #include "unittest/JitHarness.h" #include "unittest/TestVertexJit.h" #include "unittest/UnitTest.h" @@ -606,6 +608,28 @@ static bool TestPath() { return true; } +static bool TestAndroidContentURI() { + static const char *treeURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO"; + static const char *directoryURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO"; + static const char *fileURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO%2FTekken%206.iso"; + + AndroidStorageContentURI treeURI; + EXPECT_TRUE(treeURI.Parse(std::string(treeURIString))); + AndroidStorageContentURI fileURI; + EXPECT_TRUE(fileURI.Parse(std::string(fileURIString))); + + EXPECT_TRUE(treeURI.TreeContains(fileURI)); + + EXPECT_TRUE(fileURI.CanNavigateUp()); + fileURI.NavigateUp(); + EXPECT_FALSE(fileURI.CanNavigateUp()); + + EXPECT_EQ_STR(fileURI.FilePath(), fileURI.RootPath()); + + EXPECT_EQ_STR(fileURI.ToString(), std::string(directoryURIString)); + return true; +} + typedef bool (*TestFunc)(); struct TestItem { const char *name; @@ -643,6 +667,7 @@ TestItem availableTests[] = { TEST_ITEM(MemMap), TEST_ITEM(ShaderGenerators), TEST_ITEM(Path), + TEST_ITEM(AndroidContentURI), }; int main(int argc, const char *argv[]) {