From 3c916747afa9e07137356f8f41991573427918da Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:00:07 +0300 Subject: [PATCH] Added graphic packs settings --- src/android/app/src/main/AndroidManifest.xml | 7 +- src/android/app/src/main/cpp/EmulationState.h | 82 ++-- src/android/app/src/main/cpp/native-lib.cpp | 109 ++++++ .../info/cemu/Cemu/BaseSelectionAdapter.java | 48 +++ .../BaseSingleSelectionRecyclerViewItem.java | 71 ++++ .../main/java/info/cemu/Cemu/FileUtil.java | 33 +- .../cemu/Cemu/GenericRecyclerViewAdapter.java | 8 + .../Cemu/GraphicPackItemRecyclerViewItem.java | 58 +++ .../Cemu/GraphicPackRecyclerViewItem.java | 52 +++ .../info/cemu/Cemu/GraphicPacksFragment.java | 109 ++++++ .../Cemu/GraphicPacksRecyclerViewAdapter.java | 135 +++++++ .../cemu/Cemu/GraphicPacksRootFragment.java | 354 ++++++++++++++++++ .../java/info/cemu/Cemu/NativeLibrary.java | 99 ++++- .../java/info/cemu/Cemu/OptionsFragment.java | 1 + .../java/info/cemu/Cemu/SelectionAdapter.java | 26 +- .../java/info/cemu/Cemu/SettingsActivity.java | 13 +- .../Cemu/SingleSelectionRecyclerViewItem.java | 4 +- .../cemu/Cemu/StringSelectionAdapter.java | 46 +++ .../src/main/java/info/cemu/Cemu/ZipUtil.java | 40 ++ .../main/res/drawable/ic_chevron_right.xml | 11 + .../app/src/main/res/drawable/ic_download.xml | 10 + .../app/src/main/res/drawable/ic_lists.xml | 11 + .../src/main/res/drawable/ic_package_2.xml | 10 + .../res/layout/fragment_graphic_packs.xml | 17 + .../layout/fragment_graphic_packs_root.xml | 17 + .../main/res/layout/layout_graphic_pack.xml | 27 ++ .../res/layout/layout_graphic_pack_item.xml | 34 ++ .../layout_graphic_pack_search_item.xml | 45 +++ .../layout/layout_graphic_packs_download.xml | 21 ++ .../src/main/res/menu/menu_graphic_packs.xml | 19 + .../res/navigation/nav_settings_graph.xml | 35 +- .../app/src/main/res/values/strings.xml | 21 +- 32 files changed, 1509 insertions(+), 64 deletions(-) create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/BaseSelectionAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/BaseSingleSelectionRecyclerViewItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/GraphicPackItemRecyclerViewItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/GraphicPackRecyclerViewItem.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksFragment.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRecyclerViewAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRootFragment.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/StringSelectionAdapter.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ZipUtil.java create mode 100644 src/android/app/src/main/res/drawable/ic_chevron_right.xml create mode 100644 src/android/app/src/main/res/drawable/ic_download.xml create mode 100644 src/android/app/src/main/res/drawable/ic_lists.xml create mode 100644 src/android/app/src/main/res/drawable/ic_package_2.xml create mode 100644 src/android/app/src/main/res/layout/fragment_graphic_packs.xml create mode 100644 src/android/app/src/main/res/layout/fragment_graphic_packs_root.xml create mode 100644 src/android/app/src/main/res/layout/layout_graphic_pack.xml create mode 100644 src/android/app/src/main/res/layout/layout_graphic_pack_item.xml create mode 100644 src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml create mode 100644 src/android/app/src/main/res/layout/layout_graphic_packs_download.xml create mode 100644 src/android/app/src/main/res/menu/menu_graphic_packs.xml diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index bb78d539..11fee32e 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + android:exported="false" + android:parentActivityName=".MainActivity" /> - \ No newline at end of file diff --git a/src/android/app/src/main/cpp/EmulationState.h b/src/android/app/src/main/cpp/EmulationState.h index 8640b900..2c0865c6 100644 --- a/src/android/app/src/main/cpp/EmulationState.h +++ b/src/android/app/src/main/cpp/EmulationState.h @@ -8,6 +8,7 @@ #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" #include "Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h" #include "CafeSystemUtils.h" +#include "Cafe/CafeSystem.h" #include "Cemu/GuiSystem/GuiSystem.h" #include "GameIconLoader.h" #include "GameTitleLoader.h" @@ -19,24 +20,34 @@ int mainEmulatorHLE(); -class EmulationState -{ +class EmulationState { GameIconLoader m_gameIconLoader; GameTitleLoader m_gameTitleLoader; + std::unordered_map m_graphicPacks; + void fillGraphicPacks() + { + m_graphicPacks.clear(); + auto graphicPacks = GraphicPack2::GetGraphicPacks(); + for (auto&& graphicPack : graphicPacks) + { + m_graphicPacks[reinterpret_cast(graphicPack.get())] = graphicPack; + } + } - public: + public: void initializeEmulation() { + g_config.Load(); FilesystemAndroid::setFilesystemCallbacks(std::make_shared()); NetworkConfig::LoadOnce(); - mainEmulatorHLE(); InputManager::instance().load(); InitializeGlobalVulkan(); createCemuDirectories(); - g_config.Load(); + mainEmulatorHLE(); + fillGraphicPacks(); } - void initializeActiveSettings(const fs::path &dataPath, const fs::path &cachePath) + void initializeActiveSettings(const fs::path& dataPath, const fs::path& cachePath) { ActiveSettings::LoadOnce({}, dataPath, dataPath, cachePath, dataPath); } @@ -51,12 +62,12 @@ class EmulationState VulkanRenderer::GetInstance()->NotifySurfaceChanged(isMainCanvas); } - void setSurface(JNIEnv *env, jobject surface, bool isMainCanvas) + void setSurface(JNIEnv* env, jobject surface, bool isMainCanvas) { - auto &windowHandleInfo = isMainCanvas ? GuiSystem::getWindowInfo().canvas_main : GuiSystem::getWindowInfo().canvas_pad; + auto& windowHandleInfo = isMainCanvas ? GuiSystem::getWindowInfo().canvas_main : GuiSystem::getWindowInfo().canvas_pad; if (windowHandleInfo.surface) { - ANativeWindow_release(static_cast(windowHandleInfo.surface)); + ANativeWindow_release(static_cast(windowHandleInfo.surface)); windowHandleInfo.surface = nullptr; } if (surface) @@ -67,7 +78,7 @@ class EmulationState void setSurfaceSize(int width, int height, bool isMainCanvas) { - auto &windowInfo = GuiSystem::getWindowInfo(); + auto& windowInfo = GuiSystem::getWindowInfo(); if (isMainCanvas) { windowInfo.width = windowInfo.phys_width = width; @@ -80,17 +91,17 @@ class EmulationState } } - void onKeyEvent(const std::string &deviceDescriptor, const std::string &deviceName, int keyCode, bool isPressed) + void onKeyEvent(const std::string& deviceDescriptor, const std::string& deviceName, int keyCode, bool isPressed) { auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); - auto androidControllerProvider = dynamic_cast(apiProvider.get()); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); androidControllerProvider->on_key_event(deviceDescriptor, deviceName, keyCode, isPressed); } - void onAxisEvent(const std::string &deviceDescriptor, const std::string &deviceName, int axisCode, float value) + void onAxisEvent(const std::string& deviceDescriptor, const std::string& deviceName, int axisCode, float value) { auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); - auto androidControllerProvider = dynamic_cast(apiProvider.get()); + auto androidControllerProvider = dynamic_cast(apiProvider.get()); androidControllerProvider->on_axis_event(deviceDescriptor, deviceName, axisCode, value); } @@ -120,7 +131,7 @@ class EmulationState { auto emulatedController = AndroidEmulatedController::getAndroidEmulatedController( i) - .getEmulatedController(); + .getEmulatedController(); if (!emulatedController) continue; if (emulatedController->type() != EmulatedController::Type::VPAD) @@ -144,7 +155,7 @@ class EmulationState void setEmulatedControllerType(size_t index, EmulatedController::Type type) { - auto &androidEmulatedController = AndroidEmulatedController::getAndroidEmulatedController(index); + auto& androidEmulatedController = AndroidEmulatedController::getAndroidEmulatedController(index); if (EmulatedController::Type::VPAD <= type && type < EmulatedController::Type::MAX) androidEmulatedController.setType(type); else @@ -166,7 +177,7 @@ class EmulationState void setDPI(float dpi) { - auto &windowInfo = GuiSystem::getWindowInfo(); + auto& windowInfo = GuiSystem::getWindowInfo(); windowInfo.dpi_scale = windowInfo.pad_dpi_scale = dpi; } @@ -180,7 +191,7 @@ class EmulationState return AndroidEmulatedController::getAndroidEmulatedController(index).getMappings(); } - void setControllerMapping(const std::string &deviceDescriptor, const std::string &deviceName, size_t index, uint64 mappingId, uint64 buttonId) + void setControllerMapping(const std::string& deviceDescriptor, const std::string& deviceName, size_t index, uint64 mappingId, uint64 buttonId) { auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android); auto controller = ControllerFactory::CreateController(InputAPI::Android, deviceDescriptor, deviceName); @@ -188,7 +199,7 @@ class EmulationState } void initializeAudioDevices() { - auto &config = g_config.data(); + auto& config = g_config.data(); if (!config.tv_device.empty()) AndroidAudio::createAudioDevice(IAudioAPI::AudioAPI::Cubeb, config.tv_channels, config.tv_volume, true); @@ -196,22 +207,22 @@ class EmulationState AndroidAudio::createAudioDevice(IAudioAPI::AudioAPI::Cubeb, config.pad_channels, config.pad_volume, false); } - void setOnGameTitleLoaded(const std::shared_ptr &onGameTitleLoaded) + void setOnGameTitleLoaded(const std::shared_ptr& onGameTitleLoaded) { m_gameTitleLoader.setOnTitleLoaded(onGameTitleLoaded); } - const Image &getGameIcon(TitleId titleId) + const Image& getGameIcon(TitleId titleId) { return m_gameIconLoader.getGameIcon(titleId); } - void setOnGameIconLoaded(const std::shared_ptr &onGameIconLoaded) + void setOnGameIconLoaded(const std::shared_ptr& onGameIconLoaded) { m_gameIconLoader.setOnIconLoaded(onGameIconLoaded); } - void addGamePath(const fs::path &gamePath) + void addGamePath(const fs::path& gamePath) { m_gameTitleLoader.addGamePath(gamePath); } @@ -231,4 +242,29 @@ class EmulationState initializeAudioDevices(); CafeSystemUtils::startGame(titleId); } + + void refreshGraphicPacks() + { + if (!CafeSystem::IsTitleRunning()) + { + GraphicPack2::ClearGraphicPacks(); + GraphicPack2::LoadAll(); + fillGraphicPacks(); + } + } + + const std::unordered_map& getGraphicPacks() const + { + return m_graphicPacks; + } + + void setEnabledStateForGraphicPack(int64_t id, bool state) + { + m_graphicPacks.at(id)->SetEnabled(state); + } + + GraphicPackPtr getGraphicPack(int64_t id) const + { + return m_graphicPacks.at(id); + } }; \ No newline at end of file diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index 02630cbf..71697f49 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -1,3 +1,4 @@ +#include "Cafe/GraphicPack/GraphicPack2.h" #include "AndroidGameIconLoadedCallback.h" #include "AndroidGameTitleLoadedCallback.h" #include "EmulationState.h" @@ -292,3 +293,111 @@ Java_info_cemu_Cemu_NativeLibrary_setAudioDeviceVolume([[maybe_unused]] JNIEnv * deviceVolume = volume; g_config.Save(); } + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_refreshGraphicPacks([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + s_emulationState.refreshGraphicPacks(); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_info_cemu_Cemu_NativeLibrary_getGraphicPackIdsAndVirtualPaths(JNIEnv* env, [[maybe_unused]] jclass clazz) +{ + auto graphicPackInfoClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackIdAndVirtualPath"); + auto graphicPackInfoCtorId = env->GetMethodID(graphicPackInfoClass, "", "(JLjava/lang/String;)V"); + + auto graphicPacks = s_emulationState.getGraphicPacks(); + std::vector graphicPackInfoJObjects; + for (auto&& graphicPack : graphicPacks) + { + jstring virtualPath = env->NewStringUTF(graphicPack.second->GetVirtualPath().c_str()); + jlong id = graphicPack.first; + jobject jGraphicPack = env->NewObject(graphicPackInfoClass, graphicPackInfoCtorId, id, virtualPath); + graphicPackInfoJObjects.push_back(jGraphicPack); + } + return JNIUtils::createArrayList(env, graphicPackInfoJObjects); +} + +jobject getGraphicPresets(JNIEnv* env, GraphicPackPtr graphicPack, int64_t id) +{ + auto graphicPackPresetClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackPreset"); + auto graphicPackPresetCtorId = env->GetMethodID(graphicPackPresetClass, "", "(JLjava/lang/String;Ljava/util/ArrayList;Ljava/lang/String;)V"); + + std::vector order; + auto presets = graphicPack->GetCategorizedPresets(order); + + std::vector presetsJobjects; + for (const auto& category : order) + { + const auto& entry = presets[category]; + // test if any preset is visible and update its status + if (std::none_of(entry.cbegin(), entry.cend(), [graphicPack](const auto& p) { return p->visible; })) + { + continue; + } + + jstring categoryJStr = category.empty() ? nullptr : env->NewStringUTF(category.c_str()); + std::vector presetSelections; + std::optional activePreset; + for (auto& pentry : entry) + { + if (!pentry->visible) + continue; + + presetSelections.push_back(pentry->name); + + if (pentry->active) + activePreset = pentry->name; + } + + jstring activePresetJstr = nullptr; + if (activePreset) + activePresetJstr = env->NewStringUTF(activePreset->c_str()); + else if (!presetSelections.empty()) + activePresetJstr = env->NewStringUTF(presetSelections.front().c_str()); + auto presetJObject = env->NewObject(graphicPackPresetClass, + graphicPackPresetCtorId, + id, + categoryJStr, + JNIUtils::createJavaStringArrayList(env, presetSelections), + activePresetJstr); + presetsJobjects.push_back(presetJObject); + } + return JNIUtils::createArrayList(env, presetsJobjects); +} +extern "C" JNIEXPORT jobject JNICALL +Java_info_cemu_Cemu_NativeLibrary_getGraphicPack(JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id) +{ + auto graphicPackClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPack"); + auto graphicPackCtorId = env->GetMethodID(graphicPackClass, "", "(JZLjava/lang/String;Ljava/lang/String;Ljava/util/ArrayList;)V"); + auto graphicPack = s_emulationState.getGraphicPack(id); + + jstring graphicPackName = env->NewStringUTF(graphicPack->GetName().c_str()); + jstring graphicPackDescription = env->NewStringUTF(graphicPack->GetDescription().c_str()); + return env->NewObject(graphicPackClass, + graphicPackCtorId, + id, + graphicPack->IsEnabled(), + graphicPackName, + graphicPackDescription, + getGraphicPresets(env, graphicPack, id)); +} + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setGraphicPackActive([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id, jboolean active) +{ + s_emulationState.setEnabledStateForGraphicPack(id, active); +} + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setGraphicPackActivePreset([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id, jstring category, jstring preset) +{ + std::string presetCategory = category == nullptr ? "" : JNIUtils::JStringToString(env, category); + s_emulationState.setGraphicPackActivePreset(id, presetCategory, JNIUtils::JStringToString(env, preset)); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_info_cemu_Cemu_NativeLibrary_getGraphicPackPresets(JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id) +{ + return getGraphicPresets(env, s_emulationState.getGraphicPack(id), id); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/BaseSelectionAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/BaseSelectionAdapter.java new file mode 100644 index 00000000..cb51c034 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/BaseSelectionAdapter.java @@ -0,0 +1,48 @@ +package info.cemu.Cemu; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import com.google.android.material.radiobutton.MaterialRadioButton; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.IntStream; + +public abstract class BaseSelectionAdapter extends BaseAdapter { + + protected int selectedPosition = 0; + + public BaseSelectionAdapter() { + super(); + } + + public void setSelectedValue(T selectedValue) { + selectedPosition = getPositionOf(selectedValue); + } + + public abstract int getPositionOf(T value); + + abstract protected void setRadioButtonText(MaterialRadioButton radioButton, int position); + + public abstract T getItem(int position); + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View view, ViewGroup viewGroup) { + if (view == null) + view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_single_selection_item, viewGroup, false); + MaterialRadioButton radioButton = view.findViewById(R.id.single_selection_item_radio_button); + radioButton.setEnabled(isEnabled(position)); + setRadioButtonText(radioButton, position); + radioButton.setChecked(position == selectedPosition); + return view; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/BaseSingleSelectionRecyclerViewItem.java b/src/android/app/src/main/java/info/cemu/Cemu/BaseSingleSelectionRecyclerViewItem.java new file mode 100644 index 00000000..94b7c98e --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/BaseSingleSelectionRecyclerViewItem.java @@ -0,0 +1,71 @@ +package info.cemu.Cemu; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public abstract class BaseSingleSelectionRecyclerViewItem implements RecyclerViewItem { + private static class SingleSelectionViewHolder extends RecyclerView.ViewHolder { + TextView label; + TextView description; + + public SingleSelectionViewHolder(View itemView) { + super(itemView); + label = itemView.findViewById(R.id.selection_label); + description = itemView.findViewById(R.id.selection_description); + } + } + + public interface OnItemSelectedListener { + void onItemSelected(T selectedValue, BaseSingleSelectionRecyclerViewItem selectionRecyclerViewItem); + } + + private final SelectionAdapter selectionAdapter; + private final String label; + private String description; + private final OnItemSelectedListener onItemSelectedListener; + private SingleSelectionViewHolder singleSelectionViewHolder; + private RecyclerView.Adapter recyclerViewAdapter; + + public BaseSingleSelectionRecyclerViewItem(String label, String description, SelectionAdapter selectionAdapter, OnItemSelectedListener onItemSelectedListener) { + this.label = label; + this.description = description; + this.selectionAdapter = selectionAdapter; + this.onItemSelectedListener = onItemSelectedListener; + } + + public void setDescription(String description) { + this.description = description; + recyclerViewAdapter.notifyItemChanged(singleSelectionViewHolder.getAdapterPosition()); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new SingleSelectionViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_single_selection, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter recyclerViewAdapter) { + this.recyclerViewAdapter = recyclerViewAdapter; + singleSelectionViewHolder = (SingleSelectionViewHolder) viewHolder; + singleSelectionViewHolder.label.setText(label); + singleSelectionViewHolder.description.setText(description); + singleSelectionViewHolder.itemView.setOnClickListener(view -> { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(viewHolder.itemView.getContext()); + builder.setTitle(label).setAdapter(selectionAdapter, (dialogInterface, position) -> { + if (!selectionAdapter.isEnabled(position)) + return; + T selectedValue = selectionAdapter.getItem(position); + selectionAdapter.setSelectedValue(selectedValue); + if (onItemSelectedListener != null) + onItemSelectedListener.onItemSelected(selectedValue, BaseSingleSelectionRecyclerViewItem.this); + + }).setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()).show(); + }); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/FileUtil.java b/src/android/app/src/main/java/info/cemu/Cemu/FileUtil.java index 575a138c..50af11b5 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/FileUtil.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/FileUtil.java @@ -1,17 +1,15 @@ package info.cemu.Cemu; -import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.util.Log; -import androidx.documentfile.provider.DocumentFile; - -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Objects; public class FileUtil { private static final String PATH_SEPARATOR_ENCODED = "%2F"; @@ -88,4 +86,29 @@ public class FileUtil { return false; } } + + public static void delete(Path fileToDelete) { + try { + if (!Files.exists(fileToDelete)) { + return; + } + if (Files.isRegularFile(fileToDelete)) { + Files.delete(fileToDelete); + return; + } + var files = Files.list(fileToDelete); + files.forEach(path -> { + if (Files.isDirectory(path)) { + delete(path); + } + try { + Files.delete(path); + } catch (IOException ignored) { + } + }); + files.close(); + Files.delete(fileToDelete); + } catch (IOException ignored) { + } + } } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GenericRecyclerViewAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/GenericRecyclerViewAdapter.java index 5f51331c..3ec8918b 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/GenericRecyclerViewAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/GenericRecyclerViewAdapter.java @@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; public class GenericRecyclerViewAdapter extends RecyclerView.Adapter { @@ -17,6 +18,13 @@ public class GenericRecyclerViewAdapter extends RecyclerView.Adapter adapter) { + HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder; + headerViewHolder.itemView.setOnClickListener(v -> onClickCallback.onClick()); + headerViewHolder.title.setText(text); + if (graphicPackItemType == GraphicPackItemType.SECTION) + headerViewHolder.icon.setImageResource(R.drawable.ic_lists); + else if (graphicPackItemType == GraphicPackItemType.GRAPHIC_PACK) + headerViewHolder.icon.setImageResource(R.drawable.ic_package_2); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GraphicPackRecyclerViewItem.java b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPackRecyclerViewItem.java new file mode 100644 index 00000000..1a395b8e --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPackRecyclerViewItem.java @@ -0,0 +1,52 @@ +package info.cemu.Cemu; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.checkbox.MaterialCheckBox; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class GraphicPackRecyclerViewItem implements RecyclerViewItem { + private static class GraphicPackViewHolder extends RecyclerView.ViewHolder { + TextView name; + MaterialCheckBox enableCheckBox; + TextView description; + + public GraphicPackViewHolder(View itemView) { + super(itemView); + name = itemView.findViewById(R.id.graphic_pack_name); + enableCheckBox = itemView.findViewById(R.id.graphic_pack_enable_checkbox); + description = itemView.findViewById(R.id.graphic_pack_description); + } + } + + + private final NativeLibrary.GraphicPack graphicPack; + + public GraphicPackRecyclerViewItem(NativeLibrary.GraphicPack graphicPack) { + this.graphicPack = graphicPack; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new GraphicPackViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_graphic_pack, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter recyclerViewAdapter) { + var graphicPackViewHolder = (GraphicPackViewHolder) viewHolder; + graphicPackViewHolder.name.setText(graphicPack.name); + if (graphicPack.description != null) { + graphicPackViewHolder.description.setText(graphicPack.description); + } else { + graphicPackViewHolder.description.setText(R.string.graphic_pack_no_description); + } + graphicPackViewHolder.enableCheckBox.setChecked(graphicPack.isActive()); + graphicPackViewHolder.enableCheckBox.addOnCheckedStateChangedListener((materialCheckBox, i) -> graphicPack.setActive(materialCheckBox.isChecked())); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksFragment.java new file mode 100644 index 00000000..8efc2c71 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksFragment.java @@ -0,0 +1,109 @@ +package info.cemu.Cemu; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +import info.cemu.Cemu.databinding.FragmentGraphicPacksBinding; + +public class GraphicPacksFragment extends Fragment { + private final GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter(); + private GraphicPacksRootFragment.GraphicPackViewModel graphicPackPreviousViewModel; + private GraphicPacksRootFragment.GraphicPackViewModel graphicPackCurrentViewModel; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + NavController navController = NavHostFragment.findNavController(this); + graphicPackPreviousViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getPreviousBackStackEntry())) + .get(GraphicPacksRootFragment.GraphicPackViewModel.class); + graphicPackCurrentViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getCurrentBackStackEntry())) + .get(GraphicPacksRootFragment.GraphicPackViewModel.class); + fillData(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + FragmentGraphicPacksBinding binding = FragmentGraphicPacksBinding.inflate(inflater, container, false); + binding.graphicPacksRecyclerView.setAdapter(genericRecyclerViewAdapter); + + return binding.getRoot(); + } + + private void fillData() { + var graphicNode = graphicPackPreviousViewModel.getGraphicPackNode(); + if (graphicNode instanceof GraphicPacksRootFragment.GraphicPackSectionNode sectionNode) { + fillData(sectionNode); + } else if (graphicNode instanceof GraphicPacksRootFragment.GraphicPackDataNode dataNode) { + fillData(dataNode); + } + } + + private void fillData(GraphicPacksRootFragment.GraphicPackSectionNode graphicPackSectionNode) { + graphicPackSectionNode.children.forEach(node -> { + var graphicPackItemType = node instanceof GraphicPacksRootFragment.GraphicPackSectionNode + ? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION + : GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK; + GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name, + graphicPackItemType, + () -> { + graphicPackCurrentViewModel.setGraphicPackNode(node); + Bundle bundle = new Bundle(); + bundle.putString("title", node.name); + NavHostFragment.findNavController(GraphicPacksFragment.this) + .navigate(R.id.action_graphicPacksFragment_self, bundle); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem); + }); + } + + private void fillData(GraphicPacksRootFragment.GraphicPackDataNode graphicPackDataNode) { + genericRecyclerViewAdapter.clearRecyclerViewItems(); + var graphicPack = NativeLibrary.getGraphicPack(graphicPackDataNode.getId()); + genericRecyclerViewAdapter.addRecyclerViewItem(new GraphicPackRecyclerViewItem(graphicPack)); + fillPresets(graphicPack); + } + + private void fillPresets(NativeLibrary.GraphicPack graphicPack) { + var recyclerViewItems = new ArrayList>(); + graphicPack.presets.forEach(graphicPackPreset -> { + String category = graphicPackPreset.getCategory(); + if (category == null) category = getString(R.string.active_preset_category); + var recyclerViewItem = new SingleSelectionRecyclerViewItem<>( + category, + graphicPackPreset.getActivePreset(), + new StringSelectionAdapter( + graphicPackPreset.getPresets(), + graphicPackPreset.getActivePreset() + ), + (selectedValue, selectionRecyclerViewItem) -> { + graphicPackPreset.setActivePreset(selectedValue); + selectionRecyclerViewItem.setDescription(selectedValue); + var oldPresets = graphicPack.presets; + graphicPack.reloadPresets(); + if (!new HashSet<>(oldPresets).containsAll(graphicPack.presets)) { + recyclerViewItems.forEach(genericRecyclerViewAdapter::removeRecyclerViewItem); + fillPresets(graphicPack); + } + } + ); + recyclerViewItems.add(recyclerViewItem); + genericRecyclerViewAdapter.addRecyclerViewItem(recyclerViewItem); + }); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRecyclerViewAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRecyclerViewAdapter.java new file mode 100644 index 00000000..bdce8cf4 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRecyclerViewAdapter.java @@ -0,0 +1,135 @@ +package info.cemu.Cemu; + +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter { + private static class GraphicPackSearchItemRecyclerViewItem implements RecyclerViewItem { + + private static class HeaderViewHolder extends RecyclerView.ViewHolder { + TextView name; + TextView path; + + public HeaderViewHolder(View itemView) { + super(itemView); + name = itemView.findViewById(R.id.graphic_pack_search_name); + path = itemView.findViewById(R.id.graphic_pack_search_path); + } + } + + public interface OnClickCallback { + void onClick(); + } + + private final String name; + private final String path; + private final OnClickCallback onClickCallback; + + private GraphicPackSearchItemRecyclerViewItem(String name, String path, OnClickCallback onClickCallback) { + this.name = name; + this.path = path; + this.onClickCallback = onClickCallback; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) { + return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_graphic_pack_search_item, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter adapter) { + HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder; + headerViewHolder.itemView.setOnClickListener(v -> onClickCallback.onClick()); + headerViewHolder.name.setText(name); + headerViewHolder.path.setText(path); + } + } + + public interface OnItemClickCallback { + void onClick(GraphicPacksRootFragment.GraphicPackDataNode graphicPackDataNode); + } + + private final OnItemClickCallback onItemClickCallback; + + public GraphicPacksRecyclerViewAdapter(OnItemClickCallback onItemClickCallback) { + this.onItemClickCallback = onItemClickCallback; + } + + private List recyclerViewItems = new ArrayList<>(); + private List filteredViewItems = new ArrayList<>(); + + public void setItems(List graphicPackDataNodes) { + clearItems(); + recyclerViewItems = graphicPackDataNodes.stream() + .sorted(Comparator.comparing(GraphicPacksRootFragment.GraphicPackDataNode::getPath)) + .map(graphicPackDataNode -> + new GraphicPackSearchItemRecyclerViewItem( + graphicPackDataNode.getName(), + graphicPackDataNode.getPath(), + () -> GraphicPacksRecyclerViewAdapter.this.onItemClickCallback.onClick(graphicPackDataNode) + )).collect(Collectors.toList()); + filteredViewItems = recyclerViewItems; + notifyItemRangeInserted(0, filteredViewItems.size()); + } + + public void filter(String text) { + if (text.isBlank()) { + filteredViewItems = recyclerViewItems; + notifyDataSetChanged(); + return; + } + var stringBuilder = new StringBuilder(); + String pattern = Arrays.stream(text.split(" ")) + .map(s -> "(?=.*" + Pattern.quote(s) + ")") + .collect(() -> stringBuilder, StringBuilder::append, StringBuilder::append) + .append(".*") + .toString(); + var regex = Pattern.compile(pattern, CASE_INSENSITIVE); + filteredViewItems = recyclerViewItems.stream() + .filter(g -> regex.matcher(g.path).matches()) + .collect(Collectors.toList()); + notifyDataSetChanged(); + } + + public void clearItems() { + if (filteredViewItems.isEmpty()) return; + int itemsCount = filteredViewItems.size(); + filteredViewItems.clear(); + notifyItemRangeRemoved(0, itemsCount); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) { + return filteredViewItems.get(position).onCreateViewHolder(parent); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + filteredViewItems.get(position).onBindViewHolder(holder, this); + } + + @Override + public int getItemCount() { + return filteredViewItems.size(); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRootFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRootFragment.java new file mode 100644 index 00000000..73c1f1f3 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GraphicPacksRootFragment.java @@ -0,0 +1,354 @@ +package info.cemu.Cemu; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SearchView; +import androidx.core.view.MenuProvider; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.fragment.NavHostFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import info.cemu.Cemu.databinding.FragmentGraphicPacksRootBinding; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class GraphicPacksRootFragment extends Fragment { + public abstract static class GraphicPackNode { + protected String name; + + public String getName() { + return name; + } + } + + public static class GraphicPackDataNode extends GraphicPackNode { + private final long id; + private final String path; + + public GraphicPackDataNode(long id, String name, String path) { + this.id = id; + this.name = name; + this.path = path; + } + + public long getId() { + return id; + } + + public String getPath() { + return path; + } + } + + public static class GraphicPackSectionNode extends GraphicPackNode { + ArrayList children = new ArrayList<>(); + + public GraphicPackSectionNode(String name) { + this.name = name; + } + + public GraphicPackSectionNode() { + } + + void clear() { + children.clear(); + } + + private void addGraphicPackDataByTokens(List tokens, GraphicPackDataNode dataNode) { + var node = this; + for (var token : tokens) { + node = getOrAddSectionByToken(token, node.children); + } + node.children.add(dataNode); + } + + private GraphicPackSectionNode getOrAddSectionByToken(String token, ArrayList graphicPackNodes) { + var sectionNodeOptional = graphicPackNodes.stream() + .filter(g -> (g instanceof GraphicPackSectionNode) && g.name.equals(token)) + .findFirst(); + if (!sectionNodeOptional.isPresent()) { + var sectionNode = new GraphicPackSectionNode(token); + graphicPackNodes.add(sectionNode); + return sectionNode; + } + return (GraphicPackSectionNode) sectionNodeOptional.get(); + } + + private void sort() { + children.forEach(node -> { + if (node instanceof GraphicPackSectionNode sectionNode) sectionNode.sort(); + }); + children.sort(Comparator.comparing(o -> o.name)); + } + } + + public static class GraphicPackViewModel extends ViewModel { + private GraphicPackNode graphicPackNode; + + public GraphicPackViewModel() { + } + + public GraphicPackNode getGraphicPackNode() { + return graphicPackNode; + } + + public void setGraphicPackNode(GraphicPackNode graphicPackNode) { + this.graphicPackNode = graphicPackNode; + } + } + + private final GraphicPackSectionNode graphicPackSectionRootNode = new GraphicPackSectionNode(); + private final GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter(); + private GraphicPackViewModel graphicPackViewModel; + private final GraphicPacksRecyclerViewAdapter graphicPacksRecyclerViewAdapter = new GraphicPacksRecyclerViewAdapter(graphicPackDataNode -> { + graphicPackViewModel.setGraphicPackNode(graphicPackDataNode); + Bundle bundle = new Bundle(); + bundle.putString("title", graphicPackDataNode.name); + NavHostFragment.findNavController(GraphicPacksRootFragment.this) + .navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle); + }); + private final OkHttpClient client = new OkHttpClient(); + private static final String RELEASES_API_URL = "https://api.github.com/repos/cemu-project/cemu_graphic_packs/releases/latest"; + private boolean updateInProgress; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + fillGraphicPacks(); + var navController = NavHostFragment.findNavController(GraphicPacksRootFragment.this); + graphicPackViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getCurrentBackStackEntry())) + .get(GraphicPackViewModel.class); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + FragmentGraphicPacksRootBinding binding = FragmentGraphicPacksRootBinding.inflate(inflater, container, false); + binding.graphicPacksRootRecyclerView.setAdapter(this.genericRecyclerViewAdapter); + requireActivity().addMenuProvider(new MenuProvider() { + @Override + public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { + menuInflater.inflate(R.menu.menu_graphic_packs, menu); + var searchMenuItem = menu.findItem(R.id.action_graphic_packs_search); + SearchView searchView = (SearchView) Objects.requireNonNull(searchMenuItem.getActionView()); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + graphicPacksRecyclerViewAdapter.filter(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + graphicPacksRecyclerViewAdapter.filter(newText); + return true; + } + }); + searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(@NonNull MenuItem item) { + binding.graphicPacksRootRecyclerView.setAdapter(graphicPacksRecyclerViewAdapter); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { + binding.graphicPacksRootRecyclerView.setAdapter(genericRecyclerViewAdapter); + return true; + } + }); + } + + @Override + public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { + if (menuItem.getItemId() == R.id.action_graphic_packs_download && !updateInProgress) { + checkForUpdates(); + return true; + } + if (menuItem.getItemId() == R.id.action_graphic_packs_search) { + return true; + } + return false; + } + }, getViewLifecycleOwner(), Lifecycle.State.RESUMED); + return binding.getRoot(); + } + + private void fillGraphicPacks() { + var nativeGraphicPacks = NativeLibrary.getGraphicPackIdsAndVirtualPaths(); + graphicPackSectionRootNode.clear(); + List graphicPackDataNodes = new ArrayList<>(); + nativeGraphicPacks.forEach(graphicPack -> { + var tokens = Arrays.asList(graphicPack.virtualPath().split("/")); + var dataNode = new GraphicPackDataNode(graphicPack.id(), tokens.get(tokens.size() - 1), graphicPack.virtualPath()); + graphicPackDataNodes.add(dataNode); + graphicPackSectionRootNode.addGraphicPackDataByTokens(tokens.subList(0, tokens.size() - 1), dataNode); + }); + graphicPacksRecyclerViewAdapter.setItems(graphicPackDataNodes); + + genericRecyclerViewAdapter.clearRecyclerViewItems(); + graphicPackSectionRootNode.sort(); + graphicPackSectionRootNode.children.forEach(node -> { + var graphicPackItemType = node instanceof GraphicPackSectionNode + ? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION + : GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK; + GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name, + graphicPackItemType, + () -> { + graphicPackViewModel.setGraphicPackNode(node); + Bundle bundle = new Bundle(); + bundle.putString("title", node.name); + NavHostFragment.findNavController(GraphicPacksRootFragment.this) + .navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem); + }); + } + + private void checkForUpdates() { + AlertDialog dialog = createGraphicPacksDownloadDialog(); + requireActivity().runOnUiThread(() -> Objects.requireNonNull((TextView) dialog.findViewById(R.id.textViewGraphicPacksDownload)).setText(R.string.checking_version_download_text)); + Request request = new Request.Builder() + .url(RELEASES_API_URL) + .build(); + requireActivity().runOnUiThread(() -> Objects.requireNonNull((TextView) dialog.findViewById(R.id.textViewGraphicPacksDownload)).setText(R.string.graphic_packs_download_text)); + client.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + var json = new JSONObject(Objects.requireNonNull(responseBody).string()); + var version = json.getString("name"); + var currentVersion = getCurrentVersion(); + if (currentVersion.isPresent() && currentVersion.get().equals(version)) { + onGraphicPacksLatestVersion(dialog); + return; + } + var downloadUrl = json.getJSONArray("assets") + .getJSONObject(0) + .getString("browser_download_url"); + downloadNewUpdate(downloadUrl, version, dialog); + } catch (JSONException e) { + requireActivity().runOnUiThread(dialog::dismiss); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + if (!call.isCanceled()) onDownloadError(dialog); + } + }); + } + + private Optional getCurrentVersion() { + Path graphicPacksVersionFile = Paths.get(Objects.requireNonNull(requireActivity().getExternalFilesDir(null)).getAbsolutePath(), + "graphicPacks", + "downloadedGraphicPacks", + "version.txt"); + try { + return Optional.of(new String(Files.readAllBytes(graphicPacksVersionFile))); + } catch (IOException ignored) { + return Optional.empty(); + } + } + + private void downloadNewUpdate(String graphicPacksZipDownloadUrl, String version, @NonNull AlertDialog dialog) { + Request request = new Request.Builder() + .url(graphicPacksZipDownloadUrl) + .build(); + + Path graphicPacksDirPath = Paths.get(Objects.requireNonNull(requireActivity().getExternalFilesDir(null)).getAbsolutePath(), "graphicPacks"); + client.newCall(request).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + Path graphicPacksTempDirPath = graphicPacksDirPath.resolve("downloadedGraphicPacksTemp"); + FileUtil.delete(graphicPacksTempDirPath); + ZipUtil.unzip(Objects.requireNonNull(responseBody).byteStream(), graphicPacksTempDirPath.toString()); + Files.write(graphicPacksTempDirPath.resolve("version.txt"), version.getBytes(StandardCharsets.UTF_8)); + Path downloadedGraphicPacksDirPath = graphicPacksDirPath.resolve("downloadedGraphicPacks"); + FileUtil.delete(downloadedGraphicPacksDirPath); + Files.move(graphicPacksTempDirPath, downloadedGraphicPacksDirPath); + NativeLibrary.refreshGraphicPacks(); + requireActivity().runOnUiThread(() -> fillGraphicPacks()); + onDownloadFinish(dialog); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + if (!call.isCanceled()) onDownloadError(dialog); + } + }); + } + + private void onDownloadError(AlertDialog dialog) { + requireActivity().runOnUiThread(() -> { + dialog.dismiss(); + Snackbar.make(requireView(), R.string.download_graphic_packs_error_text, Snackbar.LENGTH_SHORT).show(); + }); + } + + private void onDownloadFinish(AlertDialog dialog) { + requireActivity().runOnUiThread(() -> { + dialog.dismiss(); + Snackbar.make(requireView(), R.string.download_graphic_packs_finish_text, Snackbar.LENGTH_SHORT).show(); + }); + } + + private void onGraphicPacksLatestVersion(AlertDialog dialog) { + requireActivity().runOnUiThread(() -> { + dialog.dismiss(); + Snackbar.make(requireView(), R.string.graphic_packs_no_updates_text, Snackbar.LENGTH_SHORT).show(); + }); + } + + private AlertDialog createGraphicPacksDownloadDialog() { + updateInProgress = true; + return new MaterialAlertDialogBuilder(requireContext()).setView(R.layout.layout_graphic_packs_download) + .setTitle(R.string.graphic_packs_download_dialog_title) + .setCancelable(false) + .setOnDismissListener(d -> updateInProgress = false) + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> client.dispatcher().cancelAll()) + .show(); + } + + @Override + public void onDestroy() { + client.dispatcher().cancelAll(); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index f8174763..5475474f 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -1,11 +1,16 @@ package info.cemu.Cemu; -import android.graphics.Bitmap; -import android.view.KeyEvent; import android.view.Surface; +import androidx.annotation.Nullable; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; public class NativeLibrary { static { @@ -284,4 +289,94 @@ public class NativeLibrary { public static native int getAudioDeviceVolume(boolean tv); + public record GraphicPackIdAndVirtualPath(long id, String virtualPath) { + } + + public static native ArrayList getGraphicPackIdsAndVirtualPaths(); + + public static class GraphicPackPreset { + private final long graphicPackId; + private final String category; + private final ArrayList presets; + private String activePreset; + + @Override + public int hashCode() { + return Objects.hash(graphicPackId, category, presets, activePreset); + } + + @Override + public boolean equals(Object object) { + if (object == null) return false; + if (object == this) return true; + if (object instanceof GraphicPackPreset preset) + return this.hashCode() == preset.hashCode(); + return false; + } + + public GraphicPackPreset(long graphicPackId, String category, ArrayList presets, String activePreset) { + this.graphicPackId = graphicPackId; + this.category = category; + this.presets = presets; + this.activePreset = activePreset; + } + + public String getActivePreset() { + return activePreset; + } + + public void setActivePreset(String activePreset) { + if (presets.stream().noneMatch(s -> s.equals(activePreset))) + throw new RuntimeException("Trying to set an invalid preset: " + activePreset); + setGraphicPackActivePreset(graphicPackId, category, activePreset); + this.activePreset = activePreset; + } + + public String getCategory() { + return category; + } + + public ArrayList getPresets() { + return presets; + } + } + + public static class GraphicPack { + public final long id; + protected boolean active; + public final String name; + public final String description; + public List presets; + + public GraphicPack(long id, boolean active, String name, String description, ArrayList presets) { + this.id = id; + this.active = active; + this.name = name; + this.description = description; + this.presets = presets; + } + + public boolean isActive() { + return active; + } + + public void reloadPresets() { + presets = NativeLibrary.getGraphicPackPresets(id); + } + + public void setActive(boolean active) { + this.active = active; + setGraphicPackActive(id, active); + } + } + + public static native void refreshGraphicPacks(); + + public static native GraphicPack getGraphicPack(long id); + + public static native void setGraphicPackActive(long id, boolean active); + + public static native void setGraphicPackActivePreset(long id, String category, String preset); + + public static native ArrayList getGraphicPackPresets(long id); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/OptionsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/OptionsFragment.java index 1f83f181..83bb18ba 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/OptionsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/OptionsFragment.java @@ -51,6 +51,7 @@ public class OptionsFragment extends Fragment { genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_inputSettingsFragment))); genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphics_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_graphicSettingsFragment))); genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.audio_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_audioSettingsFragment))); + genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphic_packs), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_graphicPacksRootFragment))); binding.optionsRecyclerView.setAdapter(genericRecyclerViewAdapter); return binding.getRoot(); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java index 20cb21a0..f98f4eaf 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.OptionalInt; import java.util.stream.IntStream; -public class SelectionAdapter extends BaseAdapter { +public class SelectionAdapter extends BaseSelectionAdapter { public static class ChoiceItem { int choiceTextResourceId; T choiceValue; @@ -24,7 +24,6 @@ public class SelectionAdapter extends BaseAdapter { } protected List> choiceItems = new ArrayList<>(); - protected int selectedPosition = 0; public SelectionAdapter() { super(); @@ -36,10 +35,12 @@ public class SelectionAdapter extends BaseAdapter { setSelectedValue(selectedValue); } + @Override public void setSelectedValue(T selectedValue) { selectedPosition = getPositionOf(selectedValue); } + @Override public int getPositionOf(T value) { OptionalInt optionalInt = IntStream.range(0, choiceItems.size()).filter(position -> choiceItems.get(position).choiceValue.equals(value)).findFirst(); if (!optionalInt.isPresent()) @@ -47,6 +48,11 @@ public class SelectionAdapter extends BaseAdapter { return optionalInt.getAsInt(); } + @Override + protected void setRadioButtonText(MaterialRadioButton radioButton, int position) { + radioButton.setText(choiceItems.get(position).choiceTextResourceId); + } + @Override public int getCount() { return choiceItems.size(); @@ -56,20 +62,4 @@ public class SelectionAdapter extends BaseAdapter { public T getItem(int position) { return choiceItems.get(position).choiceValue; } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View view, ViewGroup viewGroup) { - if (view == null) - view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_single_selection_item, viewGroup, false); - MaterialRadioButton radioButton = view.findViewById(R.id.single_selection_item_radio_button); - radioButton.setEnabled(isEnabled(position)); - radioButton.setText(choiceItems.get(position).choiceTextResourceId); - radioButton.setChecked(position == selectedPosition); - return view; - } } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java index c28c595f..4a067afb 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java @@ -4,31 +4,32 @@ import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.navigation.NavController; +import androidx.navigation.NavGraph; import androidx.navigation.Navigation; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; +import java.util.Objects; + import info.cemu.Cemu.databinding.ActivitySettingsBinding; public class SettingsActivity extends AppCompatActivity { private AppBarConfiguration appBarConfiguration; - private ActivitySettingsBinding binding; + private NavController navController; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); + info.cemu.Cemu.databinding.ActivitySettingsBinding binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbarSettings); - NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings); - appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); + navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings); + appBarConfiguration = new AppBarConfiguration.Builder().build(); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); } @Override public boolean onSupportNavigateUp() { - NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings); return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp(); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java b/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java index 32ca3b17..b4aa8fa6 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java @@ -25,14 +25,14 @@ public class SingleSelectionRecyclerViewItem implements RecyclerViewItem { void onItemSelected(T selectedValue, SingleSelectionRecyclerViewItem selectionRecyclerViewItem); } - private final SelectionAdapter selectionAdapter; + private final BaseSelectionAdapter selectionAdapter; private final String label; private String description; private final OnItemSelectedListener onItemSelectedListener; private SingleSelectionViewHolder singleSelectionViewHolder; private RecyclerView.Adapter recyclerViewAdapter; - public SingleSelectionRecyclerViewItem(String label, String description, SelectionAdapter selectionAdapter, OnItemSelectedListener onItemSelectedListener) { + public SingleSelectionRecyclerViewItem(String label, String description, BaseSelectionAdapter selectionAdapter, OnItemSelectedListener onItemSelectedListener) { this.label = label; this.description = description; this.selectionAdapter = selectionAdapter; diff --git a/src/android/app/src/main/java/info/cemu/Cemu/StringSelectionAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/StringSelectionAdapter.java new file mode 100644 index 00000000..8ae51e4c --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/StringSelectionAdapter.java @@ -0,0 +1,46 @@ +package info.cemu.Cemu; + +import com.google.android.material.radiobutton.MaterialRadioButton; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.IntStream; + +public class StringSelectionAdapter extends BaseSelectionAdapter { + private final List choices; + + public StringSelectionAdapter(List choices, String selectedValue) { + super(); + this.choices = choices; + setSelectedValue(selectedValue); + } + + @Override + public void setSelectedValue(String selectedValue) { + selectedPosition = getPositionOf(selectedValue); + } + + @Override + public int getPositionOf(String value) { + OptionalInt optionalInt = IntStream.range(0, choices.size()).filter(position -> choices.get(position).equals(value)).findFirst(); + if (!optionalInt.isPresent()) + throw new RuntimeException("value " + value + " was not found in the list of choices"); + return optionalInt.getAsInt(); + } + + @Override + protected void setRadioButtonText(MaterialRadioButton radioButton, int position) { + radioButton.setText(choices.get(position)); + } + + @Override + public int getCount() { + return choices.size(); + } + + @Override + public String getItem(int position) { + return choices.get(position); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ZipUtil.java b/src/android/app/src/main/java/info/cemu/Cemu/ZipUtil.java new file mode 100644 index 00000000..754ad2bf --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ZipUtil.java @@ -0,0 +1,40 @@ +package info.cemu.Cemu; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ZipUtil { + + public static void unzip(InputStream stream, String targetDir) throws IOException { + try (ZipInputStream zipInputStream = new ZipInputStream(stream)) { + ZipEntry zipEntry; + byte[] buffer = new byte[8192]; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + String filePath = Paths.get(targetDir, zipEntry.getName()).toString(); + if (zipEntry.isDirectory()) { + createDir(filePath); + } else { + try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) { + int bytesRead; + while ((bytesRead = zipInputStream.read(buffer)) > 0) { + fileOutputStream.write(buffer, 0, bytesRead); + } + } + } + zipInputStream.closeEntry(); + } + } + } + + private static void createDir(String dir) { + File f = new File(dir); + if (!f.isDirectory()) { + f.mkdirs(); + } + } +} diff --git a/src/android/app/src/main/res/drawable/ic_chevron_right.xml b/src/android/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 00000000..94f20603 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,11 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_download.xml b/src/android/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 00000000..dba46014 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_lists.xml b/src/android/app/src/main/res/drawable/ic_lists.xml new file mode 100644 index 00000000..55d16002 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_lists.xml @@ -0,0 +1,11 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_package_2.xml b/src/android/app/src/main/res/drawable/ic_package_2.xml new file mode 100644 index 00000000..a1e30294 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_package_2.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/layout/fragment_graphic_packs.xml b/src/android/app/src/main/res/layout/fragment_graphic_packs.xml new file mode 100644 index 00000000..e49c28a9 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_graphic_packs.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_graphic_packs_root.xml b/src/android/app/src/main/res/layout/fragment_graphic_packs_root.xml new file mode 100644 index 00000000..6a45db3e --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_graphic_packs_root.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack.xml b/src/android/app/src/main/res/layout/layout_graphic_pack.xml new file mode 100644 index 00000000..744a130e --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_graphic_pack.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml b/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml new file mode 100644 index 00000000..a2cdb5ac --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_graphic_pack_item.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml b/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml new file mode 100644 index 00000000..929d4519 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_graphic_pack_search_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml b/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml new file mode 100644 index 00000000..2637fae5 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_graphic_packs_download.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/menu/menu_graphic_packs.xml b/src/android/app/src/main/res/menu/menu_graphic_packs.xml new file mode 100644 index 00000000..f9c69f3b --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_graphic_packs.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/navigation/nav_settings_graph.xml b/src/android/app/src/main/res/navigation/nav_settings_graph.xml index 6565d182..6aa3bd79 100644 --- a/src/android/app/src/main/res/navigation/nav_settings_graph.xml +++ b/src/android/app/src/main/res/navigation/nav_settings_graph.xml @@ -8,20 +8,38 @@ android:id="@+id/inputSettingsFragment" android:name="info.cemu.Cemu.InputSettingsFragment" android:label="@string/input_settings" - tools:layout="@layout/fragment_input_settings" > + tools:layout="@layout/fragment_input_settings"> + android:id="@+id/graphicPacksRootFragment" + android:name="info.cemu.Cemu.GraphicPacksRootFragment" + android:label="@string/graphic_packs" + tools:layout="@layout/fragment_graphic_packs_root"> + + + + + + + tools:layout="@layout/fragment_options"> @@ -31,7 +49,14 @@ + + TV Enable audio output for the Wii U TV Gamepad - Enable audio output for for the Wii U Gamepad + Enable audio output for the Wii U Gamepad Audio settings Mono Stereo @@ -93,4 +93,23 @@ Gamepad volume Search games No games were found + Graphic packs + Download latest community graphic packs + Download graphic packs + Search + Checking version + Downloading graphic packs + Failed to download graphic packs + Downloaded latest graphic packs + No updates available + Graphic packs download + Expand graphic packs section + Graphic pack section + Active preset + Enabled + Graphic pack name + Graphic pack description + This graphic pack has no description + Graphic pack icon + Search graphic packs \ No newline at end of file