android: import/export home folder

Use "folder" instead of "directory" in UI.
When game list is empty, show button to easily add a new content path.
Improve folder paths display in settings with middle ellipsis.
android: import/export home folder content
This commit is contained in:
Flyinghead 2024-06-21 18:37:34 +02:00
parent 9976fe10fb
commit 83a27b9125
9 changed files with 498 additions and 50 deletions

View File

@ -62,6 +62,11 @@ namespace hostfs
std::string getShaderCachePath(const std::string& filename);
void saveScreenshot(const std::string& name, const std::vector<u8>& data);
#ifdef __ANDROID__
void importHomeDirectory();
void exportHomeDirectory();
#endif
}
static inline void *allocAligned(size_t alignment, size_t size)

View File

@ -1556,7 +1556,7 @@ static void contentpath_warning_popup()
if (show_contentpath_selection)
{
scanner.stop();
const char *title = "Select a Content Directory";
const char *title = "Select a Content Folder";
ImGui::OpenPopup(title);
select_file_popup(title, [](bool cancelled, std::string selection)
{
@ -1627,17 +1627,44 @@ static void gui_debug_tab()
#endif
}
static void addContentPath(const std::string& path)
static void addContentPathCallback(const std::string& path)
{
auto& contentPath = config::ContentPath.get();
if (std::count(contentPath.begin(), contentPath.end(), path) == 0)
{
scanner.stop();
contentPath.push_back(path);
if (gui_state == GuiState::Main)
// when adding content path from empty game list
SaveSettings();
scanner.refresh();
}
}
static void addContentPath(bool start)
{
const char *title = "Select a Content Folder";
select_file_popup(title, [](bool cancelled, std::string selection) {
if (!cancelled)
addContentPathCallback(selection);
return true;
});
#ifdef __ANDROID__
if (start)
{
bool supported = hostfs::addStorage(true, false, [](bool cancelled, std::string selection) {
if (!cancelled)
addContentPathCallback(selection);
});
if (!supported)
ImGui::OpenPopup(title);
}
#else
if (start)
ImGui::OpenPopup(title);
#endif
}
static float calcComboWidth(const char *biggestLabel) {
return ImGui::CalcTextSize(biggestLabel).x + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetFrameHeight();
}
@ -1689,7 +1716,7 @@ static void gui_settings_general()
ImVec2 size;
size.x = 0.0f;
size.y = (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().FramePadding.y * 2.f)
* (config::ContentPath.get().size() + 1) ;//+ ImGui::GetStyle().FramePadding.y * 2.f;
* (config::ContentPath.get().size() + 1);
if (BeginListBox("Content Location", size, ImGuiWindowFlags_NavFlattened))
{
@ -1698,35 +1725,21 @@ static void gui_settings_general()
{
ImguiID _(config::ContentPath.get()[i].c_str());
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", config::ContentPath.get()[i].c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("X").x - ImGui::GetStyle().FramePadding.x);
if (ImGui::Button("X"))
float maxW = ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(ICON_FA_TRASH_CAN).x - ImGui::GetStyle().FramePadding.x * 2
- ImGui::GetStyle().ItemSpacing.x;
std::string s = middleEllipsis(config::ContentPath.get()[i], maxW);
ImGui::Text("%s", s.c_str());
ImGui::SameLine(0, maxW - ImGui::CalcTextSize(s.c_str()).x + ImGui::GetStyle().ItemSpacing.x);
if (ImGui::Button(ICON_FA_TRASH_CAN))
to_delete = i;
}
const char *title = "Select a Content Directory";
select_file_popup(title, [](bool cancelled, std::string selection) {
if (!cancelled)
addContentPath(selection);
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
const bool addContent = ImGui::Button("Add");
addContentPath(addContent);
ImGui::SameLine();
if (ImGui::Button("Rescan Content"))
if (ImGui::Button("Rescan Content"))
scanner.refresh();
scrollWhenDraggingOnVoid();
@ -1739,31 +1752,40 @@ static void gui_settings_general()
}
}
ImGui::SameLine();
ShowHelpMarker("The directories where your games are stored");
ShowHelpMarker("The folders where your games are stored");
size.y = ImGui::GetTextLineHeightWithSpacing() * 1.25f + ImGui::GetStyle().FramePadding.y * 2.0f;
#if defined(__linux__) && !defined(__ANDROID__)
if (BeginListBox("Data Directory", size, ImGuiWindowFlags_NavFlattened))
if (BeginListBox("Data Folder", size, ImGuiWindowFlags_NavFlattened))
{
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", get_writable_data_path("").c_str());
float w = ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x;
std::string s = middleEllipsis(get_writable_data_path(""), w);
ImGui::Text("%s", s.c_str());
ImGui::EndListBox();
}
ImGui::SameLine();
ShowHelpMarker("The directory containing BIOS files, as well as saved VMUs and states");
ShowHelpMarker("The folder containing BIOS files, as well as saved VMUs and states");
#else
if (BeginListBox("Home Directory", size, ImGuiWindowFlags_NavFlattened))
#if defined(__ANDROID__) || defined(TARGET_MAC)
size.y += ImGui::GetTextLineHeightWithSpacing() * 1.25f;
#endif
if (BeginListBox("Home Folder", size, ImGuiWindowFlags_NavFlattened))
{
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", get_writable_config_path("").c_str());
float w = ImGui::GetContentRegionAvail().x - ImGui::GetStyle().FramePadding.x;
std::string s = middleEllipsis(get_writable_config_path(""), w);
ImGui::Text("%s", s.c_str());
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3));
#ifdef __ANDROID__
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Change").x - ImGui::GetStyle().FramePadding.x);
if (ImGui::Button("Change"))
gui_setState(GuiState::Onboarding);
if (ImGui::Button("Import"))
hostfs::importHomeDirectory();
ImGui::SameLine();
if (ImGui::Button("Export"))
hostfs::exportHomeDirectory();
#endif
#ifdef TARGET_MAC
ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Reveal in Finder").x - ImGui::GetStyle().FramePadding.x);
if (ImGui::Button("Reveal in Finder"))
{
char temp[512];
@ -1774,9 +1796,15 @@ static void gui_settings_general()
ImGui::EndListBox();
}
ImGui::SameLine();
ShowHelpMarker("The directory where Flycast saves configuration files and VMUs. BIOS files should be in a subfolder named \"data\"");
ShowHelpMarker("The folder where Flycast saves configuration files and VMUs. BIOS files should be in a subfolder named \"data\"");
#endif // !linux
#endif // !TARGET_IPHONE
#else // TARGET_IPHONE
{
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(24, 3));
if (ImGui::Button("Rescan Content"))
scanner.refresh();
}
#endif
OptionCheckbox("Box Art Game List", config::BoxartDisplayMode,
"Display game cover art in the game list.");
@ -2787,7 +2815,7 @@ static void gui_settings_advanced()
{
ImGui::InputText("Lua Filename", &config::LuaFileName.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr);
ImGui::SameLine();
ShowHelpMarker("Specify lua filename to use. Should be located in Flycast config directory. Defaults to flycast.lua when empty.");
ShowHelpMarker("Specify lua filename to use. Should be located in Flycast config folder. Defaults to flycast.lua when empty.");
}
#endif
}
@ -3205,8 +3233,10 @@ static void gui_display_content()
gui_start_game("");
counter++;
}
bool gameListEmpty = false;
{
scanner.get_mutex().lock();
gameListEmpty = scanner.get_game_list().empty();
for (const auto& game : scanner.get_game_list())
{
if (gui_state == GuiState::SelectDisk)
@ -3275,7 +3305,28 @@ static void gui_display_content()
}
scanner.get_mutex().unlock();
}
bool addContent = false;
#if !defined(TARGET_IPHONE)
if (gameListEmpty && gui_state != GuiState::SelectDisk)
{
const char *label = "Your game list is empty";
// center horizontally
const float w = largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, -1.f, label).x + ImGui::GetStyle().FramePadding.x * 2;
ImGui::SameLine((ImGui::GetContentRegionMax().x - w) / 2);
if (ImGui::BeginChild("empty", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_NavFlattened))
{
ImGui::PushFont(largeFont);
ImGui::NewLine();
ImGui::Text("%s", label);
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8));
addContent = ImGui::Button("Add Game Folder");
ImGui::PopFont();
}
ImGui::EndChild();
}
#endif
ImGui::PopStyleVar();
addContentPath(addContent);
}
scrollWhenDraggingOnVoid();
windowDragScroll();
@ -3300,7 +3351,7 @@ static bool systemdir_selected_callback(bool cancelled, std::string selection)
if (!make_directory(data_path))
{
WARN_LOG(BOOT, "Cannot create 'data' directory: %s", data_path.c_str());
gui_error("Invalid selection:\nFlycast cannot write to this directory.");
gui_error("Invalid selection:\nFlycast cannot write to this folder.");
return false;
}
}
@ -3311,7 +3362,7 @@ static bool systemdir_selected_callback(bool cancelled, std::string selection)
if (file == nullptr)
{
WARN_LOG(BOOT, "Cannot write in the 'data' directory");
gui_error("Invalid selection:\nFlycast cannot write to this directory.");
gui_error("Invalid selection:\nFlycast cannot write to this folder.");
return false;
}
fclose(file);
@ -3339,7 +3390,7 @@ static bool systemdir_selected_callback(bool cancelled, std::string selection)
static void gui_display_onboarding()
{
const char *title = "Select Flycast Home Directory";
const char *title = "Select Flycast Home Folder";
ImGui::OpenPopup(title);
select_file_popup(title, &systemdir_selected_callback);
}

