diff --git a/core/nullDC.cpp b/core/nullDC.cpp index 5129cd830..d18543a0c 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -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 +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& 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; diff --git a/core/oslib/storage.cpp b/core/oslib/storage.cpp index 0aaa4b090..70a0c580c 100644 --- a/core/oslib/storage.cpp +++ b/core/oslib/storage.cpp @@ -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); } } diff --git a/core/oslib/storage.h b/core/oslib/storage.h index ae5a1531c..416832d90 100644 --- a/core/oslib/storage.h +++ b/core/oslib/storage.h @@ -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 diff --git a/core/rend/CustomTexture.cpp b/core/rend/CustomTexture.cpp index fd082cdd0..93078201f 100644 --- a/core/rend/CustomTexture.cpp +++ b/core/rend/CustomTexture.cpp @@ -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; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 43ec7bcfe..a1843b602 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -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 diff --git a/shell/android-studio/flycast/build.gradle b/shell/android-studio/flycast/build.gradle index c209f1794..545c5731d 100644 --- a/shell/android-studio/flycast/build.gradle +++ b/shell/android-studio/flycast/build.gradle @@ -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' } diff --git a/shell/android-studio/flycast/src/androidTest/java/com/flycast/emulator/AndroidStorageTest.java b/shell/android-studio/flycast/src/androidTest/java/com/flycast/emulator/AndroidStorageTest.java new file mode 100644 index 000000000..9a9519c67 --- /dev/null +++ b/shell/android-studio/flycast/src/androidTest/java/com/flycast/emulator/AndroidStorageTest.java @@ -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 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 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); + } + }); + } +} \ No newline at end of file diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java index 0954cb3ba..e3ac631fe 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/AndroidStorage.java @@ -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 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]; diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java index 1d02e1dff..a4cd5ebe1 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/BaseGLActivity.java @@ -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(); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/FileInfo.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/FileInfo.java index 5b036a33d..ba007f8aa 100644 --- a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/FileInfo.java +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/FileInfo.java @@ -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; } diff --git a/shell/android-studio/flycast/src/main/jni/src/android_storage.h b/shell/android-studio/flycast/src/main/jni/src/android_storage.h index b35fcb11a..3fcb217b4 100644 --- a/shell/android-studio/flycast/src/main/jni/src/android_storage.h +++ b/shell/android-studio/flycast/src/main/jni/src/android_storage.h @@ -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); }; diff --git a/shell/android-studio/gradle.properties b/shell/android-studio/gradle.properties index 534063aa9..aaace3531 100644 --- a/shell/android-studio/gradle.properties +++ b/shell/android-studio/gradle.properties @@ -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