Rewrite C++ parts and UI update

This update took way way too long to create. However, it was worthwhile. :)
This commit is contained in:
◱ PixelyIon 2019-07-25 01:49:43 +05:30
parent fd2bf8ebd5
commit 696ebde527
83 changed files with 1918 additions and 809 deletions

6
.gitmodules vendored Normal file
View File

@ -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

View File

@ -0,0 +1,34 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="400" />
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
<option name="SOFT_MARGINS" value="80,140" />
<Objective-C>
<option name="INDENT_DIRECTIVE_AS_CODE" value="true" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="FUNCTION_PARAMETERS_WRAP" value="0" />
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="0" />
<option name="CLASS_CONSTRUCTOR_INIT_LIST_WRAP" value="0" />
</Objective-C>
<Objective-C-extensions>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="SNAKE_CASE" />
<pair source="c" header="h" fileNamingConvention="SNAKE_CASE" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_ATTRIBUTE_WRAP" value="0" />
</XML>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="JAVA">
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
<option name="WRAP_LONG_LINES" value="true" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

4
.idea/discord.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordIntegrationProjectSettings" description="Lightswitch is an experimental Nintendo Switch emulator for Android phones." />
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SSBasedInspection" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -1,5 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="10">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>

View File

@ -2,5 +2,7 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/fmt" vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/libraries/tinyxml2" vcs="Git" />
</component> </component>
</project> </project>

View File

@ -1,20 +1,28 @@
cmake_minimum_required (VERSION 3.2) cmake_minimum_required(VERSION 3.8)
project(Lightswitch VERSION 1 LANGUAGES CXX) project(Lightswitch VERSION 1 LANGUAGES CXX)
set_property(GLOBAL PROPERTY CMAKE_CXX_STANDARD 17 PROPERTY CMAKE_CXX_STANDARD_REQUIRED TRUE) 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) set(source_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp)
include_directories(${source_DIR}/include) include_directories(libraries/unicorn/include)
include_directories(${source_DIR}) include_directories(${source_DIR})
add_library(lightswitch SHARED add_library(lightswitch SHARED
${source_DIR}/lightswitch.cpp ${source_DIR}/lightswitch.cpp
${source_DIR}/core/arm/cpu.cpp ${source_DIR}/switch/os/os.cpp
${source_DIR}/core/arm/memory.cpp ${source_DIR}/switch/os/ipc.cpp
${source_DIR}/switch/os/kernel.cpp
${source_DIR}/core/hos/kernel/ipc.cpp ${source_DIR}/switch/os/svc.cpp
${source_DIR}/core/hos/kernel/kernel.cpp ${source_DIR}/switch/hw/cpu.cpp
${source_DIR}/core/hos/kernel/svc.cpp ${source_DIR}/switch/hw/memory.cpp
${source_DIR}/core/hos/loaders/nro.cpp ${source_DIR}/switch/common.cpp
) ${source_DIR}/switch/loader/nro.cpp
target_link_libraries(lightswitch ${source_DIR}/lib/${ANDROID_ABI}/libunicorn.a) )
target_link_libraries(lightswitch ${CMAKE_SOURCE_DIR}/libraries/unicorn/libunicorn.a fmt tinyxml2)
target_compile_options(lightswitch PRIVATE -Wno-c++17-extensions)

View File

@ -4,28 +4,28 @@ android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion "29.0.0" buildToolsVersion "29.0.0"
defaultConfig { defaultConfig {
applicationId "gq.cyuubi.lightswitch" applicationId "emu.lightswitch.lightswitch"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
externalNativeBuild {
cmake {
cppFlags ""
}
}
ndk { ndk {
abiFilters "arm64-v8a" abiFilters "arm64-v8a"
} }
} }
buildTypes { buildTypes {
release { release {
minifyEnabled true
useProguard false
}
debug {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' useProguard false
} }
} }
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version "3.8.0+"
path "CMakeLists.txt" path "CMakeLists.txt"
} }
} }
@ -40,6 +40,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 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 'com.google.android.material:material:1.0.0'
implementation 'me.xdrop:fuzzywuzzy:1.2.0'
} }

