From 83a27b9125ef11f5597da637140afcd3829c429e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 21 Jun 2024 18:37:34 +0200 Subject: [PATCH] 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 --- core/oslib/oslib.h | 5 + core/ui/gui.cpp | 137 ++++++---- core/ui/gui_util.cpp | 35 ++- core/ui/gui_util.h | 2 + .../com/flycast/emulator/AndroidStorage.java | 83 ++++++- .../com/flycast/emulator/BaseGLActivity.java | 14 +- .../java/com/flycast/emulator/HomeMover.java | 235 ++++++++++++++++++ .../src/main/jni/src/android_storage.h | 33 +++ shell/libretro/libretro_core_options.h | 4 +- 9 files changed, 498 insertions(+), 50 deletions(-) create mode 100644 shell/android-studio/flycast/src/main/java/com/flycast/emulator/HomeMover.java diff --git a/core/oslib/oslib.h b/core/oslib/oslib.h index 985f6cb77..916b33e98 100644 --- a/core/oslib/oslib.h +++ b/core/oslib/oslib.h @@ -62,6 +62,11 @@ namespace hostfs std::string getShaderCachePath(const std::string& filename); void saveScreenshot(const std::string& name, const std::vector& data); + +#ifdef __ANDROID__ + void importHomeDirectory(); + void exportHomeDirectory(); +#endif } static inline void *allocAligned(size_t alignment, size_t size) diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index cb0fd6a91..21cbc2a0e 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -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); } diff --git a/core/ui/gui_util.cpp b/core/ui/gui_util.cpp index 8654a4d2c..83562dd96 100644 --- a/core/ui/gui_util.cpp +++ b/core/ui/gui_util.cpp @@ -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; + } +} diff --git a/core/ui/gui_util.h b/core/ui/gui_util.h index 8bb28f6ed..7fec8ebae 100644 --- a/core/ui/gui_util.h +++ b/core/ui/gui_util.h @@ -310,3 +310,5 @@ private: u64 endTime = 0; std::mutex mutex; }; + +std::string middleEllipsis(const std::string& s, float width); 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 e3ac631fe..9685b1e9f 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 @@ -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"); + } } 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 a4cd5ebe1..dcd92f866 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 @@ -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); diff --git a/shell/android-studio/flycast/src/main/java/com/flycast/emulator/HomeMover.java b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/HomeMover.java new file mode 100644 index 000000000..0bd030a86 --- /dev/null +++ b/shell/android-studio/flycast/src/main/java/com/flycast/emulator/HomeMover.java @@ -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 . +*/ +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 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; + } +} 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 3fcb217b4..d21e35ea0 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 @@ -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& data) return static_cast(customStorage()).saveScreenshot(name, data); } +void importHomeDirectory() { + static_cast(customStorage()).importHomeDirectory(); +} + +void exportHomeDirectory() { + static_cast(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::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; + } +} diff --git a/shell/libretro/libretro_core_options.h b/shell/libretro/libretro_core_options.h index 23ef7d3c6..bff923f72 100644 --- a/shell/libretro/libretro_core_options.h +++ b/shell/libretro/libretro_core_options.h @@ -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",