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:
Flyinghead 2024-06-17 09:53:32 +02:00
parent e4ca99afba
commit 0a9eaa6cb8
12 changed files with 328 additions and 85 deletions

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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'
}

View File

@ -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);
}
});
}
}

View File

@ -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];

View File

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

View File

@ -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;
}

View File

@ -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);
};

View File

@ -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