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:
parent
9976fe10fb
commit
83a27b9125
|
@ -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)
|
||||
|
|
137
core/ui/gui.cpp
137
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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,3 +310,5 @@ private:
|
|||
u64 endTime = 0;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
||||
std::string middleEllipsis(const std::string& s, float width);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue