Start experimenting with DocumentsContract (the thing DocumentFile wraps)

wip

Some progress towards making the file browser work with DOCUMENT_TREE

More directory browsing progress

More Scoped Storage hackery. Can now browse to a folder and use PPSSPP's game browser to load ISOs from it.

Remove the defunct fdopendir approach. Buildfixes.
This commit is contained in:
Henrik Rydgård 2021-02-28 13:46:08 +01:00
parent 5030f1f719
commit 87a25fd230
13 changed files with 152 additions and 55 deletions

View File

@ -20,6 +20,7 @@
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
#include "Common/Net/URL.h"
#include "Common/File/DirListing.h"
#include "Common/File/FileUtil.h"

View File

@ -3,7 +3,7 @@
#include <string>
#include <vector>
#include <stdio.h>
#include <cstdio>
#include <inttypes.h>
@ -30,10 +30,10 @@ struct FileInfo {
bool GetFileInfo(const Path &path, FileInfo *fileInfo);
enum {
GETFILES_GETHIDDEN = 1
GETFILES_GETHIDDEN = 1,
GETFILES_URIENCODE_ANDROID = 2, // Android shenanigans
};
size_t GetFilesInDir(const Path &directory, std::vector<FileInfo> *files, const char *filter = nullptr, int flags = 0);
int64_t GetDirectoryRecursiveSize(const Path &path, const char *filter = nullptr, int flags = 0);

View File

@ -9,6 +9,7 @@
#include "Common/File/PathBrowser.h"
#include "Common/File/FileUtil.h"
#include "Common/File/DirListing.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Common/Log.h"
@ -16,6 +17,10 @@
#include "Core/System.h"
#if PPSSPP_PLATFORM(ANDROID)
#include "android/jni/app-android.h"
#endif
bool LoadRemoteFileList(const Path &url, bool *cancel, std::vector<File::FileInfo> &files) {
_dbg_assert_(url.Type() == PathType::HTTP);
@ -231,11 +236,11 @@ bool PathBrowser::GetListing(std::vector<File::FileInfo> &fileInfo, const char *
while (!IsListingReady() && (!cancel || !*cancel)) {
// In case cancel changes, just sleep.
guard.unlock();
sleep_ms(100);
sleep_ms(50);
guard.lock();
}
#ifdef _WIN32
#if PPSSPP_PLATFORM(WINDOWS)
if (path_.IsRoot()) {
// Special path that means root of file system.
std::vector<std::string> drives = File::GetWindowsDrives();
@ -254,6 +259,30 @@ bool PathBrowser::GetListing(std::vector<File::FileInfo> &fileInfo, const char *
}
#endif
#if PPSSPP_PLATFORM(ANDROID)
if (Android_IsContentUri(path_.ToString())) {
std::vector<std::string> 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;
}
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;
}
#endif
if (path_.Type() == PathType::HTTP) {
fileInfo = ApplyFilter(pendingFiles_, filter);
return true;
@ -272,6 +301,15 @@ bool PathBrowser::CanNavigateUp() {
}
#endif
*/
#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;
}
#endif
return path_.CanNavigateUp();
}

View File

@ -155,7 +155,7 @@ std::string UriDecode(const std::string & sSrc)
return sResult;
}
// Only alphanum is safe.
// Only alphanum and underscore is safe.
const char SAFE[256] =
{
/* 0 1 2 3 4 5 6 7 8 9 A B C D E F */
@ -165,7 +165,7 @@ const char SAFE[256] =
/* 3 */ 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
/* 4 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
/* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
/* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,1, // last here is underscore. it's ok.
/* 6 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
/* 7 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,

View File

@ -337,7 +337,7 @@ bool LoadFile(FileLoader **fileLoaderPtr, std::string *error_string) {
case IdentifiedFileType::UNKNOWN_ELF:
case IdentifiedFileType::UNKNOWN:
default:
ERROR_LOG(LOADER, "Failed to identify file");
ERROR_LOG(LOADER, "Failed to identify file: %s", fileLoader->GetPath().c_str());
*error_string = "Failed to identify file";
break;
}

View File

@ -1206,34 +1206,7 @@ void MainScreen::sendMessage(const char *message, const char *value) {
LaunchFile(screenManager(), Path(std::string(value)));
}
if (!strcmp(message, "browse_folderSelect")) {
std::string filename;
#if PPSSPP_PLATFORM(ANDROID)
// Hacky way to get a normal path from a Android Storage Framework path.
// Is not gonna work forever, but ship-hack for 1.11.
std::string url = value;
const char *prefix = "content://com.android.externalstorage.documents/tree/";
const char *primaryPrefix = "/storage/primary/";
if (startsWith(url, prefix)) {
url = UriDecode(url.substr(strlen(prefix)));
size_t colonPos = url.find(":");
if (colonPos != std::string::npos) {
url[colonPos] = '/';
}
url = "/storage/" + url;
if (startsWith(url, primaryPrefix)) {
url = g_Config.memStickDirectory.ToString() + url.substr(strlen(primaryPrefix));
}
INFO_LOG(SYSTEM, "Translated '%s' into '%s'", value, url.c_str());
} else {
// It's not gonna work.
// TODO: Show an error message?
INFO_LOG(SYSTEM, "Failed to parse content string: '%s'", value);
return;
}
filename = url;
#else
filename = value;
#endif
std::string filename = value;
INFO_LOG(SYSTEM, "Got folder: '%s'", filename.c_str());
int tab = tabHolder_->GetCurrentTab();
// Don't allow browsing in the other tabs (I don't think it's possible to reach the option though)

View File

@ -482,13 +482,13 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch
host = new NativeHost();
}
#endif
if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {
g_Config.externalDirectory = Path(external_dir);
#if PPSSPP_PLATFORM(ANDROID)
if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {
g_Config.externalDirectory = Path(g_extFilesDir);
#endif
} else {
g_Config.externalDirectory = Path(external_dir);
}
#endif
g_Config.defaultCurrentDirectory = Path("/");
g_Config.internalDataDirectory = Path(savegame_dir);

View File

@ -30,7 +30,7 @@
<uses-permission-sdk-23 android:name="android.permission.RECORD_AUDIO" />
<!-- AndroidX minimum SDK workaround. We don't care if it's broken on older versions. -->
<uses-sdk tools:overrideLibrary="androidx.appcompat.resources,androidx.appcompat,androidx.fragment,androidx.drawerlayout,androidx.vectordrawable.animated,androidx.vectordrawable,androidx.viewpager,androidx.loader,androidx.activity,androidx.annotation,androidx.customview,androidx.cursoradapter,androidx.arch,androidx.collection,androidx.core,androidx.versionedparcelable,androidx.interpolator,androidx.lifecycle,androidx.loader,androidx.savedstate,androidx.lifecycle.viewmodel,androidx.lifecycle.livedata,androidx.lifecycle.livedata.core,androidx.arch.core"/>
<uses-sdk tools:overrideLibrary="androidx.appcompat.resources,androidx.appcompat,androidx.fragment,androidx.drawerlayout,androidx.vectordrawable.animated,androidx.vectordrawable,androidx.viewpager,androidx.loader,androidx.activity,androidx.annotation,androidx.customview,androidx.cursoradapter,androidx.arch,androidx.collection,androidx.core,androidx.versionedparcelable,androidx.interpolator,androidx.lifecycle,androidx.loader,androidx.savedstate,androidx.lifecycle.viewmodel,androidx.lifecycle.livedata,androidx.lifecycle.livedata.core,androidx.arch.core,androidx.documentfile"/>
<supports-screens
android:largeScreens="true"

View File

@ -15,7 +15,8 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompat_version"
// For loading and tinting drawables on older versions of the platform
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
// implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation "androidx.documentfile:documentfile:1.0.1"
}
android {

View File

@ -172,6 +172,7 @@ static float g_safeInsetBottom = 0.0;
static jmethodID postCommand;
static jmethodID openContentUri;
static jmethodID listContentUriDir;
static jmethodID closeContentUri;
static jobject nativeActivity;
@ -241,12 +242,42 @@ int Android_OpenContentUriFd(const std::string &filename) {
if (!nativeActivity) {
return -1;
}
std::string fname = filename;
// PPSSPP adds an ending slash to directories before looking them up.
// TODO: Fix that in the caller (or don't call this for directories).
if (fname.back() == '/')
fname.pop_back();
auto env = getEnv();
jstring param = env->NewStringUTF(filename.c_str());
jstring param = env->NewStringUTF(fname.c_str());
int fd = env->CallIntMethod(nativeActivity, openContentUri, param);
return fd;
}
std::vector<std::string> Android_ListContentUri(const std::string &path) {
if (!nativeActivity) {
return std::vector<std::string>();
}
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;
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));
}
env->ReleaseStringUTFChars(str, charArray);
env->DeleteLocalRef(str);
}
env->DeleteLocalRef(fileList);
return items;
}
class ContentURIFileLoader : public ProxiedFileLoader {
public:
ContentURIFileLoader(const Path &filename)
@ -518,7 +549,11 @@ std::string GetJavaString(JNIEnv *env, jstring jstr) {
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_registerCallbacks(JNIEnv *env, jobject obj) {
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);
}
extern "C" void Java_org_ppsspp_ppsspp_NativeActivity_unregisterCallbacks(JNIEnv *env, jobject obj) {

View File

@ -3,6 +3,8 @@
#include "ppsspp_config.h"
#include <string>
#include <vector>
#include "Common/LogManager.h"
#if PPSSPP_PLATFORM(ANDROID)
@ -12,11 +14,17 @@
jclass findClass(const char* name);
JNIEnv* getEnv();
#endif
class AndroidLogger : public LogListener {
public:
void Log(const LogMessage &message) override;
};
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);
#endif

View File

@ -27,6 +27,7 @@ import android.os.PowerManager;
import android.os.Vibrator;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import androidx.documentfile.provider.DocumentFile;
import android.text.InputType;
import android.util.Log;
import android.view.Gravity;
@ -102,7 +103,7 @@ public abstract class NativeActivity extends Activity {
private boolean shuttingDown;
private static final int RESULT_LOAD_IMAGE = 1;
private static final int RESULT_BROWSE_FILE = 2;
private static final int RESULT_OPEN_DOCUMENT = 2;
private static final int RESULT_OPEN_DOCUMENT_TREE = 3;
// Allow for multiple connected gamepads but just consider them the same for now.
@ -1141,7 +1142,7 @@ public abstract class NativeActivity extends Activity {
cursor.close();
NativeApp.sendMessage("bgImage_updated", picturePath);
}
} else if (requestCode == RESULT_BROWSE_FILE) {
} else if (requestCode == RESULT_OPEN_DOCUMENT) {
Uri selectedFile = data.getData();
if (selectedFile != null) {
// Grab permanent permission so we can show it in recents list etc.
@ -1152,12 +1153,20 @@ public abstract class NativeActivity extends Activity {
NativeApp.sendMessage("browse_fileSelect", selectedFile.toString());
}
} else if (requestCode == RESULT_OPEN_DOCUMENT_TREE) {
Uri selectedFile = data.getData();
if (selectedFile != null) {
// Convert URI to normal path. (This might not be possible in Android 12+)
String path = selectedFile.toString();
Uri selectedDirectoryUri = data.getData();
if (selectedDirectoryUri != null) {
String path = selectedDirectoryUri.toString();
Log.i(TAG, "Browse folder finished: " + path);
NativeApp.sendMessage("browse_folderSelect", path);
Log.i(TAG, "is tree:" + DocumentsContract.isTreeUri(selectedDirectoryUri));
getContentResolver().takePersistableUriPermission(selectedDirectoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
DocumentFile documentFile = DocumentFile.fromTreeUri(this, selectedDirectoryUri);
Log.i(TAG, "Document name: " + documentFile.getUri());
DocumentFile[] children = documentFile.listFiles();
for (DocumentFile child : children) {
Log.i(TAG, "Child: " + child.getUri() + " " + child.getName());
}
NativeApp.sendMessage("browse_folderSelect", documentFile.getUri().toString());
}
}
}
@ -1296,8 +1305,11 @@ public abstract class NativeActivity extends Activity {
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
//intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
startActivityForResult(intent, RESULT_BROWSE_FILE);
// Possible alternative approach:
// String[] mimeTypes = {"application/octet-stream", "/x-iso9660-image"};
// intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
startActivityForResult(intent, RESULT_OPEN_DOCUMENT);
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
} catch (Exception e) {
Log.e(TAG, e.toString());
return false;
@ -1307,7 +1319,7 @@ public abstract class NativeActivity extends Activity {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); // not yet used properly
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); // Only allow local folders.
startActivityForResult(intent, RESULT_OPEN_DOCUMENT_TREE);
return true;

View File

@ -8,6 +8,8 @@ import android.os.Bundle;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import java.util.ArrayList;
public class PpssppActivity extends NativeActivity {
private static final String TAG = "PpssppActivity";
@ -128,4 +130,31 @@ public class PpssppActivity extends NativeActivity {
return -1;
}
}
public String[] listContentUriDir(String uriString) {
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.
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();
Log.i(TAG, "> " + typeStr);
listing.add(typeStr);
}
// Is ArrayList weird or what?
String[] strings = new String[listing.size()];
return listing.toArray(strings);
} catch (Exception e) {
Log.e(TAG, "Exception opening content uri: " + e.toString());
return new String[]{};
}
}
}