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