diff --git a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp index 36ff0331..e8bb7a90 100644 --- a/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp +++ b/src/Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.cpp @@ -49,7 +49,9 @@ const std::vector kOptionalDeviceExtensions = const std::vector kRequiredDeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, +#if !__ANDROID__ VK_KHR_SAMPLER_MIRROR_CLAMP_TO_EDGE_EXTENSION_NAME +#endif }; // Intel doesnt support VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) @@ -442,7 +444,7 @@ VulkanRenderer::VulkanRenderer() deviceFeatures.imageCubeArray = VK_TRUE; #if !BOOST_OS_MACOS deviceFeatures.geometryShader = VK_TRUE; - deviceFeatures.logicOp = VK_TRUE; +// deviceFeatures.logicOp = VK_TRUE; #endif deviceFeatures.occlusionQueryPrecise = VK_TRUE; deviceFeatures.depthClamp = VK_TRUE; diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index 3c266dee..1f5257fc 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -13,8 +13,7 @@ android { versionCode 1 versionName "1.0" ndk { -// abiFilters("x86_64", "arm64-v8a") - abiFilters("x86_64") + abiFilters("x86_64", "arm64-v8a") } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -48,7 +47,7 @@ android { '-DENABLE_DISCORD_RPC=OFF', '-DENABLE_WAYLAND=OFF' ) - abiFilters("x86_64") + abiFilters("x86_64", "arm64-v8a") } } } @@ -61,6 +60,9 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation 'androidx.navigation:navigation-fragment:2.5.3' + implementation 'androidx.navigation:navigation-ui:2.5.3' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 06454137..bb78d539 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -3,18 +3,29 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + android:exported="true" + android:theme="@style/Theme.Cemu"> diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt index accf2fd9..35bf5efa 100644 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ b/src/android/app/src/main/cpp/CMakeLists.txt @@ -2,11 +2,15 @@ add_library( CemuAndroid SHARED native-lib.cpp + JNIUtils.cpp + JNIUtils.h ) target_link_libraries( CemuAndroid PRIVATE + -landroid CemuCommon + CemuBin CemuAndroidGui ) diff --git a/src/android/app/src/main/cpp/JNIUtils.cpp b/src/android/app/src/main/cpp/JNIUtils.cpp new file mode 100644 index 00000000..073b6493 --- /dev/null +++ b/src/android/app/src/main/cpp/JNIUtils.cpp @@ -0,0 +1,69 @@ +#include "JNIUtils.h" + +namespace JNIUtils +{ +JavaVM *g_jvm = nullptr; +}; + +std::string JNIUtils::JStringToString(JNIEnv *env, jstring jstr) +{ + const char *c_str = env->GetStringUTFChars(jstr, nullptr); + std::string str(c_str); + env->ReleaseStringUTFChars(jstr, c_str); + return str; +} + +jobject JNIUtils::createJavaStringArrayList(JNIEnv *env, const std::vector &strings) +{ + jclass clsArrayList = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = env->GetMethodID(clsArrayList, "", "()V"); + jobject arrayListObject = env->NewObject(clsArrayList, arrayListConstructor); + jmethodID addMethod = env->GetMethodID(clsArrayList, "add", "(Ljava/lang/Object;)Z"); + env->DeleteLocalRef(clsArrayList); + + for (const auto &string : strings) + { + jstring element = env->NewStringUTF(string.c_str()); + env->CallBooleanMethod(arrayListObject, addMethod, element); + env->DeleteLocalRef(element); + } + return arrayListObject; +} + +JNIUtils::Scopedjobject JNIUtils::getEnumValue(JNIEnv *env, const std::string &enumClassName, const std::string &enumName) +{ + jclass enumClass = env->FindClass(enumClassName.c_str()); + jfieldID fieldID = env->GetStaticFieldID(enumClass, enumName.c_str(), ("L" + enumClassName + ";").c_str()); + jobject enumValue = env->GetStaticObjectField(enumClass, fieldID); + env->DeleteLocalRef(enumClass); + Scopedjobject enumObj = Scopedjobject(enumValue); + env->DeleteLocalRef(enumValue); + return enumObj; +} + +jobject JNIUtils::createArrayList(JNIEnv *env, const std::vector &objects) +{ + static Scopedjclass listClass = Scopedjclass("java/util/ArrayList"); + static jmethodID listConstructor = env->GetMethodID(*listClass, "", "()V"); + static jmethodID listAdd = env->GetMethodID(*listClass, "add", "(Ljava/lang/Object;)Z"); + + jobject arrayList = env->NewObject(*listClass, listConstructor); + for (auto &&obj : objects) + env->CallBooleanMethod(arrayList, listAdd, obj); + return arrayList; +} + +jobject JNIUtils::createJavaStringArrayList(JNIEnv *env, const std::vector &strings) +{ + jclass arrayListClass = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "", "()V"); + jobject arrayListObject = env->NewObject(arrayListClass, arrayListConstructor); + jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + for (const auto &string : strings) + { + jstring element = env->NewString((jchar *)string.c_str(), string.length()); + env->CallBooleanMethod(arrayListObject, addMethod, element); + env->DeleteLocalRef(element); + } + return arrayListObject; +} diff --git a/src/android/app/src/main/cpp/JNIUtils.h b/src/android/app/src/main/cpp/JNIUtils.h new file mode 100644 index 00000000..197ff571 --- /dev/null +++ b/src/android/app/src/main/cpp/JNIUtils.h @@ -0,0 +1,143 @@ +#pragma once + +#include +#include + +namespace JNIUtils +{ +extern JavaVM *g_jvm; + +std::string JStringToString(JNIEnv *env, jstring jstr); + +jobject createJavaStringArrayList(JNIEnv *env, const std::vector &stringList); + +jobject createJavaStringArrayList(JNIEnv *env, const std::vector &stringList); + +class ScopedJNIENV +{ + public: + ScopedJNIENV() + { + jint result = g_jvm->GetEnv((void **)&m_env, JNI_VERSION_1_6); + if (result == JNI_EDETACHED) + { + JavaVMAttachArgs args; + args.version = JNI_VERSION_1_6; + args.name = nullptr; + args.group = nullptr; + result = g_jvm->AttachCurrentThread(&m_env, &args); + if (result == JNI_OK) + m_threadWasAttached = true; + } + } + + JNIEnv *&operator*() { return m_env; } + + JNIEnv *operator->() { return m_env; } + + ~ScopedJNIENV() + { + if (m_threadWasAttached) + g_jvm->DetachCurrentThread(); + } + + private: + JNIEnv *m_env = nullptr; + bool m_threadWasAttached = false; +}; + +class Scopedjobject +{ + public: + Scopedjobject() = default; + + Scopedjobject(Scopedjobject &&other) + { + this->m_jobject = other.m_jobject; + other.m_jobject = nullptr; + } + void deleteRef() + { + if (m_jobject) + { + ScopedJNIENV()->DeleteGlobalRef(m_jobject); + m_jobject = nullptr; + } + } + Scopedjobject &operator=(Scopedjobject &&other) + { + if (this != &other) + { + deleteRef(); + m_jobject = other.m_jobject; + other.m_jobject = nullptr; + } + return *this; + } + jobject &operator*() { return m_jobject; } + + Scopedjobject(jobject obj) + { + if (obj) + m_jobject = ScopedJNIENV()->NewGlobalRef(obj); + } + + ~Scopedjobject() + { + deleteRef(); + } + + bool isValid() const { return m_jobject; } + + private: + jobject m_jobject = nullptr; +}; + +class Scopedjclass +{ + public: + Scopedjclass() = default; + + Scopedjclass(Scopedjclass &&other) + { + this->m_jclass = other.m_jclass; + other.m_jclass = nullptr; + } + + Scopedjclass &operator=(Scopedjclass &&other) + { + if (this != &other) + { + if (m_jclass) + ScopedJNIENV()->DeleteGlobalRef(m_jclass); + m_jclass = other.m_jclass; + other.m_jclass = nullptr; + } + return *this; + } + + Scopedjclass(const std::string &className) + { + ScopedJNIENV scopedEnv; + jobject tempObj = scopedEnv->FindClass(className.c_str()); + m_jclass = static_cast(scopedEnv->NewGlobalRef(tempObj)); + scopedEnv->DeleteLocalRef(tempObj); + } + + ~Scopedjclass() + { + if (m_jclass) + ScopedJNIENV()->DeleteGlobalRef(m_jclass); + } + + bool isValid() const { return m_jclass; } + + jclass &operator*() { return m_jclass; } + + private: + jclass m_jclass = nullptr; +}; + +Scopedjobject getEnumValue(JNIEnv *env, const std::string &enumClassName, const std::string &enumName); +jobject createArrayList(JNIEnv *env, const std::vector &objects); +} // namespace JNIUtils diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index 7e5d3f17..1a79da85 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -1,10 +1,183 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include "AndroidGui/Utils.h" +#include "config/ActiveSettings.h" +#include "config/NetworkSettings.h" +#include "input/InputManager.h" +#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h" -extern "C" JNIEXPORT jstring JNICALL -Java_com_example_myapplication_MainActivity_stringFromJNI( - JNIEnv *env, - jobject /* this */) { - std::string hello = "Hello from C++"; - return env->NewStringUTF(hello.c_str()); + +#include "JNIUtils.h" + +extern "C" +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIUtils::g_jvm = vm; + return JNI_VERSION_1_6; } + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setSurface(JNIEnv *env, jclass clazz, jobject surface, + jboolean is_main_canvas) { + WindowHandleInfo *windowHandleInfo; + if (is_main_canvas) + windowHandleInfo = &gui_getWindowInfo().canvas_main; + else + windowHandleInfo = &gui_getWindowInfo().canvas_pad; + if (windowHandleInfo->handle) { + ANativeWindow_release(static_cast(windowHandleInfo->handle)); + windowHandleInfo->handle = nullptr; + } + if (surface) + windowHandleInfo->handle = ANativeWindow_fromSurface(env, surface); + gui_getWindowInfo().window_main.handle = windowHandleInfo->handle; +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setSurfaceSize(JNIEnv *env, jclass clazz, jint width, jint height, + jboolean is_main_canvas) { + WindowHandleInfo *windowHandleInfo; + if (is_main_canvas) + windowHandleInfo = &gui_getWindowInfo().canvas_main; + else + windowHandleInfo = &gui_getWindowInfo().canvas_pad; + gui_getWindowInfo().width = width; + gui_getWindowInfo().height = height; +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_startGame(JNIEnv *env, jclass clazz, jlong title_id) { + gui_startGame(*reinterpret_cast(&title_id)); +} + +std::shared_ptr g_gameTitleLoadedCallback; + +class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback { + jmethodID m_onGameTitleLoadedMID; + JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj; +public: + AndroidGameTitleLoadedCallback(jmethodID onGameTitleLoadedMID, + jobject gameTitleLoadedCallbackObj) + : m_onGameTitleLoadedMID(onGameTitleLoadedMID), + m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {} + + void onTitleLoaded(const Game &game) override { + JNIUtils::ScopedJNIENV env; + jstring name = env->NewStringUTF(game.name.c_str()); + jlong titleId = *reinterpret_cast(&game.titleId); + env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, titleId, name); + env->DeleteLocalRef(name); + } +}; + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setGameTitleLoadedCallback(JNIEnv *env, jclass clazz, + jobject game_title_loaded_callback) { + jclass gameTitleLoadedCallbackClass = env->GetObjectClass(game_title_loaded_callback); + jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, + "onGameTitleLoaded", + "(JLjava/lang/String;)V"); + env->DeleteLocalRef(gameTitleLoadedCallbackClass); + g_gameTitleLoadedCallback = std::make_shared( + onGameTitleLoadedMID, + game_title_loaded_callback); + gui_setOnGameTitleLoaded(g_gameTitleLoadedCallback); +} + +std::shared_ptr g_gameIconLoadedCallback; + +class AndroidGameIconLoadedCallback : public GameIconLoadedCallback { + jmethodID m_onGameIconLoadedMID; + JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj; +public: + AndroidGameIconLoadedCallback(jmethodID onGameIconLoadedMID, + jobject gameTitleLoadedCallbackObj) + : m_onGameIconLoadedMID(onGameIconLoadedMID), + m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {} + + void onIconLoaded(TitleId titleId, const Image &iconData) override { + JNIUtils::ScopedJNIENV env; + jlong jTitleId = *reinterpret_cast(&titleId); + jintArray jIconData = env->NewIntArray(iconData.m_width * iconData.m_height); + env->SetIntArrayRegion(jIconData, 0, iconData.m_width * iconData.m_height, + reinterpret_cast(iconData.m_image)); + env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, jTitleId, + jIconData, iconData.m_width, iconData.m_height); + } +}; + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_setGameIconLoadedCallback(JNIEnv *env, jclass clazz, + jobject game_icon_loaded_callback) { + jclass gameIconLoadedCallbackClass = env->GetObjectClass(game_icon_loaded_callback); + jmethodID gameIconLoadedMID = env->GetMethodID(gameIconLoadedCallbackClass, + "onGameIconLoaded", + "(J[III)V"); + env->DeleteLocalRef(gameIconLoadedCallbackClass); + g_gameIconLoadedCallback = std::make_shared(gameIconLoadedMID, + game_icon_loaded_callback); + gui_setOnGameIconLoaded(g_gameIconLoadedCallback); +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_requestGameIcon(JNIEnv *env, jclass clazz, jlong title_id) { + TitleId titleId = *reinterpret_cast(&title_id); + gui_requestGameIcon(titleId); +} + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_reloadGameTitles(JNIEnv *env, jclass clazz) { + gui_reloadGameTitles(); +} +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_initializeActiveSettings(JNIEnv *env, jclass clazz, + jstring data_path, jstring cache_path) { + std::string dataPath = JNIUtils::JStringToString(env, data_path); + std::string cachePath = JNIUtils::JStringToString(env, cache_path); + ActiveSettings::LoadOnce({}, dataPath, dataPath, cachePath, dataPath); +} + +int mainEmulatorHLE(); + +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_initializeEmulation(JNIEnv *env, jclass clazz) { + NetworkConfig::LoadOnce(); + mainEmulatorHLE(); + InputManager::instance().load(); + InitializeGlobalVulkan(); + createCemuDirectories(); +} +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_initializerRenderer(JNIEnv *env, jclass clazz) { + g_renderer = std::make_unique(); +} +extern "C" +JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_initializeRendererSurface(JNIEnv *env, jclass clazz, + jboolean is_main_canvas) { + int width, height; + if (is_main_canvas) + gui_getWindowPhysSize(width, height); + else + gui_getPadWindowPhysSize(width, height); + VulkanRenderer::GetInstance()->InitializeSurface( + {width, height}, + is_main_canvas); + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java b/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java new file mode 100644 index 00000000..98c37110 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/CemuApplication.java @@ -0,0 +1,12 @@ +package info.cemu.Cemu; + +import android.app.Application; + +public class CemuApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + NativeLibrary.initializeActiveSettings(getExternalFilesDir(null).getAbsoluteFile().toString(), getCacheDir().toString()); + NativeLibrary.initializeEmulation(); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java new file mode 100644 index 00000000..16f878e6 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulationActivity.java @@ -0,0 +1,47 @@ +package info.cemu.Cemu; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsAnimationCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; + +import android.os.Bundle; + +import info.cemu.Cemu.databinding.ActivityEmulationBinding; +import info.cemu.Cemu.databinding.ActivityMainBinding; + +public class EmulationActivity extends AppCompatActivity { + + public static final String GAME_TITLE_ID = "GameTitleId"; + long gameTitleId; + ActivityEmulationBinding binding; + EmulationFragment emulationFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + gameTitleId = extras.getLong(GAME_TITLE_ID); + } + setFullscreen(); + binding = ActivityEmulationBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + emulationFragment = (EmulationFragment) getSupportFragmentManager().findFragmentById(R.id.emulation_frame); + if (emulationFragment == null) { + emulationFragment = EmulationFragment.newInstance(gameTitleId); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.emulation_frame, emulationFragment) + .commit(); + } + } + + void setFullscreen() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); + controller.hide(WindowInsetsCompat.Type.systemBars()); + controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java new file mode 100644 index 00000000..93355862 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java @@ -0,0 +1,83 @@ +package info.cemu.Cemu; + +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.view.LayoutInflater; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; + +import java.util.Objects; + +import info.cemu.Cemu.databinding.FragmentEmulationBinding; +import info.cemu.Cemu.databinding.FragmentGamesBinding; + +public class EmulationFragment extends Fragment implements SurfaceHolder.Callback { + FragmentEmulationBinding binding; + + public EmulationFragment() { + } + + static EmulationFragment newInstance(long gameTitleId) { + EmulationFragment emulationFragment = new EmulationFragment(); + Bundle arguments = new Bundle(); + arguments.putLong(EmulationActivity.GAME_TITLE_ID, gameTitleId); + emulationFragment.setArguments(arguments); + return emulationFragment; + } + + long gameTitleId; + boolean isRunning; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle arguments = getArguments(); + if (arguments != null) { + gameTitleId = arguments.getLong(EmulationActivity.GAME_TITLE_ID); + } + + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FragmentEmulationBinding.inflate(inflater, container, false); + SurfaceView mainCanvas = binding.mainCanvas; + mainCanvas.getHolder().addCallback(this); + return binding.getRoot(); + } + + private void startGame() { + if (!isRunning) { + isRunning = true; + NativeLibrary.initializerRenderer(); + NativeLibrary.initializeRendererSurface(true); + NativeLibrary.startGame(gameTitleId); + } + } + + @Override + public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { + + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) { + NativeLibrary.setSurface(surfaceHolder.getSurface(), true); + NativeLibrary.setSurfaceSize(width, height, true); + startGame(); + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { + + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/Game.java b/src/android/app/src/main/java/info/cemu/Cemu/Game.java new file mode 100644 index 00000000..68bda1f8 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/Game.java @@ -0,0 +1,30 @@ +package info.cemu.Cemu; + +import android.graphics.Bitmap; + +public class Game { + Long titleId; + String title; + Bitmap icon; + + public Game(Long titleId, String title) { + this.titleId = titleId; + this.title = title; + } + + public void setIconData(int[] colors, int width, int height) { + icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); + } + + public Bitmap getIcon() { + return icon; + } + + public Long getTitleId() { + return titleId; + } + + public String getTitle() { + return title; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java new file mode 100644 index 00000000..7df4a747 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GameAdapter.java @@ -0,0 +1,88 @@ +package info.cemu.Cemu; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + + +import java.util.HashMap; +import java.util.Vector; + +public class GameAdapter extends RecyclerView.Adapter { + HashMap gameInfoMap; + Vector gameTitleIds; + private final GameTitleClickAction gameTitleClickAction; + + public interface GameTitleClickAction { + void action(long titleId); + } + + public GameAdapter(GameTitleClickAction gameTitleClickAction) { + super(); + this.gameTitleClickAction = gameTitleClickAction; + gameTitleIds = new Vector<>(); + gameInfoMap = new HashMap<>(); + } + + @NonNull + @Override + public GameAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_game, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) { + Game game = gameInfoMap.get(gameTitleIds.get(position)); + if (game != null) { + holder.icon.setImageBitmap(game.getIcon()); + holder.text.setText(game.getTitle()); + holder.itemView.setOnClickListener(v -> { + long titleId = game.titleId; + gameTitleClickAction.action(titleId); + }); + } + } + + @Override + public int getItemCount() { + return gameTitleIds.size(); + } + + void clear() { + int itemCount = gameTitleIds.size(); + notifyItemRangeRemoved(0, itemCount); + gameInfoMap.clear(); + gameTitleIds.clear(); + } + + void addGameInfo(long titleId, String title) { + gameTitleIds.addElement(titleId); + gameInfoMap.put(titleId, new Game(titleId, title)); + notifyItemInserted(gameTitleIds.size() - 1); + } + + void setGameIcon(long titleId, int[] colors, int width, int height) { + Game game = gameInfoMap.get(titleId); + if (game != null) { + game.setIconData(colors, width, height); + notifyItemChanged(gameTitleIds.indexOf(titleId)); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + ImageView icon; + TextView text; + + public ViewHolder(View itemView) { + super(itemView); + icon = itemView.findViewById(R.id.game_icon); + text = itemView.findViewById(R.id.game_title); + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java new file mode 100644 index 00000000..29caedd1 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/GamesFragment.java @@ -0,0 +1,56 @@ +package info.cemu.Cemu; + +import android.content.Intent; +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.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import info.cemu.Cemu.databinding.FragmentGamesBinding; + +public class GamesFragment extends Fragment { + FragmentGamesBinding binding; + GameAdapter gameAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FragmentGamesBinding.inflate(inflater, container, false); + RecyclerView recyclerView = binding.gamesRecyclerView; + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + View rootView = binding.getRoot(); + gameAdapter = new GameAdapter(titleId -> { + Intent intent = new Intent(getContext(), EmulationActivity.class); + intent.putExtra(EmulationActivity.GAME_TITLE_ID, titleId); + startActivity(intent); + }); + binding.gamesSwipeRefresh.setOnRefreshListener(() -> { + gameAdapter.clear(); + NativeLibrary.reloadGameTitles(); + binding.gamesSwipeRefresh.setRefreshing(false); + }); + recyclerView.setAdapter(gameAdapter); + NativeLibrary.setGameTitleLoadedCallback((titleId, title) -> { + requireActivity().runOnUiThread(() -> { + gameAdapter.addGameInfo(titleId, title); + NativeLibrary.requestGameIcon(titleId); + }); + }); + NativeLibrary.setGameIconLoadedCallback((titleId, colors, width, height) -> { + requireActivity().runOnUiThread(() -> gameAdapter.setGameIcon(titleId, colors, width, height)); + }); + NativeLibrary.reloadGameTitles(); + return rootView; + } + +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java index 0764945c..37f6ef1c 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java @@ -1,35 +1,54 @@ package info.cemu.Cemu; +import android.content.Intent; +import android.os.Bundle; + +import com.google.android.material.snackbar.Snackbar; + import androidx.appcompat.app.AppCompatActivity; -import android.os.Bundle; -import android.widget.TextView; +import android.view.View; + +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; import info.cemu.Cemu.databinding.ActivityMainBinding; +import android.view.Menu; +import android.view.MenuItem; + + public class MainActivity extends AppCompatActivity { - - static { - System.loadLibrary("CemuAndroid"); - } - private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - // Example of a call to a native method - TextView tv = binding.sampleText; - tv.setText(stringFromJNI()); + binding.fab.setOnClickListener( + view -> { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } + ); } - /** - * A native method that is implemented by the 'Cemu' native library, - * which is packaged with this application. - */ - public native String stringFromJNI(); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } } \ 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 new file mode 100644 index 00000000..44dd98a8 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -0,0 +1,40 @@ +package info.cemu.Cemu; + +import android.view.Surface; + +public class NativeLibrary { + static { + System.loadLibrary("CemuAndroid"); + } + + public static native void setSurface(Surface surface, boolean isMainCanvas); + + public static native void setSurfaceSize(int width, int height, boolean isMainCanvas); + + public static native void initializerRenderer(); + + public static native void initializeRendererSurface(boolean isMainCanvas); + + public static native void startGame(long titleId); + + + public interface GameTitleLoadedCallback { + void onGameTitleLoaded(long titleId, String title); + } + + public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback); + + public interface GameIconLoadedCallback { + void onGameIconLoaded(long titleId, int[] colors, int width, int height); + } + + public static native void setGameIconLoadedCallback(GameIconLoadedCallback gameIconLoadedCallback); + + public static native void requestGameIcon(long titleId); + + public static native void reloadGameTitles(); + + public static native void initializeActiveSettings(String dataPath, String cachePath); + + public static native void initializeEmulation(); +} 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 new file mode 100644 index 00000000..2c935d9d --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/SettingsActivity.java @@ -0,0 +1,35 @@ +package info.cemu.Cemu; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.os.Bundle; + +import java.util.Objects; + +import info.cemu.Cemu.databinding.ActivitySettingsBinding; + +public class SettingsActivity extends AppCompatActivity { + + private ActivitySettingsBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivitySettingsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbarSettings); + Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onSupportNavigateUp() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + getSupportFragmentManager().popBackStack(); + } else { + finish(); + } + return true; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java new file mode 100644 index 00000000..e9bf7043 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/SettingsFragment.java @@ -0,0 +1,95 @@ +package info.cemu.Cemu; + +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.SpinnerAdapter; + +import java.util.ArrayList; +import java.util.List; + +import info.cemu.Cemu.databinding.FragmentSettingsBinding; + +import info.cemu.Cemu.databinding.SettingsCheckboxBinding; +import info.cemu.Cemu.databinding.SettingsHeaderBinding; +import info.cemu.Cemu.databinding.SettingsCheckboxBinding; +import info.cemu.Cemu.databinding.SettingsSpinnerBinding; + +public class SettingsFragment extends Fragment { + protected FragmentSettingsBinding binding; + + public SettingsFragment() { + } + +// public abstract int getSettingsNameId(); + + protected interface OnCheckBoxStateListener { + void onStateChanged(boolean checked); + } + + protected interface OnItemSelectedPositionListener { + void onItemSelected(int position); + } + + protected void addCheckBox(String title, String description, boolean initialValue, OnCheckBoxStateListener checkBoxStateListener) { + View checkboxLayout = getLayoutInflater().inflate(R.layout.settings_spinner, null); + binding.settingsLayout.addView(checkboxLayout); + SettingsCheckboxBinding checkboxBinding = SettingsCheckboxBinding.bind(checkboxLayout); + checkboxBinding.textviewCheckboxName.setText(title); + checkboxBinding.checkbox.setChecked(initialValue); + checkboxBinding.textviewCheckboxDescription.setText(description); + checkboxLayout.setOnClickListener(v -> { + boolean isChecked = !checkboxBinding.checkbox.isChecked(); + checkboxBinding.checkbox.setChecked(isChecked); + checkBoxStateListener.onStateChanged(isChecked); + }); + } + +// protected void addSeparator() { +// View divider = getLayoutInflater().inflate(R.layout.divider_layout, null); +// binding.settingsLayout.addView(divider); +// } + + protected void addHeader(String title) { + View headerLayout = getLayoutInflater().inflate(R.layout.settings_header, null); + binding.settingsLayout.addView(headerLayout); + SettingsHeaderBinding headerBinding = SettingsHeaderBinding.bind(headerLayout); + headerBinding.textviewHeaderName.setText(title); + } + + protected void addSpinner(String title, List items, int initialPosition, OnItemSelectedPositionListener itemSelectedPositionListener) { + View spinnerLayout = getLayoutInflater().inflate(R.layout.settings_spinner, null); + binding.settingsLayout.addView(spinnerLayout); + SettingsSpinnerBinding spinnerBinding = SettingsSpinnerBinding.bind(spinnerLayout); + spinnerBinding.textviewSpinnerName.setText(title); + spinnerLayout.setOnClickListener(v -> spinnerBinding.spinner.performClick()); + ArrayAdapter adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, items); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerBinding.spinner.setAdapter(adapter); + spinnerBinding.spinner.setSelection(initialPosition); + spinnerBinding.spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + itemSelectedPositionListener.onItemSelected(position); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FragmentSettingsBinding.inflate(inflater, container, false); + return binding.getRoot(); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/ic_settings.xml b/src/android/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..f112c1d1 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml new file mode 100644 index 00000000..7d630bff --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml index 0fdf9852..0e5dc3c2 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml @@ -1,19 +1,28 @@ - - + + + android:layout_gravity="bottom|end" + android:layout_marginEnd="@dimen/settings_fab_margin" + android:layout_marginBottom="16dp" + android:contentDescription="@string/settings" + app:srcCompat="@drawable/ic_settings" /> - \ No newline at end of file + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 00000000..81b281d7 --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml new file mode 100644 index 00000000..ca69352a --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml new file mode 100644 index 00000000..d80fcb10 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_games.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 00000000..6c77450e --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/android/app/src/main/res/layout/layout_game.xml b/src/android/app/src/main/res/layout/layout_game.xml new file mode 100644 index 00000000..e1125c09 --- /dev/null +++ b/src/android/app/src/main/res/layout/layout_game.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/src/android/app/src/main/res/layout/settings_checkbox.xml b/src/android/app/src/main/res/layout/settings_checkbox.xml new file mode 100644 index 00000000..a3fe1c3a --- /dev/null +++ b/src/android/app/src/main/res/layout/settings_checkbox.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/settings_header.xml b/src/android/app/src/main/res/layout/settings_header.xml new file mode 100644 index 00000000..b541bfc4 --- /dev/null +++ b/src/android/app/src/main/res/layout/settings_header.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/android/app/src/main/res/layout/settings_spinner.xml b/src/android/app/src/main/res/layout/settings_spinner.xml new file mode 100644 index 00000000..86954226 --- /dev/null +++ b/src/android/app/src/main/res/layout/settings_spinner.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/android/app/src/main/res/menu/menu_main.xml b/src/android/app/src/main/res/menu/menu_main.xml new file mode 100644 index 00000000..076de2d3 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/navigation/nav_graph.xml b/src/android/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..843e307c --- /dev/null +++ b/src/android/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,11 @@ + + + + \ 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 new file mode 100644 index 00000000..e7197238 --- /dev/null +++ b/src/android/app/src/main/res/navigation/nav_settings_graph.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/values-land/dimens.xml b/src/android/app/src/main/res/values-land/dimens.xml new file mode 100644 index 00000000..65c04647 --- /dev/null +++ b/src/android/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/src/android/app/src/main/res/values-night/themes.xml b/src/android/app/src/main/res/values-night/themes.xml index c0c8b990..d25b0f2f 100644 --- a/src/android/app/src/main/res/values-night/themes.xml +++ b/src/android/app/src/main/res/values-night/themes.xml @@ -1,16 +1,7 @@ - \ No newline at end of file diff --git a/src/android/app/src/main/res/values-v23/themes.xml b/src/android/app/src/main/res/values-v23/themes.xml new file mode 100644 index 00000000..17852cdf --- /dev/null +++ b/src/android/app/src/main/res/values-v23/themes.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/values-w1240dp/dimens.xml b/src/android/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 00000000..63f5808e --- /dev/null +++ b/src/android/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ + + 200dp + \ No newline at end of file diff --git a/src/android/app/src/main/res/values-w600dp/dimens.xml b/src/android/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 00000000..65c04647 --- /dev/null +++ b/src/android/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/src/android/app/src/main/res/values/colors.xml b/src/android/app/src/main/res/values/colors.xml index f8c6127d..c8524cd9 100644 --- a/src/android/app/src/main/res/values/colors.xml +++ b/src/android/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF \ No newline at end of file diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..0bb1f4f2 --- /dev/null +++ b/src/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + \ No newline at end of file diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 24871da5..57b78d8a 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -1,3 +1,50 @@ Cemu + Settings + + First Fragment + Second Fragment + Next + Previous + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris + volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus + dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad + litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend + diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a, + ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n + Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus + egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed + neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada + fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae, + molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor + bibendum, vel congue leo egestas.\n\n + Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit + amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel, + molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer + interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at + lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula, + in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque + est.\n\n + Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh. + Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui + non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In + eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc, + quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra + ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a + placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus + convallis.\n\n + Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et + malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa + gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper, + libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper + sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus + libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus + vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. + + Settings + + Hello blank fragment + Game Icon \ No newline at end of file diff --git a/src/android/app/src/main/res/values/themes.xml b/src/android/app/src/main/res/values/themes.xml index 8209cef4..2d664e10 100644 --- a/src/android/app/src/main/res/values/themes.xml +++ b/src/android/app/src/main/res/values/themes.xml @@ -1,16 +1,9 @@ - + +