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.
This commit is contained in:
Henrik Rydgård 2021-03-14 11:01:51 +01:00
parent 87a25fd230
commit e3cf04bb34
8 changed files with 198 additions and 27 deletions

View File

@ -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<File::FileInfo> &files) {
@ -261,22 +262,25 @@ bool PathBrowser::GetListing(std::vector<File::FileInfo> &fileInfo, const char *
#if PPSSPP_PLATFORM(ANDROID)
if (Android_IsContentUri(path_.ToString())) {
std::vector<std::string> files = Android_ListContentUri(path_.ToString());
std::vector<File::FileInfo> files = Android_ListContentUri(path_.ToString());
fileInfo.clear();
for (auto &file : files) {
ERROR_LOG(FILESYS, "!! %s", file.c_str());
std::vector<std::string> parts;
SplitString(file, '|', parts);
if (parts.size() != 4) {
continue;
std::vector<std::string> 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();
}

View File

@ -1006,6 +1006,7 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="..\android\jni\AndroidContentURI.h" />
<ClInclude Include="..\android\jni\AndroidVulkanContext.h">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">true</ExcludedFromBuild>

View File

@ -538,6 +538,9 @@
<ClInclude Include="..\android\jni\OpenSLContext.h">
<Filter>Other Platforms\Android</Filter>
</ClInclude>
<ClInclude Include="..\android\jni\AndroidContentURI.h">
<Filter>Other Platforms\Android</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="icon1.ico">

View File

@ -0,0 +1,107 @@
#pragma once
#include <string>
#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<std::string> 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;
}
};

View File

@ -254,22 +254,43 @@ int Android_OpenContentUriFd(const std::string &filename) {
return fd;
}
std::vector<std::string> 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<File::FileInfo> Android_ListContentUri(const std::string &path) {
if (!nativeActivity) {
return std::vector<std::string>();
return std::vector<File::FileInfo>();
}
auto env = getEnv();
jstring param = env->NewStringUTF(path.c_str());
jobject retval = env->CallObjectMethod(nativeActivity, listContentUriDir, param);
jobjectArray fileList = (jobjectArray)retval;
std::vector<std::string> items;
std::vector<File::FileInfo> 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<std::string> 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);

View File

@ -6,6 +6,7 @@
#include <vector>
#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<std::string> 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<File::FileInfo> Android_ListContentUri(const std::string &uri);
#endif

View File

@ -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<String> listing = new ArrayList<String>();
// 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?

View File

@ -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[]) {