diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ca304b62 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "app/libraries/tinyxml2"] + path = app/libraries/tinyxml2 + url = https://github.com/leethomason/tinyxml2 +[submodule "app/libraries/fmt"] + path = app/libraries/fmt + url = https://github.com/fmtlib/fmt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..b522fcfe --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 00000000..1a65715f --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..35ee3ce2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a75096..51b39f74 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,40 @@ + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..c399eec8 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,7 @@ + + \ No newline at end of file diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ea548beb..7e3bf2ff 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,20 +1,28 @@ -cmake_minimum_required (VERSION 3.2) +cmake_minimum_required(VERSION 3.8) project(Lightswitch VERSION 1 LANGUAGES CXX) set_property(GLOBAL PROPERTY CMAKE_CXX_STANDARD 17 PROPERTY CMAKE_CXX_STANDARD_REQUIRED TRUE) +set(BUILD_TESTS FALSE) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) +add_subdirectory("libraries/tinyxml2") +add_subdirectory("libraries/fmt") + set(source_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp) -include_directories(${source_DIR}/include) +include_directories(libraries/unicorn/include) include_directories(${source_DIR}) add_library(lightswitch SHARED ${source_DIR}/lightswitch.cpp - ${source_DIR}/core/arm/cpu.cpp - ${source_DIR}/core/arm/memory.cpp - - ${source_DIR}/core/hos/kernel/ipc.cpp - ${source_DIR}/core/hos/kernel/kernel.cpp - ${source_DIR}/core/hos/kernel/svc.cpp - ${source_DIR}/core/hos/loaders/nro.cpp -) -target_link_libraries(lightswitch ${source_DIR}/lib/${ANDROID_ABI}/libunicorn.a) + ${source_DIR}/switch/os/os.cpp + ${source_DIR}/switch/os/ipc.cpp + ${source_DIR}/switch/os/kernel.cpp + ${source_DIR}/switch/os/svc.cpp + ${source_DIR}/switch/hw/cpu.cpp + ${source_DIR}/switch/hw/memory.cpp + ${source_DIR}/switch/common.cpp + ${source_DIR}/switch/loader/nro.cpp + ) +target_link_libraries(lightswitch ${CMAKE_SOURCE_DIR}/libraries/unicorn/libunicorn.a fmt tinyxml2) +target_compile_options(lightswitch PRIVATE -Wno-c++17-extensions) diff --git a/app/build.gradle b/app/build.gradle index 5e35c98b..e3730acc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,28 +4,28 @@ android { compileSdkVersion 29 buildToolsVersion "29.0.0" defaultConfig { - applicationId "gq.cyuubi.lightswitch" + applicationId "emu.lightswitch.lightswitch" minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" - externalNativeBuild { - cmake { - cppFlags "" - } - } ndk { abiFilters "arm64-v8a" } } buildTypes { release { + minifyEnabled true + useProguard false + } + debug { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + useProguard false } } externalNativeBuild { cmake { + version "3.8.0+" path "CMakeLists.txt" } } @@ -40,6 +40,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.preference:preference:1.1.0-beta01' + implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'com.google.android.material:material:1.0.0' + implementation 'me.xdrop:fuzzywuzzy:1.2.0' } diff --git a/app/libraries/fmt b/app/libraries/fmt new file mode 160000 index 00000000..6a497e1d --- /dev/null +++ b/app/libraries/fmt @@ -0,0 +1 @@ +Subproject commit 6a497e1d061993cce54c2d71506a90155a3725e6 diff --git a/app/libraries/tinyxml2 b/app/libraries/tinyxml2 new file mode 160000 index 00000000..61a4c7d5 --- /dev/null +++ b/app/libraries/tinyxml2 @@ -0,0 +1 @@ +Subproject commit 61a4c7d507322c9f494f5880d4c94b60e4ce9590 diff --git a/app/src/main/cpp/include/unicorn/arm64.h b/app/libraries/unicorn/include/unicorn/arm64.h similarity index 100% rename from app/src/main/cpp/include/unicorn/arm64.h rename to app/libraries/unicorn/include/unicorn/arm64.h diff --git a/app/src/main/cpp/include/unicorn/platform.h b/app/libraries/unicorn/include/unicorn/platform.h similarity index 100% rename from app/src/main/cpp/include/unicorn/platform.h rename to app/libraries/unicorn/include/unicorn/platform.h diff --git a/app/src/main/cpp/include/unicorn/unicorn.h b/app/libraries/unicorn/include/unicorn/unicorn.h similarity index 100% rename from app/src/main/cpp/include/unicorn/unicorn.h rename to app/libraries/unicorn/include/unicorn/unicorn.h diff --git a/app/src/main/cpp/lib/arm64-v8a/libunicorn.a b/app/libraries/unicorn/libunicorn.a similarity index 100% rename from app/src/main/cpp/lib/arm64-v8a/libunicorn.a rename to app/libraries/unicorn/libunicorn.a diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d42488e2..1dece4ef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,30 +1,39 @@ + xmlns:tools="http://schemas.android.com/tools" + package="emu.lightswitch.lightswitch"> - - + + - + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + + android:name="android.support.PARENT_ACTIVITY" + android:value="emu.lightswitch.lightswitch.MainActivity"/> + + + - + - + diff --git a/app/src/main/cpp/core/arm/cpu.cpp b/app/src/main/cpp/core/arm/cpu.cpp deleted file mode 100644 index e543ba5e..00000000 --- a/app/src/main/cpp/core/arm/cpu.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include "cpu.h" -#include "memory.h" -#include "core/hos/kernel/svc.h" - -// TODO: Handle Unicorn errors -namespace core::cpu { - uc_engine *uc; - - void HookInterrupt(uc_engine *uc, uint32_t intno, void *user_data); - - bool Initialize() - { - uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc); - - uc_hook hook{}; - uc_hook_add(uc, &hook, UC_HOOK_INTR, (void *) HookInterrupt, 0, 1, 0); - - // Map stack memory - if (!memory::Map(0x3000000, 0x1000000, "Stack")) return false; - SetRegister(UC_ARM64_REG_SP, 0x3100000); - - // Map TLS memory - if (!memory::Map(0x2000000, 0x1000, "TLS")) return false; - SetRegister(UC_ARM64_REG_TPIDRRO_EL0, 0x2000000); - - return true; - } - - void Run(uint64_t address) - { - uc_err err = uc_emu_start(uc, address, 1ULL << 63, 0, 0); - if (err) syslog(LOG_ERR, "uc_emu_start failed: %s", uc_strerror(err)); - } - - uint64_t GetRegister(uint32_t regid) - { - uint64_t registerValue; - uc_reg_read(uc, regid, ®isterValue); - return registerValue; - } - - void SetRegister(uint32_t regid, uint64_t value) - { - uc_reg_write(uc, regid, &value); - } - - void HookInterrupt(uc_engine *uc, uint32_t intno, void *user_data) - { - if (intno == 2) - { - uint32_t instr{}; - uc_mem_read(uc, GetRegister(UC_ARM64_REG_PC) - 4, &instr, 4); - - uint32_t svcId = instr >> 5 & 0xFF; - - if (core::kernel::SvcHandler(svcId) == 0x177202) - uc_close(uc); - } - else - { - syslog(LOG_ERR, "Unhandled interrupt #%i", intno); - uc_close(uc); - } - } -} - -// FIXME: Move this back to memory.cpp - zephyren25 -namespace core::memory { - bool Map(uint64_t address, size_t size, std::string label) { - void *ptr = mmap((void*)(address), size, PROT_EXEC | PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANON, 0, 0); - if (!ptr) - { - syslog(LOG_ERR, "Failed mapping region '%s'", label.c_str()); - return false; - } - - uc_err err = uc_mem_map_ptr(core::cpu::uc, address, size, UC_PROT_ALL, (void*)(address)); - if (err) - { - syslog(LOG_ERR, "UC map failed: %s", uc_strerror(err)); - return false; - } - - syslog(LOG_DEBUG, "Successfully mapped region '%s' to 0x%x", label.c_str(), address); - return true; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/cpu.h b/app/src/main/cpp/core/arm/cpu.h deleted file mode 100644 index 82cbc6ac..00000000 --- a/app/src/main/cpp/core/arm/cpu.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include -#include - -namespace core::cpu { - bool Initialize(); - void Run(uint64_t address); - - uint64_t GetRegister(uint32_t regid); - void SetRegister(uint32_t regid, uint64_t value); - -// bool MapUnicorn(uint64_t address, size_t size); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/memory.cpp b/app/src/main/cpp/core/arm/memory.cpp deleted file mode 100644 index 0e8bf7d5..00000000 --- a/app/src/main/cpp/core/arm/memory.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -#include "memory.h" - -namespace core::memory { - /*std::vector memoryRegions; - - bool Map(uint64_t address, size_t size, std::string label) { - void* ptr = mmap((void*)(address), size, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, 0, 0); - if (!ptr) { - syslog(LOG_ERR, "Failed mapping region '%s'", label.c_str()); - return false; - } - - syslog(LOG_INFO, "Mapping region '%s' to 0x%x, pointer %x", label.c_str(), address, (uint64_t)ptr); - - if (core::cpu::MapUnicorn(address, size, ptr)) return false; - - syslog(LOG_INFO, "Successfully mapped region '%s' to 0x%x", label.c_str(), address); - - memoryRegions.push_back({label, address, size, ptr}); - return true; - }*/ - - // TODO: Boundary checks - void Write(void* data, uint64_t offset, size_t size) { std::memcpy((void*)(offset), data, size); } - - void WriteU8(uint8_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 1); } - void WriteU16(uint16_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 2); } - void WriteU32(uint32_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 4); } - void WriteU64(uint64_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 8); } - - void Read(void* destination, uint64_t offset, size_t size) { std::memcpy(destination, (void*)(offset), size); } - - uint8_t ReadU8(uint64_t offset) { uint8_t value; Read(reinterpret_cast(&value), offset, 1); return value; } - uint16_t ReadU16(uint64_t offset) { uint16_t value; Read(reinterpret_cast(&value), offset, 2); return value; } - uint32_t ReadU32(uint64_t offset) { uint32_t value; Read(reinterpret_cast(&value), offset, 4); return value; } - uint64_t ReadU64(uint64_t offset) { uint64_t value; Read(reinterpret_cast(&value), offset, 8); return value; } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/memory.h b/app/src/main/cpp/core/arm/memory.h deleted file mode 100644 index f7e02762..00000000 --- a/app/src/main/cpp/core/arm/memory.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include -#include - -#define BASE_ADDRESS 0x80000000 - -namespace core::memory { - /*struct MemoryRegion { - std::string label; - uint64_t address; - size_t size; - void* ptr; - };*/ - - bool Map(uint64_t address, size_t size, std::string label = {}); - - void Write(void* data, uint64_t offset, size_t size); - - void WriteU8(uint8_t value, uint64_t offset); - void WriteU16(uint16_t value, uint64_t offset); - void WriteU32(uint32_t value, uint64_t offset); - void WriteU64(uint64_t value, uint64_t offset); - - void Read(void* destination, uint64_t offset, size_t size); - - uint8_t ReadU8(uint64_t offset); - uint16_t ReadU16(uint64_t offset); - uint32_t ReadU32(uint64_t offset); - uint64_t ReadU64(uint64_t offset); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/ipc.cpp b/app/src/main/cpp/core/hos/kernel/ipc.cpp deleted file mode 100644 index 8fdba736..00000000 --- a/app/src/main/cpp/core/hos/kernel/ipc.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include -#include "ipc.h" - -namespace core::kernel -{ - IpcRequest::IpcRequest(uint8_t* tlsPtr) - { - for(int i = 0; i < 32; i++) - syslog(LOG_DEBUG, "%02x\t%02x %02x %02x %02x %02x %02x %02x %02x", i*8, tlsPtr[0+(i*8)], tlsPtr[1+(i*8)], tlsPtr[2+(i*8)], tlsPtr[3+(i*8)], tlsPtr[4+(i*8)], tlsPtr[5+(i*8)], tlsPtr[6+(i*8)], tlsPtr[7+(i*8)]); - syslog(LOG_DEBUG, "-----------------------"); - uint32_t word1 = ((uint32_t*)tlsPtr)[1]; - type = *(uint16_t*)tlsPtr; - xCount = tlsPtr[2] & 0xF0 >> 4; - aCount = tlsPtr[2] & 0x0F; - bCount = tlsPtr[3] & 0xF0 >> 4; - wCount = tlsPtr[3] & 0x0F; - dataSize = word1 & 0x3FF; - dataPos = 8; - - if(tlsPtr[2] || tlsPtr[3]) - { - syslog(LOG_ERR, "IPC - X/A/B/W descriptors"); - exit(0); - } - - syslog(LOG_DEBUG, "Enable handle descriptor: %s", word1 >> 31 ? "yes" : "no"); - if(word1 >> 31) - { - syslog(LOG_ERR, "IPC - Handle descriptor"); - exit(0); - } - - // Align to 16 bytes - if((dataPos % 16) != 0) - dataPos += 16 - (dataPos % 16); - dataPtr = &tlsPtr[dataPos+16]; - - syslog(LOG_DEBUG, "Type: %x", type); - syslog(LOG_DEBUG, "X descriptors: 0x%x", xCount); - syslog(LOG_DEBUG, "A descriptors: 0x%x", aCount); - syslog(LOG_DEBUG, "B descriptors: 0x%x", bCount); - syslog(LOG_DEBUG, "W descriptors: 0x%x", wCount); - syslog(LOG_DEBUG, "Raw data size: 0x%x", word1 & 0x3FF); - syslog(LOG_DEBUG, "Data offset=%x, Data size=%x", dataPos, dataSize); - syslog(LOG_DEBUG, "Payload CmdId=%i", *((uint32_t*)&tlsPtr[dataPos+8])); - syslog(LOG_DEBUG, "Setting dataPtr to %08x", dataPos+16); - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/ipc.h b/app/src/main/cpp/core/hos/kernel/ipc.h deleted file mode 100644 index 657392b2..00000000 --- a/app/src/main/cpp/core/hos/kernel/ipc.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -namespace core::kernel -{ -class IpcRequest -{ -public: - IpcRequest(uint8_t* tlsPtr); - - template - T GetValue() - { - dataPos += sizeof(T); - return *reinterpret_cast(&dataPtr[dataPos-sizeof(T)]); - } - - uint16_t type, xCount, aCount, bCount, wCount; - uint32_t dataSize; - -private: - uint8_t* dataPtr; - uint32_t dataPos; -}; - -class IpcResponse -{ -public: - IpcResponse() {} -}; -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/kernel.cpp b/app/src/main/cpp/core/hos/kernel/kernel.cpp deleted file mode 100644 index 7c4675af..00000000 --- a/app/src/main/cpp/core/hos/kernel/kernel.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include -#include "kernel.h" - -namespace core::kernel -{ - std::unordered_map handles; - uint32_t handleIndex = 0xd001; - - uint32_t NewHandle(KObjectPtr obj) - { - handles.insert({handleIndex, obj}); - syslog(LOG_DEBUG, "Creating new handle 0x%x", handleIndex); - return handleIndex++; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/kernel.h b/app/src/main/cpp/core/hos/kernel/kernel.h deleted file mode 100644 index 309d3c23..00000000 --- a/app/src/main/cpp/core/hos/kernel/kernel.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include -#include - -#define SM_HANDLE 0xd000 // sm: is hardcoded for now - -namespace core::kernel -{ - class KObject - { - public: - KObject(uint32_t handle) : handle(handle) {} - uint32_t Handle() { return handle; } - private: - uint32_t handle; - }; - - typedef std::shared_ptr KObjectPtr; - - uint32_t NewHandle(KObjectPtr obj); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/svc.cpp b/app/src/main/cpp/core/hos/kernel/svc.cpp deleted file mode 100644 index 5a1b9a9e..00000000 --- a/app/src/main/cpp/core/hos/kernel/svc.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include -#include -#include -#include -#include "core/arm/cpu.h" -#include "core/arm/memory.h" -#include "kernel.h" -#include "ipc.h" -#include "svc.h" - -using namespace core::cpu; -namespace core::kernel { - static uint32_t ConnectToNamedPort() - { - std::string port(8, '\0'); - memory::Read((void*)port.data(), GetRegister(UC_ARM64_REG_X1), 8); - - if(std::strcmp(port.c_str(), "sm:") == 0) - SetRegister(UC_ARM64_REG_W1, SM_HANDLE); - else - { - syslog(LOG_ERR, "svcConnectToNamedPort tried connecting to invalid port '%s'", port.c_str()); - exit(0); - } - - return 0; - } - - static uint32_t SendSyncRequest() - { - syslog(LOG_INFO, "svcSendSyncRequest called for handle 0x%x, dumping TLS:", GetRegister(UC_ARM64_REG_X0)); - uint8_t tls[0x100]; - memory::Read(&tls, 0x2000000, 0x100); - - core::kernel::IpcRequest* r = new core::kernel::IpcRequest(tls); - - exit(0); - return 0; - } - - static uint32_t OutputDebugString() - { - std::string debug(GetRegister(UC_ARM64_REG_X1), '\0'); - memory::Read((void*)debug.data(), GetRegister(UC_ARM64_REG_X0), GetRegister(UC_ARM64_REG_X1)); - - syslog(LOG_DEBUG, "svcOutputDebugString: %s", debug.c_str()); - return 0; - } - - static uint32_t GetInfo() - { - switch (GetRegister(UC_ARM64_REG_X1)) - { - case 8: // IsCurrentProcessBeingDebugged - SetRegister(UC_ARM64_REG_X1, 0); // We're just lying to ourselves. Think about it - break; - case 12: // AddressSpaceBaseAddr - SetRegister(UC_ARM64_REG_X1, BASE_ADDRESS); - break; - case 18: // TitleId - SetRegister(UC_ARM64_REG_X1, 0); // TODO: Add this - break; - default: - syslog(LOG_ERR, "Unimplemented GetInfo ID! ID1 = %i, ID2 = %i", GetRegister(UC_ARM64_REG_X1), GetRegister(UC_ARM64_REG_X3)); - return 0x177202; - } - - return 0; - } - - std::pair svcTable[] = - { - {0x00, nullptr}, - {0x01, nullptr}, - {0x02, nullptr}, - {0x03, nullptr}, - {0x04, nullptr}, - {0x05, nullptr}, - {0x06, nullptr}, - {0x07, nullptr}, - {0x08, nullptr}, - {0x09, nullptr}, - {0x0a, nullptr}, - {0x0b, nullptr}, - {0x0c, nullptr}, - {0x0d, nullptr}, - {0x0e, nullptr}, - {0x0f, nullptr}, - {0x10, nullptr}, - {0x11, nullptr}, - {0x12, nullptr}, - {0x13, nullptr}, - {0x14, nullptr}, - {0x15, nullptr}, - {0x16, nullptr}, - {0x17, nullptr}, - {0x18, nullptr}, - {0x19, nullptr}, - {0x1a, nullptr}, - {0x1b, nullptr}, - {0x1c, nullptr}, - {0x1d, nullptr}, - {0x1e, nullptr}, - {0x1f, ConnectToNamedPort}, - {0x20, nullptr}, - {0x21, SendSyncRequest}, - {0x22, nullptr}, - {0x23, nullptr}, - {0x24, nullptr}, - {0x25, nullptr}, - {0x26, nullptr}, - {0x27, OutputDebugString}, - {0x28, nullptr}, - {0x29, GetInfo}, - {0x2a, nullptr}, - {0x2b, nullptr}, - {0x2c, nullptr}, - {0x2d, nullptr}, - {0x2e, nullptr}, - {0x2f, nullptr}, - {0x30, nullptr}, - {0x31, nullptr}, - {0x32, nullptr}, - {0x33, nullptr}, - {0x34, nullptr}, - {0x35, nullptr}, - {0x36, nullptr}, - {0x37, nullptr}, - {0x38, nullptr}, - {0x39, nullptr}, - {0x3a, nullptr}, - {0x3b, nullptr}, - {0x3c, nullptr}, - {0x3d, nullptr}, - {0x3e, nullptr}, - {0x3f, nullptr}, - {0x40, nullptr}, - {0x41, nullptr}, - {0x42, nullptr}, - {0x43, nullptr}, - {0x44, nullptr}, - {0x45, nullptr}, - {0x46, nullptr}, - {0x47, nullptr}, - {0x48, nullptr}, - {0x49, nullptr}, - {0x4a, nullptr}, - {0x4b, nullptr}, - {0x4c, nullptr}, - {0x4d, nullptr}, - {0x4e, nullptr}, - {0x4f, nullptr}, - {0x50, nullptr}, - {0x51, nullptr}, - {0x52, nullptr}, - {0x53, nullptr}, - {0x54, nullptr}, - {0x55, nullptr}, - {0x56, nullptr}, - {0x57, nullptr}, - {0x58, nullptr}, - {0x59, nullptr}, - {0x5a, nullptr}, - {0x5b, nullptr}, - {0x5c, nullptr}, - {0x5d, nullptr}, - {0x5e, nullptr}, - {0x5f, nullptr}, - {0x60, nullptr}, - {0x61, nullptr}, - {0x62, nullptr}, - {0x63, nullptr}, - {0x64, nullptr}, - {0x65, nullptr}, - {0x66, nullptr}, - {0x67, nullptr}, - {0x68, nullptr}, - {0x69, nullptr}, - {0x6a, nullptr}, - {0x6b, nullptr}, - {0x6c, nullptr}, - {0x6d, nullptr}, - {0x6e, nullptr}, - {0x6f, nullptr}, - {0x70, nullptr}, - {0x71, nullptr}, - {0x72, nullptr}, - {0x73, nullptr}, - {0x74, nullptr}, - {0x75, nullptr}, - {0x76, nullptr}, - {0x77, nullptr}, - {0x78, nullptr}, - {0x79, nullptr}, - {0x7a, nullptr}, - {0x7b, nullptr}, - {0x7c, nullptr}, - {0x7d, nullptr}, - {0x7e, nullptr}, - {0x7f, nullptr} - }; - - uint32_t SvcHandler(uint32_t svc) - { - std::pair* result = &(svcTable[svc]); - - if (result->second) - { - uint32_t returnCode = result->second(); - SetRegister(UC_ARM64_REG_W0, returnCode); - return returnCode; - } - else - { - syslog(LOG_ERR, "Unimplemented SVC 0x%02x", svc); - return 0x177202; // "Unimplemented behaviour" - } - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/svc.h b/app/src/main/cpp/core/hos/kernel/svc.h deleted file mode 100644 index 0d9b7ae4..00000000 --- a/app/src/main/cpp/core/hos/kernel/svc.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace core::kernel { - uint32_t SvcHandler(uint32_t svc); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/loaders/nro.cpp b/app/src/main/cpp/core/hos/loaders/nro.cpp deleted file mode 100644 index 6414a842..00000000 --- a/app/src/main/cpp/core/hos/loaders/nro.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include -#include "core/arm/cpu.h" -#include "core/arm/memory.h" -#include "nro.h" - -void ReadDataFromFile(std::string file, char* output, uint32_t offset, size_t size) -{ - std::ifstream f(file, std::ios::binary | std::ios::beg); - - f.seekg(offset); - f.read(output, size); - - f.close(); -} - -namespace core::loader { - bool LoadNro(std::string filePath) - { - syslog(LOG_INFO, "Loading NRO file %s\n", filePath.c_str()); - - NroHeader header; - ReadDataFromFile(filePath, reinterpret_cast(&header), 0x0, sizeof(NroHeader)); - if (header.magic != 0x304F524E) - { - syslog(LOG_ERR, "Invalid NRO magic 0x%x\n", header.magic); - return false; - } - - std::vector text, ro, data; - text.resize(header.segments[0].size); - ro.resize (header.segments[1].size); - data.resize(header.segments[2].size); - - ReadDataFromFile(filePath, reinterpret_cast(text.data()), header.segments[0].fileOffset, header.segments[0].size); - ReadDataFromFile(filePath, reinterpret_cast(ro.data()), header.segments[1].fileOffset, header.segments[1].size); - ReadDataFromFile(filePath, reinterpret_cast(data.data()), header.segments[2].fileOffset, header.segments[2].size); - - if (!memory::Map(BASE_ADDRESS, header.segments[0].size, ".text") || - !memory::Map(BASE_ADDRESS + header.segments[0].size, header.segments[1].size, ".ro") || - !memory::Map(BASE_ADDRESS + header.segments[0].size + header.segments[1].size, header.segments[2].size, ".data") || - !memory::Map(BASE_ADDRESS + header.segments[0].size + header.segments[1].size + header.segments[2].size, header.bssSize, ".bss")) - { - syslog(LOG_ERR, "Failed mapping regions for executable"); - return false; - } - - memory::Write(text.data(), BASE_ADDRESS, text.size()); - memory::Write(ro.data(), BASE_ADDRESS + header.segments[0].size, ro.size()); - memory::Write(data.data(), BASE_ADDRESS + header.segments[0].size + header.segments[1].size, data.size()); - return true; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/loaders/nro.h b/app/src/main/cpp/core/hos/loaders/nro.h deleted file mode 100644 index 1b8291d1..00000000 --- a/app/src/main/cpp/core/hos/loaders/nro.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -namespace core::loader { - struct NroSegmentHeader { - uint32_t fileOffset; - uint32_t size; - }; - - struct NroHeader { - uint32_t unused; - uint32_t modOffset; - uint64_t padding; - - uint32_t magic; - uint32_t version; - uint32_t size; - uint32_t flags; - - NroSegmentHeader segments[3]; - - uint32_t bssSize; - uint32_t reserved0; - uint64_t buildId[4]; - uint64_t reserved1; - - NroSegmentHeader extraSegments[3]; - }; - - bool LoadNro(std::string filePath); -} \ No newline at end of file diff --git a/app/src/main/cpp/lightswitch.cpp b/app/src/main/cpp/lightswitch.cpp index fadc19a7..d3bbf0ae 100644 --- a/app/src/main/cpp/lightswitch.cpp +++ b/app/src/main/cpp/lightswitch.cpp @@ -1,16 +1,50 @@ #include #include -#include -#include -#include -#include +#include +#include +#include +#include "switch/device.h" +#include "switch/common.h" + +std::thread *game_thread; + +void signal_handle(int sig_no) { + throw lightSwitch::exception("A signal has been raised: " + std::to_string(sig_no)); +} + +void thread_main(std::string rom_path, std::string pref_path, std::string log_path) { + auto log = std::make_shared(log_path); + log->write(lightSwitch::Logger::INFO, "Launching ROM {0}", rom_path); +// long long i = 0; +// while(true){ +// log->write(lightSwitch::Logger::INFO, "#{0}", i); +// sleep(1); +// i++; +// } + auto settings = std::make_shared(pref_path); + try { + lightSwitch::device device(log, settings); + device.run(rom_path); + log->write(lightSwitch::Logger::INFO, "Emulation has ended!"); + } catch (std::exception &e) { + log->write(lightSwitch::Logger::ERROR, e.what()); + } catch (...) { + log->write(lightSwitch::Logger::ERROR, "An unknown exception has occurred."); + } +} extern "C" JNIEXPORT void JNICALL -Java_gq_cyuubi_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring file_) { - const char *file = env->GetStringUTFChars(file_, 0); - core::cpu::Initialize(); - core::loader::LoadNro(file); - core::cpu::Run(BASE_ADDRESS); - env->ReleaseStringUTFChars(file_, file); +Java_emu_lightswitch_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring rom_path_, + jstring pref_path_, jstring log_path_) { + const char *rom_path = env->GetStringUTFChars(rom_path_, 0); + const char *pref_path = env->GetStringUTFChars(pref_path_, 0); + const char *log_path = env->GetStringUTFChars(log_path_, 0); + // std::signal(SIGABRT, signal_handle); + if (game_thread) pthread_kill(game_thread->native_handle(), SIGABRT); + // Running on UI thread is not a good idea, any crashes and such will be propagated + game_thread = new std::thread(thread_main, std::string(rom_path, strlen(rom_path)), std::string(pref_path, strlen(pref_path)), std::string(log_path, strlen(log_path))); + env->ReleaseStringUTFChars(rom_path_, rom_path); + env->ReleaseStringUTFChars(pref_path_, pref_path); + env->ReleaseStringUTFChars(log_path_, log_path); } \ No newline at end of file diff --git a/app/src/main/cpp/switch/common.cpp b/app/src/main/cpp/switch/common.cpp new file mode 100644 index 00000000..f2417590 --- /dev/null +++ b/app/src/main/cpp/switch/common.cpp @@ -0,0 +1,81 @@ +#include "common.h" +#include +#include + +namespace lightSwitch { + // Settings + Settings::Settings(std::string pref_xml) { + tinyxml2::XMLDocument pref; + if (pref.LoadFile(pref_xml.c_str())) { + syslog(LOG_ERR, "TinyXML2 Error: %s", pref.ErrorStr()); + throw pref.ErrorID(); + } + tinyxml2::XMLElement *elem = pref.LastChild()->FirstChild()->ToElement(); + while (elem) { + switch (elem->Value()[0]) { + case 's': + string_map.insert( + std::pair((char *) elem->FindAttribute("name")->Value(), + (char *) elem->GetText())); + break; + case 'b': + bool_map.insert( + std::pair((char *) elem->FindAttribute("name")->Value(), + elem->FindAttribute("value")->BoolValue())); + default: + break; + }; + if (elem->NextSibling()) + elem = elem->NextSibling()->ToElement(); + else break; + } + pref.Clear(); + } + + char *Settings::GetString(char *key) { + return string_map.at(key); + } + + bool Settings::GetBool(char *key) { + return bool_map.at(key); + } + + void Settings::List() { + auto it_s = string_map.begin(); + while (it_s != string_map.end()) { + syslog(LOG_INFO, "Key: %s", it_s->first); + syslog(LOG_INFO, "Value: %s", GetString(it_s->first)); + it_s++; + } + auto it_b = bool_map.begin(); + while (it_b != bool_map.end()) { + syslog(LOG_INFO, "Key: %s", it_b->first); + syslog(LOG_INFO, "Value: %i", GetBool(it_b->first)); + it_b++; + } + } + + // Logger + Logger::Logger(std::string log_path) { + log_file.open(log_path, std::ios::app); + write_header("Logging started"); + } + + Logger::~Logger() { + write_header("Logging ended"); + } + + void Logger::write(Logger::LogLevel level, std::string str) { +#ifdef NDEBUG + if (level == DEBUG) + return; +#endif + log_file << "1|" << level_str[level] << "|" << str << "\n"; + log_file.flush(); + } + + void Logger::write_header(std::string str) { + log_file << "0|" << str << "\n"; + log_file.flush(); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/common.h b/app/src/main/cpp/switch/common.h new file mode 100644 index 00000000..526a39c1 --- /dev/null +++ b/app/src/main/cpp/switch/common.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "hw/cpu.h" +#include "hw/memory.h" +#include "constant.h" + +namespace lightSwitch { + class Settings { + private: + struct KeyCompare { + bool operator()(char const *a, char const *b) const { + return std::strcmp(a, b) < 0; + } + }; + + std::map string_map; + std::map bool_map; + + public: + Settings(std::string pref_xml); + + char *GetString(char *key); + + bool GetBool(char *key); + + void List(); + }; + + class Logger { + private: + std::ofstream log_file; + const char *level_str[4] = {"0", "1", "2", "3"}; + int level_syslog[4] = {LOG_ERR, LOG_WARNING, LOG_INFO, LOG_DEBUG}; + + public: + enum LogLevel { + ERROR, + WARN, + INFO, + DEBUG + }; + + Logger(std::string log_path); + + ~Logger(); + + void write_header(std::string str); + + void write(LogLevel level, std::string str); + + template + void write(Logger::LogLevel level, const S &format_str, Args &&... args) { + write(level, fmt::format(format_str, args...)); + } + }; + + struct device_state { + std::shared_ptr cpu; + std::shared_ptr mem; + std::shared_ptr settings; + std::shared_ptr logger; + }; + //typedef std::shared_ptr device_state; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/constant.h b/app/src/main/cpp/switch/constant.h new file mode 100644 index 00000000..9e026f18 --- /dev/null +++ b/app/src/main/cpp/switch/constant.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace lightSwitch { + typedef std::runtime_error exception; + + namespace constant { + constexpr uint64_t base_addr = 0x80000000; + constexpr uint64_t stack_addr = 0x3000000; + constexpr size_t stack_size = 0x1000000; + constexpr uint64_t tls_addr = 0x2000000; + constexpr size_t tls_size = 0x1000; + constexpr uint32_t nro_magic = 0x304F524E; // NRO0 in reverse + constexpr uint_t svc_unimpl = 0x177202; // "Unimplemented behaviour" + constexpr uint32_t base_handle_index = 0xd001; + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/device.h b/app/src/main/cpp/switch/device.h new file mode 100644 index 00000000..b95bbffc --- /dev/null +++ b/app/src/main/cpp/switch/device.h @@ -0,0 +1,36 @@ +#pragma once + +#include "os/os.h" +#include "loader/nro.h" + +namespace lightSwitch { + class device { + private: + std::shared_ptr cpu; + std::shared_ptr memory; + os::OS os; + device_state state; + const std::map ext_case = { + {"nro", 1}, + {"NRO", 1} + }; + public: + device(std::shared_ptr &logger, std::shared_ptr &settings) : cpu(new hw::Cpu()), memory(new hw::Memory(cpu->GetEngine())), state{cpu, memory, settings, logger}, os({cpu, memory, settings, logger}) {}; + + void run(std::string rom_file) { + try { + switch (ext_case.at(rom_file.substr(rom_file.find_last_of('.') + 1))) { + case 1: { + loader::NroLoader loader(rom_file, state); + break; + } + default: + break; + } + cpu->Execute(constant::base_addr); + } catch (std::out_of_range &e) { + throw exception("The ROM extension wasn't recognized."); + } + } + }; +}; \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/cpu.cpp b/app/src/main/cpp/switch/hw/cpu.cpp new file mode 100644 index 00000000..b44a47fe --- /dev/null +++ b/app/src/main/cpp/switch/hw/cpu.cpp @@ -0,0 +1,46 @@ +#include "cpu.h" +#include "../constant.h" + + +namespace lightSwitch::hw { + Cpu::Cpu() { + err = uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc); + if (err) + throw std::runtime_error("An error occurred while running 'uc_open': " + std::string(uc_strerror(err))); + } + + Cpu::~Cpu() { + uc_close(uc); + } + + void Cpu::Execute(uint64_t address) { + // Set Registers + SetRegister(UC_ARM64_REG_SP, constant::stack_addr + 0x100000); // Stack Pointer (For some reason programs move the stack pointer backwards so 0x100000 is added) + SetRegister(UC_ARM64_REG_TPIDRRO_EL0, constant::tls_addr); // User Read-Only Thread ID Register + err = uc_emu_start(uc, address, std::numeric_limits::max(), 0, 0); + if (err) + throw std::runtime_error("An error occurred while running 'uc_emu_start': " + std::string(uc_strerror(err))); + } + + void Cpu::StopExecution() { + uc_emu_stop(uc); + } + + uint64_t Cpu::GetRegister(uint32_t reg_id) { + uint64_t registerValue; + err = uc_reg_read(uc, reg_id, ®isterValue); + if (err) + throw std::runtime_error("An error occurred while running 'uc_reg_read': " + std::string(uc_strerror(err))); + return registerValue; + } + + void Cpu::SetRegister(uint32_t regid, uint64_t value) { + err = uc_reg_write(uc, regid, &value); + if (err) + throw std::runtime_error("An error occurred while running 'uc_reg_write': " + std::string(uc_strerror(err))); + } + + uc_engine *Cpu::GetEngine() { + return uc; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/cpu.h b/app/src/main/cpp/switch/hw/cpu.h new file mode 100644 index 00000000..bc9c343e --- /dev/null +++ b/app/src/main/cpp/switch/hw/cpu.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace lightSwitch::hw { + class Cpu { + private: + uc_engine *uc; + uc_err err; + + public: + Cpu(); + + ~Cpu(); + + void SetHook(void *HookInterrupt); + + void Execute(uint64_t address); + + void StopExecution(); + + uint64_t GetRegister(uint32_t regid); + + void SetRegister(uint32_t regid, uint64_t value); + + uc_engine *GetEngine(); + }; +} diff --git a/app/src/main/cpp/switch/hw/memory.cpp b/app/src/main/cpp/switch/hw/memory.cpp new file mode 100644 index 00000000..a32c32d0 --- /dev/null +++ b/app/src/main/cpp/switch/hw/memory.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include "memory.h" +#include "../constant.h" + +namespace lightSwitch::hw { + // TODO: Boundary checks + Memory::Memory(uc_engine *uc_) : uc(uc_) { + // Map stack memory + Memory::Map(constant::stack_addr, constant::stack_size, stack); + // Map TLS memory + Memory::Map(constant::tls_addr, constant::tls_size, tls); + } + + void Memory::Map(uint64_t address, size_t size, Region region) { + region_map.insert(std::pair(region, {address, size})); + void *ptr = mmap((void *) address, size, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, 0, 0); + if (!ptr) + throw exception("An occurred while mapping region"); + uc_err err = uc_mem_map_ptr(uc, address, size, UC_PROT_ALL, (void *) address); + if (err) + throw exception("Unicorn failed to map region: " + std::string(uc_strerror(err))); + } + + void Memory::Write(void *data, uint64_t offset, size_t size) { + std::memcpy((void *) offset, data, size); + } + + template + void Memory::Write(T value, uint64_t offset) { + Write(reinterpret_cast(&value), offset, sizeof(T)); + } + + void Memory::Read(void *destination, uint64_t offset, size_t size) { + std::memcpy(destination, (void *) (offset), size); + } + + template + T Memory::Read(uint64_t offset) { + T value; + Read(reinterpret_cast(&value), offset, sizeof(T)); + return value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/memory.h b/app/src/main/cpp/switch/hw/memory.h new file mode 100644 index 00000000..adf1d70a --- /dev/null +++ b/app/src/main/cpp/switch/hw/memory.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace lightSwitch::hw { + class Memory { + private: + uc_engine *uc; + public: + enum Region { + stack, tls, text, rodata, data, bss + }; + struct RegionData { + uint64_t address; + size_t size; + }; + std::map region_map; + + Memory(uc_engine *uc_); + + void Map(uint64_t address, size_t size, Region region); + + void Write(void *data, uint64_t offset, size_t size); + + template + void Write(T value, uint64_t offset); + + void Read(void *destination, uint64_t offset, size_t size); + + template + T Read(uint64_t offset); + }; + +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/loader.h b/app/src/main/cpp/switch/loader/loader.h new file mode 100644 index 00000000..fd32804d --- /dev/null +++ b/app/src/main/cpp/switch/loader/loader.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "../common.h" + +namespace lightSwitch::loader { + class Loader { + protected: + std::string file_path; + std::ifstream file; + device_state state; + + template + void ReadOffset(T *output, uint32_t offset, size_t size) { + file.seekg(offset, std::ios_base::beg); + file.read(reinterpret_cast(output), size); + } + + virtual void Load(device_state state) = 0; + + public: + Loader(std::string &file_path_, device_state &state_) : file_path(file_path_), state(state_), file(file_path, std::ios::binary | std::ios::beg) {} + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/nro.cpp b/app/src/main/cpp/switch/loader/nro.cpp new file mode 100644 index 00000000..51cc75b1 --- /dev/null +++ b/app/src/main/cpp/switch/loader/nro.cpp @@ -0,0 +1,36 @@ +#include +#include "nro.h" + +namespace lightSwitch::loader { + void NroLoader::Load(device_state state) { + NroHeader header{}; + ReadOffset((uint32_t *) &header, 0x0, sizeof(NroHeader)); + if (header.magic != constant::nro_magic) + throw exception(fmt::format("Invalid NRO magic 0x{0:x}", header.magic)); + + auto text = new uint32_t[header.text.size](); + auto ro = new uint32_t[header.ro.size](); + auto data = new uint32_t[header.data.size](); + + ReadOffset(text, header.text.offset, header.text.size); + ReadOffset(ro, header.ro.offset, header.ro.size); + ReadOffset(data, header.data.offset, header.data.size); + + state.mem->Map(constant::base_addr, header.text.size, hw::Memory::text); + state.logger->write(Logger::DEBUG, "Successfully mapped region .text to 0x{0:x}, size is 0x{1:x}.", constant::base_addr, header.text.size); + state.mem->Map(constant::base_addr + header.text.size, header.ro.size, hw::Memory::rodata); + state.logger->write(Logger::DEBUG, "Successfully mapped region .ro to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size, header.ro.size); + state.mem->Map(constant::base_addr + header.text.size + header.ro.size, header.data.size, hw::Memory::data); + state.logger->write(Logger::DEBUG, "Successfully mapped region .data to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size + header.ro.size, header.data.size); + state.mem->Map(constant::base_addr + header.text.size + header.ro.size + header.data.size, header.bssSize, hw::Memory::bss); + state.logger->write(Logger::DEBUG, "Successfully mapped region .bss to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size + header.ro.size + header.data.size, header.bssSize); + + state.mem->Write(text, constant::base_addr, header.text.size); + state.mem->Write(ro, constant::base_addr + header.text.size, header.ro.size); + state.mem->Write(data, constant::base_addr + header.text.size + header.ro.size, header.data.size); + + delete[] text; + delete[] ro; + delete[] data; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/nro.h b/app/src/main/cpp/switch/loader/nro.h new file mode 100644 index 00000000..6119d995 --- /dev/null +++ b/app/src/main/cpp/switch/loader/nro.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "loader.h" + +namespace lightSwitch::loader { + class NroLoader : public Loader { + private: + struct NroSegmentHeader { + uint32_t offset; + uint32_t size; + }; + + struct NroHeader { + uint32_t : 32; + uint32_t mod_offset; + uint64_t : 64; + + uint32_t magic; + uint32_t version; + uint32_t size; + uint32_t flags; + + NroSegmentHeader text; + NroSegmentHeader ro; + NroSegmentHeader data; + + uint32_t bssSize; + uint32_t : 32; + uint64_t build_id[4]; + uint64_t : 64; + + NroSegmentHeader api_info; + NroSegmentHeader dynstr; + NroSegmentHeader dynsym; + }; + + void Load(device_state state); + + public: + NroLoader(std::string file_path, device_state state) : Loader(file_path, state) { Load(state); }; + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/ipc.cpp b/app/src/main/cpp/switch/os/ipc.cpp new file mode 100644 index 00000000..9c5a6f90 --- /dev/null +++ b/app/src/main/cpp/switch/os/ipc.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "ipc.h" + +namespace lightSwitch::os::ipc { + IpcRequest::IpcRequest(uint8_t *tls_ptr, device_state &state) : req_info((command_struct *) tls_ptr) { + state.logger->write(Logger::DEBUG, "Enable handle descriptor: {0}", (bool) req_info->handle_desc); + if (req_info->handle_desc) + throw exception("IPC - Handle descriptor"); + + // Align to 16 bytes + data_pos = 8; + data_pos = ((data_pos - 1) & ~(15U)) + 16; // ceil data_pos with multiples 16 + data_ptr = &tls_ptr[data_pos + sizeof(command_struct)]; + + state.logger->write(Logger::DEBUG, "Type: 0x{:x}", (uint8_t) req_info->type); + state.logger->write(Logger::DEBUG, "X descriptors: {}", (uint8_t) req_info->x_no); + state.logger->write(Logger::DEBUG, "A descriptors: {}", (uint8_t) req_info->a_no); + state.logger->write(Logger::DEBUG, "B descriptors: {}", (uint8_t) req_info->b_no); + state.logger->write(Logger::DEBUG, "W descriptors: {}", (uint8_t) req_info->w_no); + state.logger->write(Logger::DEBUG, "Raw data offset: 0x{:x}", data_pos); + state.logger->write(Logger::DEBUG, "Raw data size: {}", (uint16_t) req_info->data_sz); + state.logger->write(Logger::DEBUG, "Payload Command ID: {}", *((uint32_t *) &tls_ptr[data_pos + 8])); + } + + template + T IpcRequest::GetValue() { + data_pos += sizeof(T); + return *reinterpret_cast(&data_ptr[data_pos - sizeof(T)]); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/ipc.h b/app/src/main/cpp/switch/os/ipc.h new file mode 100644 index 00000000..7df3002a --- /dev/null +++ b/app/src/main/cpp/switch/os/ipc.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "switch/common.h" + +namespace lightSwitch::os::ipc { + class IpcRequest { + private: + uint8_t *data_ptr; + uint32_t data_pos; + public: + struct command_struct { + // https://switchbrew.org/wiki/IPC_Marshalling#IPC_Command_Structure + uint16_t type : 16; + uint8_t x_no : 4; + uint8_t a_no : 4; + uint8_t b_no : 4; + uint8_t w_no : 4; + uint16_t data_sz : 10; + uint8_t c_flags : 4; + uint32_t : 17; + bool handle_desc : 1; + } *req_info; + + IpcRequest(uint8_t *tlsPtr, device_state& state); + + template + T GetValue(); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/kernel.cpp b/app/src/main/cpp/switch/os/kernel.cpp new file mode 100644 index 00000000..dbf5cd4b --- /dev/null +++ b/app/src/main/cpp/switch/os/kernel.cpp @@ -0,0 +1,12 @@ +#include "kernel.h" +#include "svc.h" + +namespace lightSwitch::os { + Kernel::Kernel(device_state state_) : state(state_) {} + + uint32_t Kernel::NewHandle(KObjectPtr obj) { + handles.insert({handle_index, obj}); + state.logger->write(Logger::DEBUG, "Creating new handle 0x{0:x}", handle_index); + return handle_index++; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/kernel.h b/app/src/main/cpp/switch/os/kernel.h new file mode 100644 index 00000000..0c33f659 --- /dev/null +++ b/app/src/main/cpp/switch/os/kernel.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include "switch/common.h" + +namespace lightSwitch::os { + class KObject { + private: + uint32_t handle; + public: + KObject(uint32_t handle) : handle(handle) {} + + uint32_t Handle() { return handle; } + }; + + typedef std::shared_ptr KObjectPtr; + + class Kernel { + private: + device_state state; + uint32_t handle_index = constant::base_handle_index; + std::unordered_map handles; + public: + Kernel(device_state state_); + + uint32_t NewHandle(KObjectPtr obj); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/os.cpp b/app/src/main/cpp/switch/os/os.cpp new file mode 100644 index 00000000..583b82ca --- /dev/null +++ b/app/src/main/cpp/switch/os/os.cpp @@ -0,0 +1,49 @@ + +#include +#include "os.h" + +namespace lightSwitch::os { + OS::OS(device_state state_) : state(std::move(state_)) { + uc_err err = uc_hook_add(state.cpu->GetEngine(), &hook, UC_HOOK_INTR, + (void *) HookInterrupt, &state, 1, 0); + if (err) + throw std::runtime_error("An error occurred while running 'uc_hook_add': " + + std::string(uc_strerror(err))); + } + + OS::~OS() { + uc_hook_del(state.cpu->GetEngine(), hook); + } + + void OS::HookInterrupt(uc_engine *uc, uint32_t int_no, void *user_data) { + device_state state = *((device_state *) user_data); + try { + if (int_no == 2) { + uint32_t instr{}; + uc_err err = uc_mem_read(uc, state.cpu->GetRegister(UC_ARM64_REG_PC) - 4, &instr, 4); + if (err) + throw exception("An error occurred while running 'uc_mem_read': " + std::string(uc_strerror(err))); + uint32_t svcId = instr >> 5U & 0xFF; + SvcHandler(svcId, state); + } else { + state.logger->write(Logger::ERROR, "An unhandled interrupt has occurred: {}", int_no); + exit(int_no); + } + } catch (exception &e) { + state.logger->write(Logger::WARN, "An exception occurred during an interrupt: {}", e.what()); + } catch (...) { + state.logger->write(Logger::WARN, "An unknown exception has occurred."); + } + } + + void OS::SvcHandler(uint32_t svc, device_state &state) { + if (svc::svcTable[svc]) + (*svc::svcTable[svc])(state); + else + state.logger->write(Logger::WARN, "Unimplemented SVC 0x{0:x}", svc); + } + + void OS::SvcHandler(uint32_t svc) { + SvcHandler(svc, state); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/os.h b/app/src/main/cpp/switch/os/os.h new file mode 100644 index 00000000..7d146ed7 --- /dev/null +++ b/app/src/main/cpp/switch/os/os.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "switch/common.h" +#include "ipc.h" +#include "kernel.h" +#include "svc.h" + +namespace lightSwitch::os { + class OS { + private: + device_state state; + uc_hook hook{}; + public: + OS(device_state state_); + + ~OS(); + + static void HookInterrupt(uc_engine *uc, uint32_t int_no, void *user_data); + + static void SvcHandler(uint32_t svc, device_state &state); + + void SvcHandler(uint32_t svc); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/svc.cpp b/app/src/main/cpp/switch/os/svc.cpp new file mode 100644 index 00000000..ba603994 --- /dev/null +++ b/app/src/main/cpp/switch/os/svc.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include "svc.h" + +namespace lightSwitch::os::svc { + void ConnectToNamedPort(device_state &state) { + char port[constant::port_size]{0}; + state.mem->Read(port, state.cpu->GetRegister(UC_ARM64_REG_X1), constant::port_size); + if (std::strcmp(port, "sm:") == 0) + state.cpu->SetRegister(UC_ARM64_REG_W1, constant::sm_handle); + else { + state.logger->write(Logger::ERROR, "svcConnectToNamedPort tried connecting to invalid port \"{0}\"", port); + state.cpu->StopExecution(); + } + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void SendSyncRequest(device_state &state) { + state.logger->write(Logger::DEBUG, "svcSendSyncRequest called for handle 0x{0:x}.", state.cpu->GetRegister(UC_ARM64_REG_X0)); + uint8_t tls[constant::tls_ipc_size]; + state.mem->Read(&tls, constant::tls_addr, constant::tls_ipc_size); + ipc::IpcRequest request(tls, state); + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void OutputDebugString(device_state &state) { + std::string debug(state.cpu->GetRegister(UC_ARM64_REG_X1), '\0'); + state.mem->Read((void *) debug.data(), state.cpu->GetRegister(UC_ARM64_REG_X0), state.cpu->GetRegister(UC_ARM64_REG_X1)); + state.logger->write(Logger::INFO, "ROM Output: {0}", debug.c_str()); + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void GetInfo(device_state &state) { + switch (state.cpu->GetRegister(UC_ARM64_REG_X1)) { + case constant::infoState::AllowedCpuIdBitmask: + case constant::infoState::AllowedThreadPriorityMask: + case constant::infoState::IsCurrentProcessBeingDebugged: + state.cpu->SetRegister(UC_ARM64_REG_X1, 0); + break; + case constant::infoState::AddressSpaceBaseAddr: + state.cpu->SetRegister(UC_ARM64_REG_X1, constant::base_addr); + break; + case constant::infoState::TitleId: + state.cpu->SetRegister(UC_ARM64_REG_X1, 0); // TODO: Complete this + break; + default: + state.logger->write(Logger::WARN, "Unimplemented GetInfo call. ID1: {0}, ID2: {1}", state.cpu->GetRegister(UC_ARM64_REG_X1), state.cpu->GetRegister(UC_ARM64_REG_X3)); + state.cpu->SetRegister(UC_ARM64_REG_X1, constant::svc_unimpl); + return; + } + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void ExitProcess(device_state &state) { + state.cpu->StopExecution(); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/svc.h b/app/src/main/cpp/switch/os/svc.h new file mode 100644 index 00000000..dac3a9b3 --- /dev/null +++ b/app/src/main/cpp/switch/os/svc.h @@ -0,0 +1,184 @@ +#pragma once + +#include "ipc.h" +#include "../common.h" + +// https://switchbrew.org/wiki/SVC + +namespace lightSwitch::constant { + constexpr uint64_t sm_handle = 0xd000; // sm: is hardcoded for now + constexpr uint32_t tls_ipc_size = 0x100; + constexpr uint8_t port_size = 0x8; + namespace infoState { + // 1.0.0+ + constexpr uint8_t AllowedCpuIdBitmask = 0x0; + constexpr uint8_t AllowedThreadPriorityMask = 0x1; + constexpr uint8_t AliasRegionBaseAddr = 0x2; + constexpr uint8_t AliasRegionSize = 0x3; + constexpr uint8_t HeapRegionBaseAddr = 0x4; + constexpr uint8_t HeapRegionSize = 0x5; + constexpr uint8_t TotalMemoryAvailable = 0x6; + constexpr uint8_t TotalMemoryUsage = 0x7; + constexpr uint8_t IsCurrentProcessBeingDebugged = 0x8; + constexpr uint8_t ResourceLimit = 0x9; + constexpr uint8_t IdleTickCount = 0xA; + constexpr uint8_t RandomEntropy = 0xB; + // 2.0.0+ + constexpr uint8_t AddressSpaceBaseAddr = 0xC; + constexpr uint8_t AddressSpaceSize = 0xD; + constexpr uint8_t StackRegionBaseAddr = 0xE; + constexpr uint8_t StackRegionSize = 0xF; + // 3.0.0+ + constexpr uint8_t PersonalMmHeapSize = 0x10; + constexpr uint8_t PersonalMmHeapUsage = 0x11; + constexpr uint8_t TitleId = 0x12; + // 5.0.0+ + constexpr uint8_t UserExceptionContextAddr = 0x14; + // 6.0.0+ + constexpr uint8_t TotalMemoryAvailableWithoutMmHeap = 0x15; + constexpr uint8_t TotalMemoryUsedWithoutMmHeap = 0x16; + }; +}; + +namespace lightSwitch::os::svc { + void ConnectToNamedPort(device_state &state); + + void SendSyncRequest(device_state &state); + + void OutputDebugString(device_state &state); + + void GetInfo(device_state &state); + + void ExitProcess(device_state &state); + + void static (*svcTable[0x80])(device_state &) = { + nullptr, // 0x00 + nullptr, // 0x01 + nullptr, // 0x02 + nullptr, // 0x03 + nullptr, // 0x04 + nullptr, // 0x05 + nullptr, // 0x06 + ExitProcess, // 0x07 + nullptr, // 0x08 + nullptr, // 0x09 + nullptr, // 0x0a + nullptr, // 0x0b + nullptr, // 0x0c + nullptr, // 0x0d + nullptr, // 0x0e + nullptr, // 0x0f + nullptr, // 0x10 + nullptr, // 0x11 + nullptr, // 0x12 + nullptr, // 0x13 + nullptr, // 0x14 + nullptr, // 0x15 + nullptr, // 0x16 + nullptr, // 0x17 + nullptr, // 0x18 + nullptr, // 0x19 + nullptr, // 0x1a + nullptr, // 0x1b + nullptr, // 0x1c + nullptr, // 0x1d + nullptr, // 0x1e + ConnectToNamedPort, // 0x1f + nullptr, // 0x20 + SendSyncRequest, // 0x21 + nullptr, // 0x22 + nullptr, // 0x23 + nullptr, // 0x24 + nullptr, // 0x25 + nullptr, // 0x26 + OutputDebugString, // 0x27 + nullptr, // 0x28 + GetInfo, // 0x29 + nullptr, // 0x2a + nullptr, // 0x2b + nullptr, // 0x2c + nullptr, // 0x2d + nullptr, // 0x2e + nullptr, // 0x2f + nullptr, // 0x30 + nullptr, // 0x31 + nullptr, // 0x32 + nullptr, // 0x33 + nullptr, // 0x34 + nullptr, // 0x35 + nullptr, // 0x36 + nullptr, // 0x37 + nullptr, // 0x38 + nullptr, // 0x39 + nullptr, // 0x3a + nullptr, // 0x3b + nullptr, // 0x3c + nullptr, // 0x3d + nullptr, // 0x3e + nullptr, // 0x3f + nullptr, // 0x40 + nullptr, // 0x41 + nullptr, // 0x42 + nullptr, // 0x43 + nullptr, // 0x44 + nullptr, // 0x45 + nullptr, // 0x46 + nullptr, // 0x47 + nullptr, // 0x48 + nullptr, // 0x49 + nullptr, // 0x4a + nullptr, // 0x4b + nullptr, // 0x4c + nullptr, // 0x4d + nullptr, // 0x4e + nullptr, // 0x4f + nullptr, // 0x50 + nullptr, // 0x51 + nullptr, // 0x52 + nullptr, // 0x53 + nullptr, // 0x54 + nullptr, // 0x55 + nullptr, // 0x56 + nullptr, // 0x57 + nullptr, // 0x58 + nullptr, // 0x59 + nullptr, // 0x5a + nullptr, // 0x5b + nullptr, // 0x5c + nullptr, // 0x5d + nullptr, // 0x5e + nullptr, // 0x5f + nullptr, // 0x60 + nullptr, // 0x61 + nullptr, // 0x62 + nullptr, // 0x63 + nullptr, // 0x64 + nullptr, // 0x65 + nullptr, // 0x66 + nullptr, // 0x67 + nullptr, // 0x68 + nullptr, // 0x69 + nullptr, // 0x6a + nullptr, // 0x6b + nullptr, // 0x6c + nullptr, // 0x6d + nullptr, // 0x6e + nullptr, // 0x6f + nullptr, // 0x70 + nullptr, // 0x71 + nullptr, // 0x72 + nullptr, // 0x73 + nullptr, // 0x74 + nullptr, // 0x75 + nullptr, // 0x76 + nullptr, // 0x77 + nullptr, // 0x78 + nullptr, // 0x79 + nullptr, // 0x7a + nullptr, // 0x7b + nullptr, // 0x7c + nullptr, // 0x7d + nullptr, // 0x7e + nullptr // 0x7f + }; +} \ No newline at end of file diff --git a/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java new file mode 100644 index 00000000..6725756d --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java @@ -0,0 +1,149 @@ +package emu.lightswitch.lightswitch; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +class GameItem extends BaseItem { + private File file; + transient private TitleEntry meta; + private int index; + + GameItem(File file) { + this.file = file; + index = file.getName().lastIndexOf("."); + meta = NroLoader.getTitleEntry(getPath()); + if (meta == null) { + meta = new TitleEntry(file.getName(), GameAdapter.mContext.getString(R.string.aset_missing), null); + } + } + + public boolean hasIcon() { + return !getSubTitle().equals(GameAdapter.mContext.getString(R.string.aset_missing)); + } + + public Bitmap getIcon() { + return meta.getIcon(); + } + + public String getTitle() { + return meta.getName() + " (" + getType() + ")"; + } + + String getSubTitle() { + return meta.getAuthor(); + } + + public String getType() { + return file.getName().substring(index + 1).toUpperCase(); + } + + public File getFile() { + return file; + } + + public String getPath() { + return file.getAbsolutePath(); + } + + @Override + String key() { + if (meta.getIcon() == null) + return meta.getName(); + return meta.getName() + " " + meta.getAuthor(); + } +} + +public class GameAdapter extends HeaderAdapter implements View.OnClickListener { + + GameAdapter(Context context) { super(context); } + + @Override + public void load(File file) throws IOException, ClassNotFoundException { + super.load(file); + for (int i = 0; i < item_array.size(); i++) + item_array.set(i, new GameItem(item_array.get(i).getFile())); + notifyDataSetChanged(); + } + + @Override + public void onClick(View view) { + int position = (int) view.getTag(); + if (getItemViewType(position) == ContentType.Item) { + GameItem item = (GameItem) getItem(position); + if (view.getId() == R.id.icon) { + Dialog builder = new Dialog(mContext); + builder.requestWindowFeature(Window.FEATURE_NO_TITLE); + Objects.requireNonNull(builder.getWindow()).setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + ImageView imageView = new ImageView(mContext); + assert item != null; + imageView.setImageBitmap(item.getIcon()); + builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + builder.show(); + } + } + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + ViewHolder viewHolder; + int type = type_array.get(position).type; + if (convertView == null) { + if (type == ContentType.Item) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.game_item, parent, false); + viewHolder.icon = convertView.findViewById(R.id.icon); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); + convertView.setTag(viewHolder); + } else { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.section_item, parent, false); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + convertView.setTag(viewHolder); + } + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + if (type == ContentType.Item) { + GameItem data = (GameItem) getItem(position); + viewHolder.txtTitle.setText(data.getTitle()); + viewHolder.txtSub.setText(data.getSubTitle()); + Bitmap icon = data.getIcon(); + if (icon != null) { + viewHolder.icon.setImageBitmap(icon); + viewHolder.icon.setOnClickListener(this); + viewHolder.icon.setTag(position); + } else { + viewHolder.icon.setImageDrawable(mContext.getDrawable(R.drawable.ic_missing_icon)); + viewHolder.icon.setOnClickListener(null); + } + } else { + viewHolder.txtTitle.setText((String) getItem(position)); + } + return convertView; + } + + private static class ViewHolder { + ImageView icon; + TextView txtTitle; + TextView txtSub; + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java new file mode 100644 index 00000000..35e0d169 --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java @@ -0,0 +1,189 @@ +package emu.lightswitch.lightswitch; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import androidx.annotation.NonNull; +import me.xdrop.fuzzywuzzy.FuzzySearch; +import me.xdrop.fuzzywuzzy.model.ExtractedResult; + +import java.io.*; +import java.util.ArrayList; + +class ContentType implements Serializable { + transient static final int Header = 0; + transient static final int Item = 1; + public final int type; + public int index; + + ContentType(int index, int type) { + this(type); + this.index = index; + } + + private ContentType(int type) { + switch (type) { + case Item: + case Header: + break; + default: + throw (new IllegalArgumentException()); + } + this.type = type; + } +} + +abstract class BaseItem implements Serializable { + abstract String key(); +} + +abstract class HeaderAdapter extends BaseAdapter implements Filterable, Serializable { + @SuppressLint("StaticFieldLeak") + static Context mContext; + ArrayList type_array; + ArrayList item_array; + private ArrayList type_array_uf; + private ArrayList header_array; + private String search_term = ""; + + HeaderAdapter(Context context) { + mContext = context; + this.item_array = new ArrayList<>(); + this.header_array = new ArrayList<>(); + this.type_array_uf = new ArrayList<>(); + this.type_array = new ArrayList<>(); + } + + public void add(Object item, int type) { + if (type == ContentType.Item) { + item_array.add((ItemType) item); + type_array_uf.add(new ContentType(item_array.size() - 1, ContentType.Item)); + } else { + header_array.add((String) item); + type_array_uf.add(new ContentType(header_array.size() - 1, ContentType.Header)); + } + if(search_term.length()!=0) + this.getFilter().filter(search_term); + else + type_array=type_array_uf; + } + + public void save(File file) throws IOException { + State state = new State<>(item_array, header_array, type_array_uf); + FileOutputStream file_obj = new FileOutputStream(file); + ObjectOutputStream out = new ObjectOutputStream(file_obj); + out.writeObject(state); + out.close(); + file_obj.close(); + } + + void load(File file) throws IOException, ClassNotFoundException { + FileInputStream file_obj = new FileInputStream(file); + ObjectInputStream in = new ObjectInputStream(file_obj); + State state = (State) in.readObject(); + in.close(); + file_obj.close(); + if (state != null) { + this.item_array = state.item_array; + this.header_array = state.header_array; + this.type_array_uf = state.type_array; + this.getFilter().filter(search_term); + } + } + + public void clear() { + item_array.clear(); + header_array.clear(); + type_array_uf.clear(); + type_array.clear(); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return type_array.size(); + } + + @Override + public Object getItem(int i) { + ContentType type = type_array.get(i); + if (type.type == ContentType.Item) + return item_array.get(type.index); + else + return header_array.get(type.index); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + return type_array.get(position).type; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @NonNull + @Override + public abstract View getView(int position, View convertView, @NonNull ViewGroup parent); + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence charSequence) { + FilterResults results = new FilterResults(); + search_term = ((String) charSequence).toLowerCase().replaceAll(" ", ""); + if (charSequence.length() == 0) { + results.values = type_array_uf; + results.count = type_array_uf.size(); + } else { + ArrayList filter_data = new ArrayList<>(); + ArrayList key_arr = new ArrayList<>(); + SparseIntArray key_ind = new SparseIntArray(); + for (int index = 0; index < type_array_uf.size(); index++) { + ContentType item = type_array_uf.get(index); + if (item.type == ContentType.Item) { + key_arr.add(item_array.get(item.index).key().toLowerCase()); + key_ind.append(key_arr.size() - 1, index); + } + } + for (ExtractedResult result : FuzzySearch.extractTop(search_term, key_arr, Math.max(1, 10 - search_term.length()))) + if (result.getScore() >= 35) + filter_data.add(type_array_uf.get(key_ind.get(result.getIndex()))); + results.values = filter_data; + results.count = filter_data.size(); + } + return results; + } + + @Override + protected void publishResults(CharSequence charSequence, FilterResults filterResults) { + type_array = (ArrayList) filterResults.values; + notifyDataSetChanged(); + } + }; + } + + class State implements Serializable { + private ArrayList item_array; + private ArrayList header_array; + private ArrayList type_array; + + State(ArrayList item_array, ArrayList header_array, ArrayList type_array) { + this.item_array = item_array; + this.header_array = header_array; + this.type_array = type_array; + } + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java new file mode 100644 index 00000000..b192e52a --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java @@ -0,0 +1,141 @@ +package emu.lightswitch.lightswitch; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.FileObserver; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ListView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.preference.PreferenceManager; + +import java.io.*; + +import static java.lang.Thread.interrupted; + +public class LogActivity extends AppCompatActivity { + File log_file; + BufferedReader reader; + Thread thread; + LogAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.log_activity); + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) + actionBar.setDisplayHomeAsUpEnabled(true); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final ListView log_list = this.findViewById(R.id.log_list); + adapter = new LogAdapter(this, Integer.parseInt(prefs.getString("log_level", "3")), getResources().getStringArray(R.array.log_level)); + log_list.setAdapter(adapter); + log_file = new File(getApplicationInfo().dataDir + "/log.bin"); + try { + InputStream inputStream = new FileInputStream(log_file); + reader = new BufferedReader(new InputStreamReader(inputStream)); + } catch (FileNotFoundException e) { + Log.w("Logger", "IO Error during access of log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.file_missing), Toast.LENGTH_LONG).show(); + finish(); + } + thread = new Thread(new Runnable() { + @Override + public void run() { + @SuppressWarnings("deprecation") // Required as FileObserver(File) is only on API level 29 also no AndroidX version present + FileObserver observer = new FileObserver(log_file.getPath()) { + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.MODIFY) { + try { + boolean done = false; + while (!done) { + final String line = reader.readLine(); + done = (line == null); + if (!done) { + runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.add(line); + } + }); + } + } + } catch (IOException e) { + Log.w("Logger", "IO Error during access of log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + } + }; + observer.onEvent(FileObserver.MODIFY, log_file.getPath()); + observer.startWatching(); + while (!interrupted()) ; + observer.stopWatching(); + } + }); + thread.start(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.toolbar_log, menu); + MenuItem mSearch = menu.findItem(R.id.action_search_log); + final SearchView searchView = (SearchView) mSearch.getActionView(); + searchView.setSubmitButtonEnabled(false); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + public boolean onQueryTextSubmit(String query) { + searchView.setIconified(false); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + adapter.getFilter().filter(newText); + return true; + } + }); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_clear: + try { + FileWriter file = new FileWriter(log_file, false); + file.close(); + } catch (IOException e) { + Log.w("Logger", "IO Error while clearing the log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + Toast.makeText(getApplicationContext(), getString(R.string.cleared), Toast.LENGTH_LONG).show(); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + try { + thread.interrupt(); + thread.join(); + reader.close(); + } catch (IOException e) { + Log.w("Logger", "IO Error during closing BufferedReader: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } catch (NullPointerException ignored) { + } catch (InterruptedException ignored) { + } + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java new file mode 100644 index 00000000..f85cefbb --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java @@ -0,0 +1,108 @@ +package emu.lightswitch.lightswitch; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import com.google.android.material.snackbar.Snackbar; + +class LogItem extends BaseItem { + private String content; + private String level; + + LogItem(String content, String level) { + this.content = content; + this.level = level; + } + + public String getLevel() { + return level; + } + + public String getMessage() { + return content; + } + + @Override + String key() { + return getMessage(); + } +} + +public class LogAdapter extends HeaderAdapter implements View.OnLongClickListener { + private ClipboardManager clipboard; + private int debug_level; + private String[] level_str; + + LogAdapter(Context context, int debug_level, String[] level_str) { + super(context); + clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + this.debug_level = debug_level; + this.level_str = level_str; + } + + void add(final String log_line) { + String[] log_meta = log_line.split("\\|", 3); + if (log_meta[0].startsWith("1")) { + int level = Integer.parseInt(log_meta[1]); + if (level > this.debug_level) return; + super.add(new LogItem(log_meta[2], level_str[level]), ContentType.Item); + } else { + super.add(log_meta[1], ContentType.Header); + } + } + + @Override + public boolean onLongClick(View view) { + LogItem item = (LogItem) getItem(((ViewHolder) view.getTag()).position); + clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.getLevel() + ": " + item.getMessage())); + Toast.makeText(view.getContext(), "Copied to clipboard", Snackbar.LENGTH_LONG).show(); + return false; + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + ViewHolder viewHolder; + int type = type_array.get(position).type; + if (convertView == null) { + if (type == ContentType.Item) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.log_item, parent, false); + convertView.setOnLongClickListener(this); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); + convertView.setTag(viewHolder); + } else { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.section_item, parent, false); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + convertView.setTag(viewHolder); + } + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + if (type == ContentType.Item) { + LogItem data = (LogItem) getItem(position); + viewHolder.txtTitle.setText(data.getMessage()); + viewHolder.txtSub.setText(data.getLevel()); + } else { + viewHolder.txtTitle.setText((String) getItem(position)); + } + viewHolder.position = position; + return convertView; + } + + private static class ViewHolder { + TextView txtTitle; + TextView txtSub; + int position; + } +} diff --git a/app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java similarity index 53% rename from app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java rename to app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java index 57dfbf4f..6885f137 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java @@ -1,47 +1,48 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; - import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; - +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements View.OnClickListener { static { System.loadLibrary("lightswitch"); } SharedPreferences sharedPreferences; - FileAdapter adapter; + GameAdapter adapter; private void notifyUser(String text) { Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show(); - // Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show(); } private List findFile(String ext, File file, @Nullable List files) { - if (files == null) { + if (files == null) files = new ArrayList<>(); - } File[] list = file.listFiles(); if (list != null) { for (File file_i : list) { @@ -51,11 +52,12 @@ public class MainActivity extends AppCompatActivity { try { String file_str = file_i.getName(); if (ext.equalsIgnoreCase(file_str.substring(file_str.lastIndexOf(".") + 1))) { - if(NroMeta.verifyFile(file_i.getAbsolutePath())) { + if (NroLoader.verifyFile(file_i.getAbsolutePath())) { files.add(file_i); } } } catch (StringIndexOutOfBoundsException e) { + Log.w("findFile", Objects.requireNonNull(e.getMessage())); } } } @@ -63,11 +65,28 @@ public class MainActivity extends AppCompatActivity { return files; } - private void refresh_files() { + private void RefreshFiles(boolean try_load) { + if (try_load) { + try { + adapter.load(new File(getApplicationInfo().dataDir + "/roms.bin")); + return; + } catch (Exception e) { + Log.w("refreshFiles", "Ran into exception while loading: " + Objects.requireNonNull(e.getMessage())); + } + } adapter.clear(); List files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null); - for (File file : files) { - adapter.add(new GameItem(file, getApplicationContext())); + if (!files.isEmpty()) { + adapter.add(getString(R.string.nro), ContentType.Header); + for (File file : files) + adapter.add(new GameItem(file), ContentType.Item); + } else { + adapter.add(getString(R.string.no_rom), ContentType.Header); + } + try { + adapter.save(new File(getApplicationInfo().dataDir + "/roms.bin")); + } catch (IOException e) { + Log.w("refreshFiles", "Ran into exception while saving: " + Objects.requireNonNull(e.getMessage())); } } @@ -76,32 +95,55 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); - if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { + if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) System.exit(0); - } } setContentView(R.layout.main_activity); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - adapter = new FileAdapter(this, new ArrayList()); + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + FloatingActionButton log_fab = findViewById(R.id.log_fab); + log_fab.setOnClickListener(this); + adapter = new GameAdapter(this); ListView game_list = findViewById(R.id.game_list); game_list.setAdapter(adapter); game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - String path = ((GameItem) parent.getItemAtPosition(position)).getPath(); - notifyUser(getString(R.string.launch_string) + " " + path); - loadFile(path); + if (adapter.getItemViewType(position) == ContentType.Item) { + String path = ((GameItem) parent.getItemAtPosition(position)).getPath(); + notifyUser(getString(R.string.launching) + " " + path); + loadFile(path, getApplicationInfo().dataDir + "/shared_prefs/" + getApplicationInfo().packageName + "_preferences.xml", getApplicationInfo().dataDir + "/log.bin"); + } } }); - refresh_files(); + RefreshFiles(true); } @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.toolbar, menu); - return true; + getMenuInflater().inflate(R.menu.toolbar_main, menu); + MenuItem mSearch = menu.findItem(R.id.action_search_main); + final SearchView searchView = (SearchView) mSearch.getActionView(); + searchView.setSubmitButtonEnabled(false); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + adapter.getFilter().filter(newText); + return true; + } + }); + return super.onCreateOptionsMenu(menu); + } + + public void onClick(View view) { + if (view.getId() == R.id.log_fab) + startActivity(new Intent(this, LogActivity.class)); } @Override @@ -111,8 +153,8 @@ public class MainActivity extends AppCompatActivity { startActivity(new Intent(this, SettingsActivity.class)); return true; case R.id.action_refresh: - notifyUser(getString(R.string.refresh_string)); - refresh_files(); + RefreshFiles(false); + notifyUser(getString(R.string.refreshed)); return true; default: return super.onOptionsItemSelected(item); @@ -120,5 +162,5 @@ public class MainActivity extends AppCompatActivity { } - public native void loadFile(String file); + public native void loadFile(String rom_path, String preference_path, String log_path); } diff --git a/app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java b/app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java similarity index 96% rename from app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java rename to app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java index 0f959f45..76928472 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java @@ -1,4 +1,4 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -31,7 +31,7 @@ final class TitleEntry { } } -public class NroMeta { +public class NroLoader { public static TitleEntry getTitleEntry(String file) { try { RandomAccessFile f = new RandomAccessFile(file, "r"); @@ -70,7 +70,8 @@ public class NroMeta { return null; } } - public static boolean verifyFile(String file) { + + static boolean verifyFile(String file) { try { RandomAccessFile f = new RandomAccessFile(file, "r"); f.seek(0x10); // Skip to NroHeader.magic diff --git a/app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java similarity index 96% rename from app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java rename to app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java index 73472b0e..12313e6e 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java @@ -1,7 +1,6 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.os.Bundle; - import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; diff --git a/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java b/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java deleted file mode 100644 index 0eb78811..00000000 --- a/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java +++ /dev/null @@ -1,114 +0,0 @@ -package gq.cyuubi.lightswitch; - -import android.app.Dialog; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.ColorDrawable; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.util.ArrayList; - -class GameItem { - File file; - TitleEntry meta; - - int index; - - public GameItem(File file, Context ctx) { - this.file = file; - index = file.getName().lastIndexOf("."); - meta = NroMeta.getTitleEntry(getPath()); - if(meta==null) { - meta = new TitleEntry(file.getName(), ctx.getString(R.string.aset_missing), null); - } - } - - public Bitmap getIcon() { - return meta.getIcon(); - } - - public String getTitle() { - return meta.getName() + " (" + getType() + ")"; - } - - public String getSubTitle() { - return meta.getAuthor(); - } - - public String getType() { - return file.getName().substring(index + 1).toUpperCase(); - } - - public String getPath() { - return file.getAbsolutePath(); - } -} - -public class FileAdapter extends ArrayAdapter implements View.OnClickListener { - Context mContext; - - public FileAdapter(Context context, @NonNull ArrayList data) { - super(context, R.layout.file_item, data); - this.mContext = context; - } - - @Override - public void onClick(View v) { - int position = (Integer) v.getTag(); - GameItem dataModel = getItem(position); - switch (v.getId()) { - case R.id.icon: - Dialog builder = new Dialog(mContext); - builder.requestWindowFeature(Window.FEATURE_NO_TITLE); - builder.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); - ImageView imageView = new ImageView(mContext); - imageView.setImageBitmap(dataModel.getIcon()); - builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - builder.show(); - break; - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - GameItem dataModel = getItem(position); - ViewHolder viewHolder; - if (convertView == null) { - viewHolder = new ViewHolder(); - LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.file_item, parent, false); - viewHolder.icon = convertView.findViewById(R.id.icon); - viewHolder.txtTitle = convertView.findViewById(R.id.text_title); - viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); - convertView.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) convertView.getTag(); - } - viewHolder.txtTitle.setText(dataModel.getTitle()); - viewHolder.txtSub.setText(dataModel.getSubTitle()); - Bitmap icon = dataModel.getIcon(); - if(icon!=null) { - viewHolder.icon.setImageBitmap(icon); - viewHolder.icon.setOnClickListener(this); - viewHolder.icon.setTag(position); - } - return convertView; - } - - private static class ViewHolder { - ImageView icon; - TextView txtTitle; - TextView txtSub; - } -} diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 00000000..ea06934b --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_console.xml b/app/src/main/res/drawable/ic_log.xml similarity index 100% rename from app/src/main/res/drawable/ic_console.xml rename to app/src/main/res/drawable/ic_log.xml diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..90609629 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/file_item.xml b/app/src/main/res/layout/game_item.xml similarity index 100% rename from app/src/main/res/layout/file_item.xml rename to app/src/main/res/layout/game_item.xml diff --git a/app/src/main/res/layout/log_activity.xml b/app/src/main/res/layout/log_activity.xml new file mode 100644 index 00000000..a993dd34 --- /dev/null +++ b/app/src/main/res/layout/log_activity.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/log_item.xml b/app/src/main/res/layout/log_item.xml new file mode 100644 index 00000000..33bf1c19 --- /dev/null +++ b/app/src/main/res/layout/log_item.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 92e727de..554ca32a 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -8,21 +8,32 @@ + app:layout_constraintTop_toTopOf="parent"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/section_item.xml b/app/src/main/res/layout/section_item.xml new file mode 100644 index 00000000..38302f89 --- /dev/null +++ b/app/src/main/res/layout/section_item.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_log.xml b/app/src/main/res/menu/toolbar_log.xml new file mode 100644 index 00000000..399f2edc --- /dev/null +++ b/app/src/main/res/menu/toolbar_log.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar.xml b/app/src/main/res/menu/toolbar_main.xml similarity index 66% rename from app/src/main/res/menu/toolbar.xml rename to app/src/main/res/menu/toolbar_main.xml index acef1249..f77e5116 100644 --- a/app/src/main/res/menu/toolbar.xml +++ b/app/src/main/res/menu/toolbar_main.xml @@ -1,6 +1,12 @@ + + + Error + Warn + Info + Debug + + + 0 + 1 + 2 + 3 + System Language English - sys - en - + sys + en + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 955130d0..beb6306e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,4 @@ #E60012 #AB0000 #E60012 - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d658c887..27db1002 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,16 +1,29 @@ Lightswitch - + + Search + Settings + Logger Refresh - The list of ROMs has been refreshed. - Launching + The list of ROMs has been refreshed. + Launching ASET Header Missing Icon + Cannot find any ROMs + NROs + NSOs - Search Search Location + Logging + Log Level Localization Language + + Clear + + The log file was not found + An I/O error has occurred + The logs have been cleared diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 17a1fcd7..d5283e39 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,5 +6,12 @@ @color/colorPrimaryDark @color/colorAccent - + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 50f306d6..12f31d90 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -25,6 +25,17 @@ app:title="@string/search_location" app:useSimpleSummaryProvider="true" /> + + + diff --git a/build.gradle b/build.gradle index 78fbc17a..f44f379e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.4.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -18,7 +18,6 @@ allprojects { repositories { google() jcenter() - } } diff --git a/gradle.properties b/gradle.properties index e92e8a7f..c85e7d8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,15 +6,16 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx2g # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true +# Enable Gradle Daemon +org.gradle.daemon=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true - +android.enableJetifier=true \ No newline at end of file