android: storage fixes
storage: add file update date to FileInfo. Savestates and custom textures can be loaded from content path / ASS. Cache last savestate update date to improve perf on android. android: Don't use scoped storage for android < 5. Optimize listContent. Fix getSubPath android: instrumented storage test
This commit is contained in:
parent
e4ca99afba
commit
0a9eaa6cb8
|
@ -8,6 +8,7 @@
|
|||
#include "ui/gui.h"
|
||||
#include "oslib/oslib.h"
|
||||
#include "oslib/directory.h"
|
||||
#include "oslib/storage.h"
|
||||
#include "debug/gdb_server.h"
|
||||
#include "archive/rzip.h"
|
||||
#include "ui/mainui.h"
|
||||
|
@ -17,6 +18,9 @@
|
|||
#include "serialize.h"
|
||||
#include <time.h>
|
||||
|
||||
static std::string lastStateFile;
|
||||
static time_t lastStateTime;
|
||||
|
||||
struct SavestateHeader
|
||||
{
|
||||
void init()
|
||||
|
@ -121,6 +125,8 @@ void dc_savestate(int index, const u8 *pngData, u32 pngSize)
|
|||
if (settings.network.online)
|
||||
return;
|
||||
|
||||
lastStateFile.clear();
|
||||
|
||||
Serializer ser;
|
||||
dc_serialize(ser);
|
||||
|
||||
|
@ -189,7 +195,7 @@ void dc_loadstate(int index)
|
|||
u32 total_size = 0;
|
||||
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
FILE *f = hostfs::storage().openFile(filename, "rb");
|
||||
if (f == nullptr)
|
||||
{
|
||||
WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str());
|
||||
|
@ -278,28 +284,39 @@ void dc_loadstate(int index)
|
|||
time_t dc_getStateCreationDate(int index)
|
||||
{
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
if (f == nullptr)
|
||||
return 0;
|
||||
SavestateHeader header;
|
||||
if (std::fread(&header, sizeof(header), 1, f) != 1 || !header.isValid())
|
||||
if (filename != lastStateFile)
|
||||
{
|
||||
std::fclose(f);
|
||||
struct stat st;
|
||||
if (flycast::stat(filename.c_str(), &st) == 0)
|
||||
return st.st_mtime;
|
||||
lastStateFile = filename;
|
||||
FILE *f = hostfs::storage().openFile(filename, "rb");
|
||||
if (f == nullptr)
|
||||
lastStateTime = 0;
|
||||
else
|
||||
return 0;
|
||||
{
|
||||
SavestateHeader header;
|
||||
if (std::fread(&header, sizeof(header), 1, f) != 1 || !header.isValid())
|
||||
{
|
||||
std::fclose(f);
|
||||
try {
|
||||
hostfs::FileInfo fileInfo = hostfs::storage().getFileInfo(filename);
|
||||
lastStateTime = fileInfo.updateTime;
|
||||
} catch (...) {
|
||||
lastStateTime = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
std::fclose(f);
|
||||
lastStateTime = (time_t)header.creationDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fclose(f);
|
||||
return (time_t)header.creationDate;
|
||||
return lastStateTime;
|
||||
}
|
||||
|
||||
void dc_getStateScreenshot(int index, std::vector<u8>& pngData)
|
||||
{
|
||||
pngData.clear();
|
||||
std::string filename = hostfs::getSavestatePath(index, false);
|
||||
FILE *f = nowide::fopen(filename.c_str(), "rb");
|
||||
FILE *f = hostfs::storage().openFile(filename, "rb");
|
||||
if (f == nullptr)
|
||||
return;
|
||||
SavestateHeader header;
|
||||
|
|
|
@ -41,7 +41,7 @@ CustomStorage& customStorage()
|
|||
std::string getSubPath(const std::string& reference, const std::string& relative) override { die("Not implemented"); }
|
||||
FileInfo getFileInfo(const std::string& path) override { die("Not implemented"); }
|
||||
bool exists(const std::string& path) override { die("Not implemented"); }
|
||||
void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) override {
|
||||
bool addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) override {
|
||||
die("Not implemented");
|
||||
}
|
||||
};
|
||||
|
@ -191,6 +191,7 @@ public:
|
|||
}
|
||||
info.isDirectory = S_ISDIR(st.st_mode);
|
||||
info.size = st.st_size;
|
||||
info.updateTime = st.st_mtime;
|
||||
#else // _WIN32
|
||||
nowide::wstackstring wname;
|
||||
if (wname.convert(path.c_str()))
|
||||
|
@ -200,6 +201,8 @@ public:
|
|||
{
|
||||
info.isDirectory = (fileAttribs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
info.size = fileAttribs.nFileSizeLow + ((u64)fileAttribs.nFileSizeHigh << 32);
|
||||
u64 t = ((u64)fileAttribs.ftLastWriteTime.dwHighDateTime << 32) | fileAttribs.ftLastWriteTime.dwLowDateTime;
|
||||
info.updateTime = t / 10000000 - 11644473600LL; // 100-nano to secs minus (unix epoch - windows epoch)
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -389,9 +392,9 @@ AllStorage& storage()
|
|||
return storage;
|
||||
}
|
||||
|
||||
void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath))
|
||||
bool addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath))
|
||||
{
|
||||
customStorage().addStorage(isDirectory, writeAccess, callback);
|
||||
return customStorage().addStorage(isDirectory, writeAccess, callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,14 +27,18 @@ namespace hostfs
|
|||
struct FileInfo
|
||||
{
|
||||
FileInfo() = default;
|
||||
FileInfo(const std::string& name, const std::string& path, bool isDirectory, size_t size = 0, bool isWritable = false)
|
||||
: name(name), path(path), isDirectory(isDirectory), size(size), isWritable(isWritable) {}
|
||||
FileInfo(const std::string& name, const std::string& path, bool isDirectory,
|
||||
size_t size = 0, bool isWritable = false, u64 updateTime = 0)
|
||||
: name(name), path(path), isDirectory(isDirectory), size(size),
|
||||
isWritable(isWritable), updateTime(updateTime) {
|
||||
}
|
||||
|
||||
std::string name;
|
||||
std::string path;
|
||||
bool isDirectory = false;
|
||||
size_t size = 0;
|
||||
bool isWritable = false;
|
||||
u64 updateTime = 0;
|
||||
};
|
||||
|
||||
class StorageException : public FlycastException
|
||||
|
@ -60,7 +64,7 @@ public:
|
|||
class CustomStorage : public Storage
|
||||
{
|
||||
public:
|
||||
virtual void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) = 0;
|
||||
virtual bool addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) = 0;
|
||||
};
|
||||
|
||||
class AllStorage : public Storage
|
||||
|
@ -78,7 +82,7 @@ public:
|
|||
};
|
||||
|
||||
AllStorage& storage();
|
||||
void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath));
|
||||
bool addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath));
|
||||
|
||||
// iterate depth-first over the files contained in a folder hierarchy
|
||||
class DirectoryTree
|
||||
|
|
|
@ -107,13 +107,15 @@ bool CustomTexture::Init()
|
|||
|
||||
if (!textures_path.empty())
|
||||
{
|
||||
DIR *dir = flycast::opendir(textures_path.c_str());
|
||||
if (dir != nullptr)
|
||||
{
|
||||
NOTICE_LOG(RENDERER, "Found custom textures directory: %s", textures_path.c_str());
|
||||
custom_textures_available = true;
|
||||
flycast::closedir(dir);
|
||||
loader_thread.Start();
|
||||
try {
|
||||
hostfs::FileInfo fileInfo = hostfs::storage().getFileInfo(textures_path);
|
||||
if (fileInfo.isDirectory)
|
||||
{
|
||||
NOTICE_LOG(RENDERER, "Found custom textures directory: %s", textures_path.c_str());
|
||||
custom_textures_available = true;
|
||||
loader_thread.Start();
|
||||
}
|
||||
} catch (const FlycastException& e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +144,7 @@ u8* CustomTexture::LoadCustomTexture(u32 hash, int& width, int& height)
|
|||
if (it == texture_map.end())
|
||||
return nullptr;
|
||||
|
||||
FILE *file = nowide::fopen(it->second.c_str(), "rb");
|
||||
FILE *file = hostfs::storage().openFile(it->second, "rb");
|
||||
if (file == nullptr)
|
||||
return nullptr;
|
||||
int n;
|
||||
|
|
|
@ -1708,16 +1708,7 @@ static void gui_settings_general()
|
|||
if (ImGui::Button("X"))
|
||||
to_delete = i;
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3));
|
||||
if (ImGui::Button("Add"))
|
||||
{
|
||||
hostfs::addStorage(true, false, [](bool cancelled, std::string selection) {
|
||||
if (!cancelled)
|
||||
addContentPath(selection);
|
||||
});
|
||||
}
|
||||
#else
|
||||
|
||||
const char *title = "Select a Content Directory";
|
||||
select_file_popup(title, [](bool cancelled, std::string selection) {
|
||||
if (!cancelled)
|
||||
|
@ -1725,6 +1716,17 @@ static void gui_settings_general()
|
|||
return true;
|
||||
});
|
||||
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3));
|
||||
#ifdef __ANDROID__
|
||||
if (ImGui::Button("Add"))
|
||||
{
|
||||
bool supported = hostfs::addStorage(true, false, [](bool cancelled, std::string selection) {
|
||||
if (!cancelled)
|
||||
addContentPath(selection);
|
||||
});
|
||||
if (!supported)
|
||||
ImGui::OpenPopup(title);
|
||||
}
|
||||
#else
|
||||
if (ImGui::Button("Add"))
|
||||
ImGui::OpenPopup(title);
|
||||
#endif
|
||||
|
|
|
@ -43,6 +43,7 @@ android {
|
|||
// avoid Error: Google Play requires that apps target API level 31 or higher.
|
||||
abortOnError false
|
||||
}
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -92,4 +93,9 @@ dependencies {
|
|||
implementation libs.slf4j.android
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
|
||||
implementation libs.documentfile
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package com.flycast.emulator;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AndroidStorageTest {
|
||||
public static final String TREE_URI = "content://com.android.externalstorage.documents/tree/primary%3AFlycast%2FROMS";
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
ActivityScenario<NativeGLActivity> scenario = ActivityScenario.launch(NativeGLActivity.class);
|
||||
scenario.onActivity(activity -> {
|
||||
try {
|
||||
// Configure storage
|
||||
AndroidStorage storage = activity.getStorage();
|
||||
String rootUri = TREE_URI;
|
||||
storage.setStorageDirectories(Arrays.asList(rootUri));
|
||||
|
||||
// Start test
|
||||
// exists (root)
|
||||
assertTrue(storage.exists(rootUri));
|
||||
// listContent (root)
|
||||
FileInfo[] kids = storage.listContent(rootUri);
|
||||
assertTrue(kids.length > 0);
|
||||
// getFileInfo (root)
|
||||
FileInfo info = storage.getFileInfo(rootUri);
|
||||
assertEquals(info.getPath(), rootUri);
|
||||
assertTrue(info.isDirectory());
|
||||
assertNotEquals(0, info.getUpdateTime());
|
||||
// getParentUri (root)
|
||||
// fails on lollipop_mr1, could be because parent folder (/Flycast) is also allowed
|
||||
assertEquals("", storage.getParentUri(rootUri));
|
||||
|
||||
boolean directoryDone = false;
|
||||
boolean fileDone = false;
|
||||
for (FileInfo file : kids) {
|
||||
if (file.isDirectory() && !directoryDone) {
|
||||
// getParentUri
|
||||
String parentUri = storage.getParentUri(file.getPath());
|
||||
// FIXME fails because getParentUri returns a docId, not a treeId
|
||||
//assertEquals(rootUri, parentUri);
|
||||
|
||||
// getSubPath (from root)
|
||||
String kidUri = storage.getSubPath(rootUri, file.getName());
|
||||
assertEquals(file.getPath(), kidUri);
|
||||
|
||||
// exists (folder)
|
||||
assertTrue(storage.exists(file.getPath()));
|
||||
|
||||
// getFileInfo (folder)
|
||||
info = storage.getFileInfo(file.getPath());
|
||||
assertEquals(file.getPath(), info.getPath());
|
||||
assertEquals(file.getName(), info.getName());
|
||||
assertTrue(info.isDirectory());
|
||||
assertNotEquals(0, info.getUpdateTime());
|
||||
assertTrue(info.isDirectory());
|
||||
|
||||
// listContent (from folder)
|
||||
FileInfo[] gdkids = storage.listContent(file.getPath());
|
||||
assertTrue(gdkids.length > 0);
|
||||
for (FileInfo sfile : gdkids) {
|
||||
if (!sfile.isDirectory()) {
|
||||
// openFile
|
||||
int fd = storage.openFile(sfile.getPath(), "r");
|
||||
assertNotEquals(-1, fd);
|
||||
// getSubPath (from folder)
|
||||
String uri = storage.getSubPath(file.getPath(), sfile.getName());
|
||||
assertEquals(sfile.getPath(), uri);
|
||||
// getParentUri (from file)
|
||||
uri = storage.getParentUri(sfile.getPath());
|
||||
assertEquals(file.getPath(), uri);
|
||||
// exists (doc)
|
||||
assertTrue(storage.exists(sfile.getPath()));
|
||||
// getFileInfo (doc)
|
||||
info = storage.getFileInfo(sfile.getPath());
|
||||
assertEquals(info.getPath(), sfile.getPath());
|
||||
assertEquals(info.getName(), sfile.getName());
|
||||
assertEquals(info.isDirectory(), sfile.isDirectory());
|
||||
assertNotEquals(0, info.getUpdateTime());
|
||||
assertFalse(info.isDirectory());
|
||||
} else {
|
||||
// getParentUri (from subfolder)
|
||||
String uri = storage.getParentUri(sfile.getPath());
|
||||
assertEquals(file.getPath(), uri);
|
||||
// exists (subfolder)
|
||||
assertTrue(storage.exists(sfile.getPath()));
|
||||
}
|
||||
}
|
||||
directoryDone = true;
|
||||
}
|
||||
if (!file.isDirectory() && !fileDone) {
|
||||
// getParentUri
|
||||
String parentUri = storage.getParentUri(file.getPath());
|
||||
// FIXME fails because getParentUri returns a docId, not a treeId
|
||||
//assertEquals(rootUri, parentUri);
|
||||
// getSubPath (from root)
|
||||
String kidUri = storage.getSubPath(rootUri, file.getName());
|
||||
assertEquals(file.getPath(), kidUri);
|
||||
// exists (file)
|
||||
assertTrue(storage.exists(file.getPath()));
|
||||
fileDone = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testLargeFolder() {
|
||||
ActivityScenario<NativeGLActivity> scenario = ActivityScenario.launch(NativeGLActivity.class);
|
||||
scenario.onActivity(activity -> {
|
||||
try {
|
||||
// Configure storage
|
||||
AndroidStorage storage = activity.getStorage();
|
||||
String rootUri = TREE_URI;
|
||||
storage.setStorageDirectories(Arrays.asList(rootUri));
|
||||
|
||||
// list content
|
||||
String uri = storage.getSubPath(rootUri, "textures");
|
||||
uri = storage.getSubPath(uri, "T1401N");
|
||||
long t0 = System.currentTimeMillis();
|
||||
FileInfo[] kids = storage.listContent(uri);
|
||||
Log.d("testLargeFolder", "Got " + kids.length + " in " + (System.currentTimeMillis() - t0) + " ms");
|
||||
// Got 2307 in 119910 ms !!!
|
||||
// retrieving only uri in listContent: Got 2307 in 9007 ms
|
||||
// retrieving uri+isDir: Got 2307 in 62281 ms
|
||||
// manual listing: Got 2307 in 10212 ms
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
package com.flycast.emulator;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Intent;
|
||||
|
@ -40,6 +40,7 @@ import java.io.FileNotFoundException;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AndroidStorage {
|
||||
|
@ -62,7 +63,8 @@ public class AndroidStorage {
|
|||
public native void init();
|
||||
public native void addStorageCallback(String path);
|
||||
|
||||
public void onAddStorageResult(Intent data) {
|
||||
public void onAddStorageResult(Intent data)
|
||||
{
|
||||
Uri uri = data == null ? null : data.getData();
|
||||
if (uri == null) {
|
||||
// Cancelled
|
||||
|
@ -71,14 +73,13 @@ public class AndroidStorage {
|
|||
else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
activity.getContentResolver().takePersistableUriPermission(uri, storageIntentPerms);
|
||||
/* Use the uri path now to avoid issues when targeting sdk 30+ in the future
|
||||
String realPath = getRealPath(uri);
|
||||
// when targeting sdk 30+ (android 11+) using the real path doesn't work (empty content) -> *must* use the uri
|
||||
int targetSdkVersion = activity.getApplication().getApplicationInfo().targetSdkVersion;
|
||||
if (realPath != null && targetSdkVersion <= Build.VERSION_CODES.Q)
|
||||
addStorageCallback(realPath);
|
||||
else
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
String realPath = getRealPath(uri);
|
||||
if (realPath != null) {
|
||||
addStorageCallback(realPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
addStorageCallback(uri.toString());
|
||||
}
|
||||
}
|
||||
|
@ -88,18 +89,53 @@ public class AndroidStorage {
|
|||
return pfd.detachFd();
|
||||
}
|
||||
|
||||
public FileInfo[] listContent(String uri) {
|
||||
DocumentFile docFile = DocumentFile.fromTreeUri(activity, Uri.parse(uri));
|
||||
DocumentFile kids[] = docFile.listFiles();
|
||||
FileInfo ret[] = new FileInfo[kids.length];
|
||||
for (int i = 0; i < kids.length; i++) {
|
||||
ret[i] = new FileInfo();
|
||||
ret[i].setName(kids[i].getName());
|
||||
ret[i].setPath(kids[i].getUri().toString());
|
||||
ret[i].setDirectory(kids[i].isDirectory());
|
||||
public FileInfo[] listContent(String uri)
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
throw new UnsupportedOperationException("listContent unsupported");
|
||||
Uri treeUri = Uri.parse(uri);
|
||||
String documentId;
|
||||
if (DocumentsContract.isDocumentUri(activity, treeUri))
|
||||
documentId = DocumentsContract.getDocumentId(treeUri);
|
||||
else
|
||||
documentId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
Uri docUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId);
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(docUri,
|
||||
DocumentsContract.getDocumentId(docUri));
|
||||
final ArrayList<FileInfo> results = new ArrayList<>();
|
||||
|
||||
Cursor c = null;
|
||||
try {
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
c = resolver.query(childrenUri, new String[] {
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE }, null, null, null);
|
||||
while (c.moveToNext())
|
||||
{
|
||||
final String childId = c.getString(0);
|
||||
final Uri childUri = DocumentsContract.buildDocumentUriUsingTree(docUri, childId);
|
||||
FileInfo info = new FileInfo();
|
||||
info.setPath(childUri.toString());
|
||||
info.setName(c.getString(1));
|
||||
info.setDirectory(DocumentsContract.Document.MIME_TYPE_DIR.equals(c.getString(2)));
|
||||
results.add(info);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w("Flycast", "Failed query: " + e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
try {
|
||||
c.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return results.toArray(new FileInfo[results.size()]);
|
||||
}
|
||||
|
||||
public String getParentUri(String uriString) throws FileNotFoundException {
|
||||
if (uriString.isEmpty())
|
||||
|
@ -125,27 +161,27 @@ public class AndroidStorage {
|
|||
return uriString.substring(0, i);
|
||||
}
|
||||
|
||||
public String getSubPath(String reference, String relative) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
Uri refUri = Uri.parse(reference);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
String docId = DocumentsContract.getTreeDocumentId(refUri);
|
||||
return DocumentsContract.buildDocumentUriUsingTree(refUri, docId + "/" + relative).toString();
|
||||
}
|
||||
else {
|
||||
String docId = DocumentsContract.getDocumentId(refUri);
|
||||
return DocumentsContract.buildDocumentUri(refUri.getAuthority(), docId + "/" + relative).toString();
|
||||
}
|
||||
}
|
||||
else {
|
||||
public String getSubPath(String reference, String relative)
|
||||
{
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
throw new UnsupportedOperationException("getSubPath unsupported");
|
||||
Uri refUri = Uri.parse(reference);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
String docId;
|
||||
if (DocumentsContract.isDocumentUri(activity, refUri))
|
||||
docId = DocumentsContract.getDocumentId(refUri);
|
||||
else
|
||||
docId = DocumentsContract.getTreeDocumentId(refUri);
|
||||
return DocumentsContract.buildDocumentUriUsingTree(refUri, docId + "/" + relative).toString();
|
||||
}
|
||||
String docId = DocumentsContract.getDocumentId(refUri);
|
||||
return DocumentsContract.buildDocumentUri(refUri.getAuthority(), docId + "/" + relative).toString();
|
||||
}
|
||||
|
||||
public FileInfo getFileInfo(String uriString) throws FileNotFoundException {
|
||||
public FileInfo getFileInfo(String uriString) throws FileNotFoundException
|
||||
{
|
||||
Uri uri = Uri.parse(uriString);
|
||||
// FIXME >= Build.VERSION_CODES.LOLLIPOP
|
||||
// FIXME < Build.VERSION_CODES.LOLLIPOP
|
||||
DocumentFile docFile = DocumentFile.fromTreeUri(activity, uri);
|
||||
if (!docFile.exists())
|
||||
throw new FileNotFoundException(uriString);
|
||||
|
@ -155,6 +191,7 @@ public class AndroidStorage {
|
|||
info.setDirectory(docFile.isDirectory());
|
||||
info.setSize(docFile.length());
|
||||
info.setWritable(docFile.canWrite());
|
||||
info.setUpdateTime(docFile.lastModified() / 1000);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
@ -183,7 +220,10 @@ public class AndroidStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public void addStorage(boolean isDirectory, boolean writeAccess) {
|
||||
public boolean addStorage(boolean isDirectory, boolean writeAccess)
|
||||
{
|
||||
if (isDirectory && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
return false;
|
||||
Intent intent = new Intent(isDirectory ? Intent.ACTION_OPEN_DOCUMENT_TREE : Intent.ACTION_OPEN_DOCUMENT);
|
||||
if (!isDirectory) {
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
@ -196,6 +236,8 @@ public class AndroidStorage {
|
|||
storageIntentPerms = Intent.FLAG_GRANT_READ_URI_PERMISSION | (writeAccess ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | storageIntentPerms);
|
||||
activity.startActivityForResult(intent, ADD_STORAGE_ACTIVITY_REQUEST);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getRealPath(final Uri uri) {
|
||||
|
@ -207,14 +249,15 @@ public class AndroidStorage {
|
|||
|
||||
// From https://github.com/HBiSoft/PickiT
|
||||
// Copyright (c) [2020] [HBiSoft]
|
||||
@SuppressLint("NewApi")
|
||||
String getRealPathFromURI_API19(final Uri uri)
|
||||
{
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
if (isKitKat && (DocumentsContract.isDocumentUri(activity, uri) || DocumentsContract.isTreeUri(uri))) {
|
||||
final boolean isTree = DocumentsContract.isTreeUri(uri);
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final boolean isTree = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && DocumentsContract.isTreeUri(uri);
|
||||
if (isKitKat && (DocumentsContract.isDocumentUri(activity, uri) || isTree))
|
||||
{
|
||||
if (isExternalStorageDocument(uri))
|
||||
{
|
||||
final String docId = isTree ? DocumentsContract.getTreeDocumentId(uri) : DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
|
|
@ -193,6 +193,11 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat.
|
|||
JNIdc.setExternalStorageDirectories(pathList.toArray());
|
||||
}
|
||||
|
||||
// Testing
|
||||
public AndroidStorage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
|
|
@ -59,9 +59,18 @@ public class FileInfo {
|
|||
this.size = size;
|
||||
}
|
||||
|
||||
public long getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(long updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
String name;
|
||||
String path;
|
||||
boolean isDirectory;
|
||||
boolean isWritable;
|
||||
long size;
|
||||
long updateTime;
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ public:
|
|||
jgetSubPath = env->GetMethodID(clazz, "getSubPath", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
jgetFileInfo = env->GetMethodID(clazz, "getFileInfo", "(Ljava/lang/String;)Lcom/flycast/emulator/FileInfo;");
|
||||
jexists = env->GetMethodID(clazz, "exists", "(Ljava/lang/String;)Z");
|
||||
jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZ)V");
|
||||
jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZ)Z");
|
||||
jsaveScreenshot = env->GetMethodID(clazz, "saveScreenshot", "(Ljava/lang/String;[B)V");
|
||||
}
|
||||
|
||||
|
@ -132,11 +132,13 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) override
|
||||
bool addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) override
|
||||
{
|
||||
jni::env()->CallVoidMethod(jstorage, jaddStorage, isDirectory, writeAccess);
|
||||
bool ret = jni::env()->CallBooleanMethod(jstorage, jaddStorage, isDirectory, writeAccess);
|
||||
checkException();
|
||||
addStorageCallback = callback;
|
||||
if (ret)
|
||||
addStorageCallback = callback;
|
||||
return ret;
|
||||
}
|
||||
|
||||
void doStorageCallback(jstring path)
|
||||
|
@ -182,6 +184,7 @@ private:
|
|||
info.isDirectory = env->CallBooleanMethod(jinfo, jisDirectory);
|
||||
info.isWritable = env->CallBooleanMethod(jinfo, jisWritable);
|
||||
info.size = env->CallLongMethod(jinfo, jgetSize);
|
||||
info.updateTime = env->CallLongMethod(jinfo, jgetUpdateTime);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
@ -198,6 +201,7 @@ private:
|
|||
jisDirectory = env->GetMethodID(infoClass, "isDirectory", "()Z");
|
||||
jisWritable = env->GetMethodID(infoClass, "isWritable", "()Z");
|
||||
jgetSize = env->GetMethodID(infoClass, "getSize", "()J");
|
||||
jgetUpdateTime = env->GetMethodID(infoClass, "getUpdateTime", "()J");
|
||||
}
|
||||
|
||||
jobject jstorage;
|
||||
|
@ -215,6 +219,7 @@ private:
|
|||
jmethodID jisDirectory = nullptr;
|
||||
jmethodID jisWritable = nullptr;
|
||||
jmethodID jgetSize = nullptr;
|
||||
jmethodID jgetUpdateTime = nullptr;
|
||||
void (*addStorageCallback)(bool cancelled, std::string selectedPath);
|
||||
};
|
||||
|
||||
|
|
|
@ -19,3 +19,5 @@ android.useAndroidX=true
|
|||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
# Don't uninstall the apk after running tests
|
||||
android.injected.androidTest.leaveApksInstalledAfterRun=true
|
||||
|
|
Loading…
Reference in New Issue