1
app/libraries/fmt Submodule

@ -0,0 +1 @@
Subproject commit 6a497e1d061993cce54c2d71506a90155a3725e6

@ -0,0 +1 @@
Subproject commit 61a4c7d507322c9f494f5880d4c94b60e4ce9590

View File

@ -1,30 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gq.cyuubi.lightswitch"> xmlns:tools="http://schemas.android.com/tools"
package="emu.lightswitch.lightswitch">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
<activity tools:ignore="GoogleAppIndexingWarning">
android:name=".SettingsActivity" <activity android:name=".LogActivity"
android:label="@string/settings" android:label="@string/log"
android:parentActivityName=".MainActivity"> android:parentActivityName=".MainActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="gq.cyuubi.lightswitch.MainActivity" /> android:value="emu.lightswitch.lightswitch.MainActivity"/>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="emu.lightswitch.lightswitch.MainActivity"/>
</activity> </activity>
<activity android:name=".MainActivity"> <activity android:name=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -1,89 +0,0 @@
#include <sys/mman.h>
#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, &registerValue);
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;
}
}

View File

@ -1,13 +0,0 @@
#pragma once
#include <syslog.h>
#include <unicorn/unicorn.h>
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);
}

View File

@ -1,40 +0,0 @@
#include <sys/mman.h>
#include <syslog.h>
#include <vector>
#include "memory.h"
namespace core::memory {
/*std::vector<MemoryRegion> 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<void*>(&value), offset, 1); }
void WriteU16(uint16_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&value), offset, 2); }
void WriteU32(uint32_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&value), offset, 4); }
void WriteU64(uint64_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&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<void*>(&value), offset, 1); return value; }
uint16_t ReadU16(uint64_t offset) { uint16_t value; Read(reinterpret_cast<void*>(&value), offset, 2); return value; }
uint32_t ReadU32(uint64_t offset) { uint32_t value; Read(reinterpret_cast<void*>(&value), offset, 4); return value; }
uint64_t ReadU64(uint64_t offset) { uint64_t value; Read(reinterpret_cast<void*>(&value), offset, 8); return value; }
}

View File

@ -1,30 +0,0 @@
#pragma once
#include <string>
#include <unicorn/unicorn.h>
#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);
}

View File

@ -1,49 +0,0 @@
#include <syslog.h>
#include <cstdlib>
#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);
}
}

View File

@ -1,31 +0,0 @@
#pragma once
#include <cstdint>
namespace core::kernel
{
class IpcRequest
{
public:
IpcRequest(uint8_t* tlsPtr);
template<typename T>
T GetValue()
{
dataPos += sizeof(T);
return *reinterpret_cast<T*>(&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() {}
};
}

View File

@ -1,16 +0,0 @@
#include <unordered_map>
#include <syslog.h>
#include "kernel.h"
namespace core::kernel
{
std::unordered_map<uint32_t, KObjectPtr> 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++;
}
}

View File

@ -1,21 +0,0 @@
#pragma once
#include <cstdint>
#include <memory>
#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<KObject> KObjectPtr;
uint32_t NewHandle(KObjectPtr obj);
}

View File

@ -1,219 +0,0 @@
#include <cstdint>
#include <string>
#include <syslog.h>
#include <utility>
#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<int, uint32_t(*)()> 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<int, uint32_t(*)()>* 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"
}
}
}

View File

@ -1,5 +0,0 @@
#pragma once
namespace core::kernel {
uint32_t SvcHandler(uint32_t svc);
}

View File

@ -1,55 +0,0 @@
#include <fstream>
#include <syslog.h>
#include <sys/mman.h>
#include <vector>
#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<char *>(&header), 0x0, sizeof(NroHeader));
if (header.magic != 0x304F524E)
{
syslog(LOG_ERR, "Invalid NRO magic 0x%x\n", header.magic);
return false;
}
std::vector<uint32_t> 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<char *>(text.data()), header.segments[0].fileOffset, header.segments[0].size);
ReadDataFromFile(filePath, reinterpret_cast<char *>(ro.data()), header.segments[1].fileOffset, header.segments[1].size);
ReadDataFromFile(filePath, reinterpret_cast<char *>(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;
}
}

View File

@ -1,31 +0,0 @@
#pragma once
#include <cstdint>
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);
}

View File

@ -1,16 +1,50 @@
#include <jni.h> #include <jni.h>
#include <string> #include <string>
#include <syslog.h> #include <csignal>
#include <core/arm/cpu.h> #include <thread>
#include <core/hos/loaders/nro.h> #include <pthread.h>
#include <core/arm/memory.h> #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<lightSwitch::Logger>(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<lightSwitch::Settings>(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" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_gq_cyuubi_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring file_) { Java_emu_lightswitch_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring rom_path_,
const char *file = env->GetStringUTFChars(file_, 0); jstring pref_path_, jstring log_path_) {
core::cpu::Initialize(); const char *rom_path = env->GetStringUTFChars(rom_path_, 0);
core::loader::LoadNro(file); const char *pref_path = env->GetStringUTFChars(pref_path_, 0);
core::cpu::Run(BASE_ADDRESS); const char *log_path = env->GetStringUTFChars(log_path_, 0);
env->ReleaseStringUTFChars(file_, file); // 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);
} }

View File

@ -0,0 +1,81 @@
#include "common.h"
#include <tinyxml2.h>
#include <syslog.h>
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 *, char *>((char *) elem->FindAttribute("name")->Value(),
(char *) elem->GetText()));
break;
case 'b':
bool_map.insert(
std::pair<char *, bool>((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();
}
}

View File

@ -0,0 +1,71 @@
#pragma once
#include <map>
#include <fstream>
#include <syslog.h>
#include <string>
#include <sstream>
#include <memory>
#include <fmt/format.h>
#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<char *, char *, KeyCompare> string_map;
std::map<char *, bool, KeyCompare> 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<typename S, typename... Args>
void write(Logger::LogLevel level, const S &format_str, Args &&... args) {
write(level, fmt::format(format_str, args...));
}
};
struct device_state {
std::shared_ptr<hw::Cpu> cpu;
std::shared_ptr<hw::Memory> mem;
std::shared_ptr<Settings> settings;
std::shared_ptr<Logger> logger;
};
//typedef std::shared_ptr<device_state_struct> device_state;
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <stdexcept>
#include <string>
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;
};
}

View File

@ -0,0 +1,36 @@
#pragma once
#include "os/os.h"
#include "loader/nro.h"
namespace lightSwitch {
class device {
private:
std::shared_ptr<hw::Cpu> cpu;
std::shared_ptr<hw::Memory> memory;
os::OS os;
device_state state;
const std::map<std::string, int> ext_case = {
{"nro", 1},
{"NRO", 1}
};
public:
device(std::shared_ptr<Logger> &logger, std::shared_ptr<Settings> &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.");
}
}
};
};

View File

@ -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<uint64_t>::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, &registerValue);
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;
}
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <syslog.h>
#include <unicorn/unicorn.h>
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();
};
}

View File

@ -0,0 +1,45 @@
#include <sys/mman.h>
#include <syslog.h>
#include <vector>
#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, RegionData>(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<typename T>
void Memory::Write(T value, uint64_t offset) {
Write(reinterpret_cast<void *>(&value), offset, sizeof(T));
}
void Memory::Read(void *destination, uint64_t offset, size_t size) {
std::memcpy(destination, (void *) (offset), size);
}
template<typename T>
T Memory::Read(uint64_t offset) {
T value;
Read(reinterpret_cast<void *>(&value), offset, sizeof(T));
return value;
}
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <unicorn/unicorn.h>
#include <map>
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, RegionData> 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<typename T>
void Write(T value, uint64_t offset);
void Read(void *destination, uint64_t offset, size_t size);
template<typename T>
T Read(uint64_t offset);
};
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include "../common.h"
namespace lightSwitch::loader {
class Loader {
protected:
std::string file_path;
std::ifstream file;
device_state state;
template<typename T>
void ReadOffset(T *output, uint32_t offset, size_t size) {
file.seekg(offset, std::ios_base::beg);
file.read(reinterpret_cast<char *>(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) {}
};
}

View File

@ -0,0 +1,36 @@
#include <vector>
#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;
}
}

View File

@ -0,0 +1,43 @@
#pragma once
#include <cstdint>
#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); };
};
}

View File

@ -0,0 +1,31 @@
#include <syslog.h>
#include <cstdlib>
#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<typename T>
T IpcRequest::GetValue() {
data_pos += sizeof(T);
return *reinterpret_cast<T *>(&data_ptr[data_pos - sizeof(T)]);
}
}

View File

@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#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<typename T>
T GetValue();
};
}

View File

@ -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++;
}
}

View File

@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#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<KObject> KObjectPtr;
class Kernel {
private:
device_state state;
uint32_t handle_index = constant::base_handle_index;
std::unordered_map<uint32_t, KObjectPtr> handles;
public:
Kernel(device_state state_);
uint32_t NewHandle(KObjectPtr obj);
};
}

View File

@ -0,0 +1,49 @@
#include <unicorn/arm64.h>
#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);
}
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <err.h>
#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);
};
}

View File

@ -0,0 +1,60 @@
#include <cstdint>
#include <string>
#include <syslog.h>
#include <utility>
#include <unicorn/arm64.h>
#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();
}
}

View File

@ -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
};
}

View File

@ -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<GameItem> 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;
}
}

View File

@ -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<ItemType extends BaseItem> extends BaseAdapter implements Filterable, Serializable {
@SuppressLint("StaticFieldLeak")
static Context mContext;
ArrayList<ContentType> type_array;
ArrayList<ItemType> item_array;
private ArrayList<ContentType> type_array_uf;
private ArrayList<String> 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<ContentType> filter_data = new ArrayList<>();
ArrayList<String> 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<ContentType>) filterResults.values;
notifyDataSetChanged();
}
};
}
class State<StateType> implements Serializable {
private ArrayList<StateType> item_array;
private ArrayList<String> header_array;
private ArrayList<ContentType> type_array;
State(ArrayList<StateType> item_array, ArrayList<String> header_array, ArrayList<ContentType> type_array) {
this.item_array = item_array;
this.header_array = header_array;
this.type_array = type_array;
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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<LogItem> 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;
}
}

View File

@ -1,47 +1,48 @@
package gq.cyuubi.lightswitch; package emu.lightswitch.lightswitch;
import android.Manifest; import android.Manifest;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ListView; import android.widget.ListView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity implements View.OnClickListener {
static { static {
System.loadLibrary("lightswitch"); System.loadLibrary("lightswitch");
} }
SharedPreferences sharedPreferences; SharedPreferences sharedPreferences;
FileAdapter adapter; GameAdapter adapter;
private void notifyUser(String text) { private void notifyUser(String text) {
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show(); Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show();
// Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
} }
private List<File> findFile(String ext, File file, @Nullable List<File> files) { private List<File> findFile(String ext, File file, @Nullable List<File> files) {
if (files == null) { if (files == null)
files = new ArrayList<>(); files = new ArrayList<>();
}
File[] list = file.listFiles(); File[] list = file.listFiles();
if (list != null) { if (list != null) {
for (File file_i : list) { for (File file_i : list) {
@ -51,11 +52,12 @@ public class MainActivity extends AppCompatActivity {
try { try {
String file_str = file_i.getName(); String file_str = file_i.getName();
if (ext.equalsIgnoreCase(file_str.substring(file_str.lastIndexOf(".") + 1))) { 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); files.add(file_i);
} }
} }
} catch (StringIndexOutOfBoundsException e) { } catch (StringIndexOutOfBoundsException e) {
Log.w("findFile", Objects.requireNonNull(e.getMessage()));
} }
} }
} }
@ -63,11 +65,28 @@ public class MainActivity extends AppCompatActivity {
return files; 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(); adapter.clear();
List<File> files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null); List<File> files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null);
for (File file : files) { if (!files.isEmpty()) {
adapter.add(new GameItem(file, getApplicationContext())); 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); super.onCreate(savedInstanceState);
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) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); 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); System.exit(0);
}
} }
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
adapter = new FileAdapter(this, new ArrayList<GameItem>()); 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); ListView game_list = findViewById(R.id.game_list);
game_list.setAdapter(adapter); game_list.setAdapter(adapter);
game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() { game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String path = ((GameItem) parent.getItemAtPosition(position)).getPath(); if (adapter.getItemViewType(position) == ContentType.Item) {
notifyUser(getString(R.string.launch_string) + " " + path); String path = ((GameItem) parent.getItemAtPosition(position)).getPath();
loadFile(path); 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 @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar, menu); getMenuInflater().inflate(R.menu.toolbar_main, menu);
return true; 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 @Override
@ -111,8 +153,8 @@ public class MainActivity extends AppCompatActivity {
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
return true; return true;
case R.id.action_refresh: case R.id.action_refresh:
notifyUser(getString(R.string.refresh_string)); RefreshFiles(false);
refresh_files(); notifyUser(getString(R.string.refreshed));
return true; return true;
default: default:
return super.onOptionsItemSelected(item); 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);
} }

View File

@ -1,4 +1,4 @@
package gq.cyuubi.lightswitch; package emu.lightswitch.lightswitch;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
@ -31,7 +31,7 @@ final class TitleEntry {
} }
} }
public class NroMeta { public class NroLoader {
public static TitleEntry getTitleEntry(String file) { public static TitleEntry getTitleEntry(String file) {
try { try {
RandomAccessFile f = new RandomAccessFile(file, "r"); RandomAccessFile f = new RandomAccessFile(file, "r");
@ -70,7 +70,8 @@ public class NroMeta {
return null; return null;
} }
} }
public static boolean verifyFile(String file) {
static boolean verifyFile(String file) {
try { try {
RandomAccessFile f = new RandomAccessFile(file, "r"); RandomAccessFile f = new RandomAccessFile(file, "r");
f.seek(0x10); // Skip to NroHeader.magic f.seek(0x10); // Skip to NroHeader.magic

View File

@ -1,7 +1,6 @@
package gq.cyuubi.lightswitch; package emu.lightswitch.lightswitch;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;

View File

@ -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<GameItem> implements View.OnClickListener {
Context mContext;
public FileAdapter(Context context, @NonNull ArrayList<GameItem> 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;
}
}

View File

@ -0,0 +1,5 @@
<vector android:alpha="0.85" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFF" android:pathData="M5,13h14v-2L5,11v2zM3,17h14v-2L3,15v2zM7,7v2h14L21,7L7,7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:alpha="0.85" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LogActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.MaterialComponents.Light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ListView
android:id="@+id/log_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:transcriptMode="normal"
android:fastScrollEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="15dp"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="2dp"
android:textSize="15sp"
android:textAppearance="?android:attr/textAppearanceListItem" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textSize="12sp"
android:textColor="@android:color/tertiary_text_light" />
</RelativeLayout>

View File

@ -8,21 +8,32 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
android:popupTheme="@style/ThemeOverlay.MaterialComponents.Light"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"/>
<ListView <ListView
android:id="@+id/game_list" android:id="@+id/game_list"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/log_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_log" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clickable="false"
android:padding="12dp">
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
android:textSize="13sp"
android:layout_marginTop="2dp"
android:layout_marginStart="5dp"/>
</RelativeLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search_log"
android:icon="@drawable/ic_search"
android:title="@string/search"
app:showAsAction="ifRoom|withText"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/action_clear"
android:icon="@drawable/ic_clear"
android:title="@string/clear"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search_main"
android:icon="@drawable/ic_search"
android:title="@string/search"
app:showAsAction="ifRoom|withText"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:icon="@drawable/ic_settings" android:icon="@drawable/ic_settings"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,11 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="log_level">
<item>Error</item>
<item>Warn</item>
<item>Info</item>
<item>Debug</item>
</string-array>
<string-array name="log_level_val">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
</string-array>
<string-array name="language_names"> <string-array name="language_names">
<item>System Language</item> <item>System Language</item>
<item>English</item> <item>English</item>
</string-array> </string-array>
<string-array name="language_values"> <string-array name="language_values">
<item>sys</item> <item>sys</item>
<item>en</item> <item>en</item>
</string-array> </string-array>
</resources> </resources>

View File

@ -3,4 +3,4 @@
<color name="colorPrimary">#E60012</color> <color name="colorPrimary">#E60012</color>
<color name="colorPrimaryDark">#AB0000</color> <color name="colorPrimaryDark">#AB0000</color>
<color name="colorAccent">#E60012</color> <color name="colorAccent">#E60012</color>
</resources> </resources>

View File

@ -1,16 +1,29 @@
<resources> <resources>
<string name="app_name">Lightswitch</string> <string name="app_name">Lightswitch</string>
<!-- Toolbar --> <!-- Common -->
<string name="search">Search</string>
<!-- Toolbar Main -->
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="log">Logger</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<!-- Main --> <!-- Main -->
<string name="refresh_string">The list of ROMs has been refreshed.</string> <string name="refreshed">The list of ROMs has been refreshed.</string>
<string name="launch_string">Launching</string> <string name="launching">Launching</string>
<string name="aset_missing">ASET Header Missing</string> <string name="aset_missing">ASET Header Missing</string>
<string name="icon">Icon</string> <string name="icon">Icon</string>
<string name="no_rom">Cannot find any ROMs</string>
<string name="nro">NROs</string>
<string name="nso">NSOs</string>
<!-- Settings --> <!-- Settings -->
<string name="search">Search</string>
<string name="search_location">Search Location</string> <string name="search_location">Search Location</string>
<string name="logging">Logging</string>
<string name="log_level">Log Level</string>
<string name="localization">Localization</string> <string name="localization">Localization</string>
<string name="localization_language">Language</string> <string name="localization_language">Language</string>
<!-- Toolbar Logger -->
<string name="clear">Clear</string>
<!-- Logger -->
<string name="file_missing">The log file was not found</string>
<string name="io_error">An I/O error has occurred</string>
<string name="cleared">The logs have been cleared</string>
</resources> </resources>

View File

@ -6,5 +6,12 @@
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
</style> </style>
<style name="ToolbarTheme">
<!-- Query Text Color -->
<item name="android:textColorPrimary">@android:color/white</item>
<!-- Hint Color -->
<item name="android:textColorHint">@android:color/darker_gray</item>
<!-- Icon Color -->
<item name="android:tint">@android:color/white</item>
</style>
</resources> </resources>

View File

@ -25,6 +25,17 @@
app:title="@string/search_location" app:title="@string/search_location"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="category_log"
android:title="@string/logging">
<ListPreference
android:defaultValue="2"
android:entries="@array/log_level"
android:entryValues="@array/log_level_val"
app:key="log_level"
app:title="@string/log_level"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="category_localization" android:key="category_localization"
android:title="@string/localization"> android:title="@string/localization">

View File

@ -7,7 +7,7 @@ buildscript {
} }
dependencies { 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -18,7 +18,6 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
} }
} }

View File

@ -6,15 +6,16 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true org.gradle.parallel=true
# Enable Gradle Daemon
org.gradle.daemon=true
# AndroidX package structure to make it clearer which packages are bundled with the # 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 # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true