View File

@ -54,6 +54,7 @@ void select_file_popup(const char *prompt, StringCallback callback,
{
fullScreenWindow(true);
ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
ImguiStyleVar _1(ImGuiStyleVar_FramePadding, ImVec2(4, 3)); // default
if (ImGui::BeginPopup(prompt, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize ))
{
@ -123,7 +124,7 @@ void select_file_popup(const char *prompt, StringCallback callback,
if (!select_current_directory.empty() && select_current_directory != "/")
{
if (ImGui::Selectable(".. Up to Parent Directory"))
if (ImGui::Selectable(".. Up to Parent Folder"))
{
subfolders_read = false;
select_current_directory = hostfs::storage().getParentPath(select_current_directory);
@ -161,7 +162,7 @@ void select_file_popup(const char *prompt, StringCallback callback,
ImGui::EndChild();
if (!selectFile)
{
if (ImGui::Button("Select Current Directory", ScaledVec2(0, 30)))
if (ImGui::Button("Select Current Folder", ScaledVec2(0, 30)))
{
if (callback(false, select_current_directory))
{
@ -1009,3 +1010,33 @@ bool Toast::draw()
return true;
}
std::string middleEllipsis(const std::string& s, float width)
{
float tw = ImGui::CalcTextSize(s.c_str()).x;
if (tw <= width)
return s;
std::string ellipsis;
char buf[5];
ImTextCharToUtf8(buf, ImGui::GetFont()->EllipsisChar);
for (int i = 0; i < ImGui::GetFont()->EllipsisCharCount; i++)
ellipsis += buf;
int l = s.length() / 2;
int d = l;
while (true)
{
std::string ss = s.substr(0, l / 2) + ellipsis + s.substr(s.length() - l / 2 - (l & 1));
tw = ImGui::CalcTextSize(ss.c_str()).x;
if (tw == width)
return ss;
d /= 2;
if (d == 0)
return ss;
if (tw > width)
l -= d;
else
l += d;
}
}

View File

@ -310,3 +310,5 @@ private:
u64 endTime = 0;
std::mutex mutex;
};
std::string middleEllipsis(const std::string& s, float width);

View File

@ -39,12 +39,15 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class AndroidStorage {
public static final int ADD_STORAGE_ACTIVITY_REQUEST = 15012010;
public static final int EXPORT_HOME_ACTIVITY_REQUEST = 15012011;
public static final int IMPORT_HOME_ACTIVITY_REQUEST = 15012012;
private Activity activity;
@ -62,6 +65,7 @@ public class AndroidStorage {
public native void init();
public native void addStorageCallback(String path);
public native void reloadConfig();
public void onAddStorageResult(Intent data)
{
@ -89,6 +93,30 @@ public class AndroidStorage {
return pfd.detachFd();
}
public InputStream openInputStream(String uri) throws FileNotFoundException {
return activity.getContentResolver().openInputStream(Uri.parse(uri));
}
public OutputStream openOutputStream(String parent, String name) throws FileNotFoundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
throw new UnsupportedOperationException("not supported");
Uri uri = Uri.parse(parent);
String subpath = getSubPath(parent, name);
if (!exists(subpath)) {
String documentId;
if (DocumentsContract.isDocumentUri(activity, uri))
documentId = DocumentsContract.getDocumentId(uri);
else
documentId = DocumentsContract.getTreeDocumentId(uri);
uri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
uri = DocumentsContract.createDocument(activity.getContentResolver(), uri,
"application/octet-stream", name);
}
else {
uri = Uri.parse(subpath);
}
return activity.getContentResolver().openOutputStream(uri);
}
public FileInfo[] listContent(String uri)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@ -220,6 +248,22 @@ public class AndroidStorage {
}
}
public String mkdir(String parent, String name) throws FileNotFoundException
{
Uri parentUri = Uri.parse(parent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (!DocumentsContract.isDocumentUri(activity, parentUri)) {
String documentId = DocumentsContract.getTreeDocumentId(parentUri);
parentUri = DocumentsContract.buildDocumentUriUsingTree(parentUri, documentId);
}
Uri newDirUri = DocumentsContract.createDocument(activity.getContentResolver(), parentUri, DocumentsContract.Document.MIME_TYPE_DIR, name);
return newDirUri.toString();
}
File dir = new File(parent, name);
dir.mkdir();
return dir.getAbsolutePath();
}
public boolean addStorage(boolean isDirectory, boolean writeAccess)
{
if (isDirectory && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
@ -231,7 +275,7 @@ public class AndroidStorage {
intent = Intent.createChooser(intent, "Select a cheat file");
}
else {
intent = Intent.createChooser(intent, "Select a content directory");
intent = Intent.createChooser(intent, "Select a content folder");
}
storageIntentPerms = Intent.FLAG_GRANT_READ_URI_PERMISSION | (writeAccess ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION : 0);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | storageIntentPerms);
@ -496,4 +540,41 @@ public class AndroidStorage {
throw new RuntimeException(e.getMessage());
}
}
public void exportHomeDirectory()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent = Intent.createChooser(intent, "Select an export folder");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
activity.startActivityForResult(intent, EXPORT_HOME_ACTIVITY_REQUEST);
}
public void onExportHomeResult(Intent data)
{
Uri uri = data == null ? null : data.getData();
if (uri == null)
// Cancelled
return;
HomeMover mover = new HomeMover(activity, this);
mover.copyHome(activity.getExternalFilesDir(null).toURI().toString(), uri.toString(), "Exporting home folder");
}
public void importHomeDirectory()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent = Intent.createChooser(intent, "Select an import folder");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivityForResult(intent, IMPORT_HOME_ACTIVITY_REQUEST);
}
public void onImportHomeResult(Intent data)
{
Uri uri = data == null ? null : data.getData();
if (uri == null)
// Cancelled
return;
HomeMover mover = new HomeMover(activity, this);
mover.setReloadConfigOnCompletion(true);
mover.copyHome(uri.toString(), activity.getExternalFilesDir(null).toURI().toString(), "Importing home folder");
}
}

View File

@ -557,8 +557,18 @@ public abstract class BaseGLActivity extends Activity implements ActivityCompat.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == AndroidStorage.ADD_STORAGE_ACTIVITY_REQUEST)
storage.onAddStorageResult(data);
switch (requestCode)
{
case AndroidStorage.ADD_STORAGE_ACTIVITY_REQUEST:
storage.onAddStorageResult(data);
break;
case AndroidStorage.IMPORT_HOME_ACTIVITY_REQUEST:
storage.onImportHomeResult(data);
break;
case AndroidStorage.EXPORT_HOME_ACTIVITY_REQUEST:
storage.onExportHomeResult(data);
break;
}
}
private static native void register(BaseGLActivity activity);

View File

@ -0,0 +1,235 @@
/*
Copyright 2024 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
package com.flycast.emulator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class HomeMover {
private Activity activity;
private AndroidStorage storage;
private StorageWrapper wrapper;
private boolean migrationThreadCancelled = false;
private boolean reloadConfigOnCompletion = false;
private class StorageWrapper
{
private File getFile(String path) {
Uri uri = Uri.parse(path);
if (uri.getScheme().equals("file"))
return new File(uri.getPath());
else
return null;
}
public String getSubPath(String parent, String kid)
{
File f = getFile(parent);
if (f != null)
return new File(f, kid).toURI().toString();
else
return storage.getSubPath(parent, kid);
}
public FileInfo[] listContent(String folder)
{
File dir = getFile(folder);
if (dir != null)
{
File[] files = dir.listFiles();
List<FileInfo> ret = new ArrayList<>(files.length);
for (File f : files) {
FileInfo info = new FileInfo();
info.setName(f.getName());
info.setDirectory(f.isDirectory());
info.setPath(f.toURI().toString());
ret.add(info);
}
return ret.toArray(new FileInfo[ret.size()]);
}
else {
return storage.listContent(folder);
}
}
public InputStream openInputStream(String path) throws FileNotFoundException {
File file = getFile(path);
if (file != null)
return new FileInputStream(file);
else
return storage.openInputStream(path);
}
public OutputStream openOutputStream(String parent, String name) throws FileNotFoundException {
File file = getFile(parent);
if (file != null)
return new FileOutputStream(new File(file, name));
else
return storage.openOutputStream(parent, name);
}
public boolean exists(String path) {
File file = getFile(path);
if (file != null)
return file.exists();
else
return storage.exists(path);
}
public String mkdir(String parent, String name) throws FileNotFoundException
{
File dir = getFile(parent);
if (dir != null)
{
File subfolder = new File(dir, name);
subfolder.mkdir();
return subfolder.toURI().toString();
}
else {
return storage.mkdir(parent, name);
}
}
}
public HomeMover(Activity activity, AndroidStorage storage) {
this.activity = activity;
this.storage = storage;
this.wrapper = new StorageWrapper();
}
public void copyHome(String source, String dest, String message)
{
migrationThreadCancelled = false;
ProgressDialog progress = new ProgressDialog(activity);
progress.setTitle("Copying");
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progress.setMax(1);
progress.setOnCancelListener(dialogInterface -> migrationThreadCancelled = true);
progress.show();
Thread thread = new Thread(new Runnable() {
private void copyFile(String path, String name, String toDir)
{
//Log.d("flycast", "Copying " + path + " to " + toDir);
try {
InputStream in = wrapper.openInputStream(path);
OutputStream out = wrapper.openOutputStream(toDir, name);
byte[] buf = new byte[8192];
while (true) {
int len = in.read(buf);
if (len == -1)
break;
out.write(buf, 0, len);
}
out.close();
in.close();
} catch (Exception e) {
Log.e("flycast", "Error copying " + path, e);
}
}
private void copyDir(String from, String toParent, String name)
{
//Log.d("flycast", "Copying folder " + from + " to " + toParent + " / " + name);
if (!wrapper.exists(from))
return;
try {
String to = wrapper.getSubPath(toParent, name);
if (!wrapper.exists(to))
to = wrapper.mkdir(toParent, name);
FileInfo[] files = wrapper.listContent(from);
incrementMaxProgress(files.length);
for (FileInfo file : files)
{
if (migrationThreadCancelled)
break;
if (!file.isDirectory())
copyFile(file.path, file.name, to);
else
copyDir(file.path, to, file.getName());
incrementProgress(1);
}
} catch (Exception e) {
Log.e("flycast", "Error copying folder " + from, e);
}
}
private void migrate()
{
incrementMaxProgress(3);
String path = wrapper.getSubPath(source, "emu.cfg");
copyFile(path, "emu.cfg", dest);
if (migrationThreadCancelled)
return;
incrementProgress(1);
String srcMappings = wrapper.getSubPath(source, "mappings");
copyDir(srcMappings, dest, "mappings");
if (migrationThreadCancelled)
return;
incrementProgress(1);
String srcData = wrapper.getSubPath(source, "data");
copyDir(srcData, dest, "data");
incrementProgress(1);
}
private void incrementMaxProgress(int max) {
activity.runOnUiThread(() -> {
progress.setMax(progress.getMax() + max);
});
}
private void incrementProgress(int i) {
activity.runOnUiThread(() -> {
progress.incrementProgressBy(i);
});
}
@Override
public void run()
{
migrate();
activity.runOnUiThread(() -> {
progress.dismiss();
if (reloadConfigOnCompletion)
storage.reloadConfig();
});
}
});
thread.start();
}
public void setReloadConfigOnCompletion(boolean reloadConfigOnCompletion) {
this.reloadConfigOnCompletion = reloadConfigOnCompletion;
}
}

View File

@ -40,6 +40,8 @@ public:
jexists = env->GetMethodID(clazz, "exists", "(Ljava/lang/String;)Z");
jaddStorage = env->GetMethodID(clazz, "addStorage", "(ZZ)Z");
jsaveScreenshot = env->GetMethodID(clazz, "saveScreenshot", "(Ljava/lang/String;[B)V");
jimportHomeDirectory = env->GetMethodID(clazz, "importHomeDirectory", "()V");
jexportHomeDirectory = env->GetMethodID(clazz, "exportHomeDirectory", "()V");
}
bool isKnownPath(const std::string& path) override {
@ -162,6 +164,16 @@ public:
checkException();
}
void importHomeDirectory() {
jni::env()->CallVoidMethod(jstorage, jimportHomeDirectory);
checkException();
}
void exportHomeDirectory() {
jni::env()->CallVoidMethod(jstorage, jexportHomeDirectory);
checkException();
}
private:
void checkException()
{
@ -213,6 +225,8 @@ private:
jmethodID jgetFileInfo;
jmethodID jexists;
jmethodID jsaveScreenshot;
jmethodID jexportHomeDirectory;
jmethodID jimportHomeDirectory;
// FileInfo accessors lazily initialized to avoid having to load the class
jmethodID jgetName = nullptr;
jmethodID jgetPath = nullptr;
@ -236,6 +250,14 @@ void saveScreenshot(const std::string& name, const std::vector<u8>& data)
return static_cast<AndroidStorage&>(customStorage()).saveScreenshot(name, data);
}
void importHomeDirectory() {
static_cast<AndroidStorage&>(customStorage()).importHomeDirectory();
}
void exportHomeDirectory() {
static_cast<AndroidStorage&>(customStorage()).exportHomeDirectory();
}
} // namespace hostfs
extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_AndroidStorage_addStorageCallback(JNIEnv *env, jobject obj, jstring path)
@ -247,3 +269,14 @@ extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_AndroidStorage_init(
{
static_cast<hostfs::AndroidStorage&>(hostfs::customStorage()).init(env, jstorage);
}
extern "C" JNIEXPORT void JNICALL Java_com_flycast_emulator_AndroidStorage_reloadConfig(JNIEnv *env)
{
if (cfgOpen())
{
const RenderType render = config::RendererType;
config::Settings::instance().load(false);
// Make sure the renderer type doesn't change mid-flight
config::RendererType = render;
}
}

View File

@ -1099,8 +1099,8 @@ struct retro_core_option_v2_definition option_defs_us[] = {
CORE_OPTION_NAME "_per_content_vmus",
"Per-Game Visual Memory Units/Systems (VMU)",
"Per-Game VMUs",
"When disabled, all games share up to 8 VMU save files (A1/A2/B1/B2/C1/C2/D1/D2) located in RetroArch's system directory.\n"
"The 'VMU A1' setting creates a unique VMU 'A1' file in RetroArch's save directory for each game that is launched.\n"
"When disabled, all games share up to 8 VMU save files (A1/A2/B1/B2/C1/C2/D1/D2) located in RetroArch's system folder.\n"
"The 'VMU A1' setting creates a unique VMU 'A1' file in RetroArch's save folder for each game that is launched.\n"
"The 'All VMUs' setting creates up to 8 unique VMU files (A1/A2/B1/B2/C1/C2/D1/D2) for each game that is launched.",
NULL,
"vmu",