mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-22 15:09:17 +01:00
NCA decryption (#99)
* NCA decryption * Remove unnecessary new lines * Remove loader error dialog * Always show ROMs * Address CRs * Add subtitle padding in grid mode
This commit is contained in:
parent
65019375ca
commit
4076d84efc
@ -12,7 +12,7 @@ set(source_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp)
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-Ofast -flto=full -Wno-unused-command-line-argument")
|
||||
if (uppercase_CMAKE_BUILD_TYPE STREQUAL "RELEASE")
|
||||
add_compile_definitions(NDEBUG)
|
||||
endif()
|
||||
endif ()
|
||||
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0048 OLD)
|
||||
add_subdirectory("libraries/tinyxml2")
|
||||
@ -24,6 +24,8 @@ include_directories("libraries/oboe/include")
|
||||
include_directories("libraries/vkhpp/include")
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0048 NEW)
|
||||
|
||||
find_package(mbedtls REQUIRED CONFIG)
|
||||
|
||||
include_directories(${source_DIR}/skyline)
|
||||
|
||||
add_library(skyline SHARED
|
||||
@ -38,6 +40,8 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/audio/track.cpp
|
||||
${source_DIR}/skyline/audio/resampler.cpp
|
||||
${source_DIR}/skyline/audio/adpcm_decoder.cpp
|
||||
${source_DIR}/skyline/crypto/aes_cipher.cpp
|
||||
${source_DIR}/skyline/crypto/key_store.cpp
|
||||
${source_DIR}/skyline/gpu.cpp
|
||||
${source_DIR}/skyline/gpu/macro_interpreter.cpp
|
||||
${source_DIR}/skyline/gpu/memory_manager.cpp
|
||||
@ -144,12 +148,13 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/services/prepo/IPrepoService.cpp
|
||||
${source_DIR}/skyline/vfs/os_filesystem.cpp
|
||||
${source_DIR}/skyline/vfs/partition_filesystem.cpp
|
||||
${source_DIR}/skyline/vfs/ctr_encrypted_backing.cpp
|
||||
${source_DIR}/skyline/vfs/rom_filesystem.cpp
|
||||
${source_DIR}/skyline/vfs/os_backing.cpp
|
||||
${source_DIR}/skyline/vfs/nacp.cpp
|
||||
${source_DIR}/skyline/vfs/nca.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static)
|
||||
target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static mbedtls::mbedcrypto)
|
||||
set(CMAKE_CXX17_EXTENSION_COMPILE_OPTION "-std=c++2a")
|
||||
target_compile_options(skyline PRIVATE -Wno-c++17-extensions -Wall -Wno-reorder -Wno-missing-braces -Wno-unused-variable -Wno-unused-private-field)
|
||||
|
@ -40,6 +40,9 @@ android {
|
||||
shrinkResources false
|
||||
}
|
||||
}
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version "3.10.2+"
|
||||
@ -63,7 +66,11 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
implementation(name: 'mbedtls', ext: 'aar')
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
flatDir {
|
||||
dirs 'libraries'
|
||||
}
|
||||
}
|
||||
|
BIN
app/libraries/mbedtls.aar
Normal file
BIN
app/libraries/mbedtls.aar
Normal file
Binary file not shown.
6
app/proguard-rules.pro
vendored
6
app/proguard-rules.pro
vendored
@ -1,9 +1,3 @@
|
||||
# Skyline Proguard Rules
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
-keep class emu.skyline.loader.AppEntry {
|
||||
void writeObject(java.io.ObjectOutputStream);
|
||||
void readObject(java.io.ObjectInputStream);
|
||||
}
|
||||
-keep class emu.skyline.GameActivity { *; }
|
||||
|
@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="emu.skyline">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-feature
|
||||
@ -47,6 +46,11 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity android:name="emu.skyline.preference.FileActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.input.ControllerActivity"
|
||||
android:exported="true">
|
||||
|
@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include "skyline/crypto/key_store.h"
|
||||
#include "skyline/vfs/nca.h"
|
||||
#include "skyline/vfs/os_backing.h"
|
||||
#include "skyline/loader/nro.h"
|
||||
#include "skyline/loader/nso.h"
|
||||
@ -8,54 +10,53 @@
|
||||
#include "skyline/loader/nsp.h"
|
||||
#include "skyline/jvm.h"
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JNIEnv *env, jobject thiz, jint jformat, jint fd) {
|
||||
skyline::loader::RomFormat format = static_cast<skyline::loader::RomFormat>(jformat);
|
||||
extern "C" JNIEXPORT jint JNICALL Java_emu_skyline_loader_RomFile_populate(JNIEnv *env, jobject thiz, jint jformat, jint fd, jstring appFilesPathJstring) {
|
||||
skyline::loader::RomFormat format{static_cast<skyline::loader::RomFormat>(jformat)};
|
||||
|
||||
auto appFilesPath{env->GetStringUTFChars(appFilesPathJstring, nullptr)};
|
||||
auto keyStore{std::make_shared<skyline::crypto::KeyStore>(appFilesPath)};
|
||||
env->ReleaseStringUTFChars(appFilesPathJstring, appFilesPath);
|
||||
|
||||
std::unique_ptr<skyline::loader::Loader> loader;
|
||||
try {
|
||||
auto backing = std::make_shared<skyline::vfs::OsBacking>(fd);
|
||||
auto backing{std::make_shared<skyline::vfs::OsBacking>(fd)};
|
||||
|
||||
switch (format) {
|
||||
case skyline::loader::RomFormat::NRO:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NroLoader(backing));
|
||||
loader = std::make_unique<skyline::loader::NroLoader>(backing);
|
||||
break;
|
||||
case skyline::loader::RomFormat::NSO:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NsoLoader(backing));
|
||||
loader = std::make_unique<skyline::loader::NsoLoader>(backing);
|
||||
break;
|
||||
case skyline::loader::RomFormat::NCA:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NcaLoader(backing));
|
||||
loader = std::make_unique<skyline::loader::NcaLoader>(backing, keyStore);
|
||||
break;
|
||||
case skyline::loader::RomFormat::NSP:
|
||||
return reinterpret_cast<jlong>(new skyline::loader::NspLoader(backing));
|
||||
loader = std::make_unique<skyline::loader::NspLoader>(backing, keyStore);
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
return static_cast<jint>(skyline::loader::LoaderResult::ParsingError);
|
||||
}
|
||||
} catch (const skyline::loader::loader_exception &e) {
|
||||
return static_cast<jint>(e.error);
|
||||
} catch (const std::exception &e) {
|
||||
return 0;
|
||||
return static_cast<jint>(skyline::loader::LoaderResult::ParsingError);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_emu_skyline_loader_RomFile_hasAssets(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
return reinterpret_cast<skyline::loader::Loader *>(instance)->nacp != nullptr;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jbyteArray JNICALL Java_emu_skyline_loader_RomFile_getIcon(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
std::vector<skyline::u8> buffer = reinterpret_cast<skyline::loader::Loader *>(instance)->GetIcon();
|
||||
|
||||
jbyteArray result = env->NewByteArray(buffer.size());
|
||||
env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast<const jbyte *>(buffer.data()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicationName(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
std::string applicationName = reinterpret_cast<skyline::loader::Loader *>(instance)->nacp->applicationName;
|
||||
|
||||
return env->NewStringUTF(applicationName.c_str());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicationPublisher(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
std::string applicationPublisher = reinterpret_cast<skyline::loader::Loader *>(instance)->nacp->applicationPublisher;
|
||||
|
||||
return env->NewStringUTF(applicationPublisher.c_str());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_emu_skyline_loader_RomFile_destroy(JNIEnv *env, jobject thiz, jlong instance) {
|
||||
delete reinterpret_cast<skyline::loader::NroLoader *>(instance);
|
||||
|
||||
jclass clazz{env->GetObjectClass(thiz)};
|
||||
jfieldID applicationNameField{env->GetFieldID(clazz, "applicationName", "Ljava/lang/String;")};
|
||||
jfieldID applicationAuthorField{env->GetFieldID(clazz, "applicationAuthor", "Ljava/lang/String;")};
|
||||
jfieldID rawIconField{env->GetFieldID(clazz, "rawIcon", "[B")};
|
||||
|
||||
if (loader->nacp) {
|
||||
env->SetObjectField(thiz, applicationNameField, env->NewStringUTF(loader->nacp->applicationName.c_str()));
|
||||
env->SetObjectField(thiz, applicationAuthorField, env->NewStringUTF(loader->nacp->applicationPublisher.c_str()));
|
||||
|
||||
auto icon{loader->GetIcon()};
|
||||
jbyteArray iconByteArray{env->NewByteArray(icon.size())};
|
||||
env->SetByteArrayRegion(iconByteArray, 0, icon.size(), reinterpret_cast<const jbyte *>(icon.data()));
|
||||
env->SetObjectField(thiz, rawIconField, iconByteArray);
|
||||
}
|
||||
|
||||
return static_cast<jint>(skyline::loader::LoaderResult::Success);
|
||||
}
|
||||
|
@ -62,6 +62,19 @@ namespace skyline {
|
||||
constexpr u16 DockedResolutionH = 1080; //!< The height component of the docked resolution
|
||||
// Time
|
||||
constexpr u64 NsInSecond = 1000000000; //!< This is the amount of nanoseconds in a second
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This is a std::runtime_error with libfmt formatting
|
||||
*/
|
||||
class exception : public std::runtime_error {
|
||||
public:
|
||||
/**
|
||||
* @param formatStr The exception string to be written, with libfmt formatting
|
||||
* @param args The arguments based on format_str
|
||||
*/
|
||||
template<typename S, typename... Args>
|
||||
inline exception(const S &formatStr, Args &&... args) : runtime_error(fmt::format(formatStr, args...)) {}
|
||||
};
|
||||
|
||||
namespace util {
|
||||
@ -159,6 +172,28 @@ namespace skyline {
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
constexpr u8 HexDigitToByte(char digit) {
|
||||
if (digit >= '0' && digit <= '9')
|
||||
return digit - '0';
|
||||
else if (digit >= 'a' && digit <= 'f')
|
||||
return digit - 'a' + 10;
|
||||
else if (digit >= 'A' && digit <= 'F')
|
||||
return digit - 'A' + 10;
|
||||
throw exception(fmt::format("Invalid hex char {}", digit));
|
||||
}
|
||||
|
||||
template<size_t Size>
|
||||
constexpr std::array<u8, Size> HexStringToArray(const std::string_view &hexString) {
|
||||
if (hexString.size() != Size * 2)
|
||||
throw exception("Invalid size");
|
||||
std::array<u8, Size> result;
|
||||
for (size_t i{}; i < Size; ++i) {
|
||||
size_t hexStrIndex{i * 2};
|
||||
result[i] = (HexDigitToByte(hexString[hexStrIndex]) << 4) | HexDigitToByte(hexString[hexStrIndex + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -350,19 +385,6 @@ namespace skyline {
|
||||
void List(const std::shared_ptr<Logger> &logger);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This is a std::runtime_error with libfmt formatting
|
||||
*/
|
||||
class exception : public std::runtime_error {
|
||||
public:
|
||||
/**
|
||||
* @param formatStr The exception string to be written, with libfmt formatting
|
||||
* @param args The arguments based on format_str
|
||||
*/
|
||||
template<typename S, typename... Args>
|
||||
inline exception(const S &formatStr, Args &&... args) : runtime_error(fmt::format(formatStr, args...)) {}
|
||||
};
|
||||
|
||||
class NCE;
|
||||
class JvmManager;
|
||||
namespace gpu {
|
||||
|
71
app/src/main/cpp/skyline/crypto/aes_cipher.cpp
Normal file
71
app/src/main/cpp/skyline/crypto/aes_cipher.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include "aes_cipher.h"
|
||||
|
||||
namespace skyline::crypto {
|
||||
AesCipher::AesCipher(std::span<u8> key, mbedtls_cipher_type_t type) {
|
||||
mbedtls_cipher_init(&decryptContext);
|
||||
if (mbedtls_cipher_setup(&decryptContext, mbedtls_cipher_info_from_type(type)) != 0)
|
||||
throw exception("Failed to setup decryption context");
|
||||
|
||||
if (mbedtls_cipher_setkey(&decryptContext, key.data(), key.size() * 8, MBEDTLS_DECRYPT) != 0)
|
||||
throw exception("Failed to set key for decryption context");
|
||||
}
|
||||
|
||||
AesCipher::~AesCipher() {
|
||||
mbedtls_cipher_free(&decryptContext);
|
||||
}
|
||||
|
||||
void AesCipher::SetIV(const std::array<u8, 0x10> &iv) {
|
||||
if (mbedtls_cipher_set_iv(&decryptContext, iv.data(), iv.size()) != 0)
|
||||
throw exception("Failed to set IV for decryption context");
|
||||
}
|
||||
|
||||
void AesCipher::Decrypt(u8 *destination, u8 *source, size_t size) {
|
||||
std::optional<std::vector<u8>> buf{};
|
||||
|
||||
u8 *targetDestination = [&]() {
|
||||
if (destination == source) {
|
||||
if (size > maxBufferSize) {
|
||||
buf.emplace(size);
|
||||
return buf->data();
|
||||
} else {
|
||||
if (size > buffer.size())
|
||||
buffer.resize(size);
|
||||
return buffer.data();
|
||||
}
|
||||
}
|
||||
return destination;
|
||||
}();
|
||||
|
||||
mbedtls_cipher_reset(&decryptContext);
|
||||
|
||||
size_t outputSize{};
|
||||
if (mbedtls_cipher_get_cipher_mode(&decryptContext) == MBEDTLS_MODE_XTS) {
|
||||
mbedtls_cipher_update(&decryptContext, source, size, targetDestination, &outputSize);
|
||||
} else {
|
||||
u32 blockSize{mbedtls_cipher_get_block_size(&decryptContext)};
|
||||
|
||||
for (size_t offset{}; offset < size; offset += blockSize) {
|
||||
size_t length{size - offset > blockSize ? blockSize : size - offset};
|
||||
mbedtls_cipher_update(&decryptContext, source + offset, length, targetDestination + offset, &outputSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (buf)
|
||||
std::memcpy(destination, buf->data(), size);
|
||||
else if (source == destination)
|
||||
std::memcpy(destination, buffer.data(), size);
|
||||
}
|
||||
|
||||
void AesCipher::XtsDecrypt(u8 *destination, u8 *source, size_t size, size_t sector, size_t sectorSize) {
|
||||
if (size % sectorSize)
|
||||
throw exception("Size must be multiple of sector size");
|
||||
|
||||
for (size_t i{}; i < size; i += sectorSize) {
|
||||
SetIV(GetTweak(sector++));
|
||||
Decrypt(destination + i, source + i, sectorSize);
|
||||
}
|
||||
}
|
||||
}
|
73
app/src/main/cpp/skyline/crypto/aes_cipher.h
Normal file
73
app/src/main/cpp/skyline/crypto/aes_cipher.h
Normal file
@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <mbedtls/cipher.h>
|
||||
#include <common.h>
|
||||
|
||||
namespace skyline::crypto {
|
||||
/**
|
||||
* @brief Wrapper for mbedtls for AES decryption using a cipher
|
||||
*/
|
||||
class AesCipher {
|
||||
private:
|
||||
mbedtls_cipher_context_t decryptContext;
|
||||
|
||||
/**
|
||||
* @brief Buffer should grow bigger than 1 MiB
|
||||
*/
|
||||
static constexpr size_t maxBufferSize = 1024 * 1024;
|
||||
|
||||
/**
|
||||
* @brief Buffer declared as class variable to avoid constant memory allocation
|
||||
*/
|
||||
std::vector<u8> buffer;
|
||||
|
||||
/**
|
||||
* @brief Calculates IV for XTS, basically just big to little endian conversion.
|
||||
*/
|
||||
inline static std::array<u8, 0x10> GetTweak(size_t sector) {
|
||||
std::array<u8, 0x10> tweak{};
|
||||
size_t le{__builtin_bswap64(sector)};
|
||||
std::memcpy(tweak.data() + 8, &le, 8);
|
||||
return tweak;
|
||||
}
|
||||
|
||||
public:
|
||||
AesCipher(std::span<u8> key, mbedtls_cipher_type_t type);
|
||||
|
||||
~AesCipher();
|
||||
|
||||
/**
|
||||
* @brief Sets initilization vector
|
||||
*/
|
||||
void SetIV(const std::array<u8, 0x10> &iv);
|
||||
|
||||
/**
|
||||
* @note destination and source can be the same
|
||||
*/
|
||||
void Decrypt(u8 *destination, u8 *source, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Decrypts data and writes back to it
|
||||
*/
|
||||
inline void Decrypt(std::span<u8> data) {
|
||||
Decrypt(data.data(), data.data(), data.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Decrypts data with XTS. IV will get calculated with the given sector
|
||||
*/
|
||||
void XtsDecrypt(u8 *destination, u8 *source, size_t size, size_t sector, size_t sectorSize);
|
||||
|
||||
/**
|
||||
* @brief Decrypts data with XTS and writes back to it
|
||||
*/
|
||||
inline void XtsDecrypt(std::span<u8> data, size_t sector, size_t sectorSize) {
|
||||
XtsDecrypt(data.data(), data.data(), data.size(), sector, sectorSize);
|
||||
}
|
||||
};
|
||||
}
|
60
app/src/main/cpp/skyline/crypto/key_store.cpp
Normal file
60
app/src/main/cpp/skyline/crypto/key_store.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <functional>
|
||||
#include <vfs/os_filesystem.h>
|
||||
#include "key_store.h"
|
||||
|
||||
namespace skyline::crypto {
|
||||
KeyStore::KeyStore(const std::string &rootPath) {
|
||||
vfs::OsFileSystem root(rootPath);
|
||||
if (root.FileExists("title.keys"))
|
||||
ReadPairs(root.OpenFile("title.keys"), &KeyStore::PopulateTitleKeys);
|
||||
if (root.FileExists("prod.keys"))
|
||||
ReadPairs(root.OpenFile("prod.keys"), &KeyStore::PopulateKeys);
|
||||
}
|
||||
|
||||
void KeyStore::ReadPairs(const std::shared_ptr<vfs::Backing> &backing, ReadPairsCallback callback) {
|
||||
std::vector<char> fileContent(backing->size);
|
||||
backing->Read(fileContent.data(), 0, fileContent.size());
|
||||
|
||||
auto lineStart{fileContent.begin()};
|
||||
std::vector<char>::iterator lineEnd;
|
||||
while ((lineEnd = std::find(lineStart, fileContent.end(), '\n')) != fileContent.end()) {
|
||||
auto keyEnd{std::find(lineStart, lineEnd, '=')};
|
||||
if (keyEnd == lineEnd) {
|
||||
throw exception("Invalid key file");
|
||||
}
|
||||
|
||||
std::string_view key(&*lineStart, keyEnd - lineStart);
|
||||
std::string_view value(&*(keyEnd + 1), lineEnd - keyEnd - 1);
|
||||
(this->*callback)(key, value);
|
||||
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void KeyStore::PopulateTitleKeys(std::string_view keyName, std::string_view value) {
|
||||
Key128 key{util::HexStringToArray<16>(keyName)};
|
||||
Key128 valueArray{util::HexStringToArray<16>(value)};
|
||||
titleKeys.insert({std::move(key), std::move(valueArray)});
|
||||
}
|
||||
|
||||
void KeyStore::PopulateKeys(std::string_view keyName, std::string_view value) {
|
||||
{
|
||||
auto it{key256Names.find(keyName)};
|
||||
if (it != key256Names.end()) {
|
||||
it->second = headerKey = util::HexStringToArray<32>(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyName.size() > 2) {
|
||||
auto it = indexedKey128Names.find(keyName.substr(0, keyName.size() - 2));
|
||||
if (it != indexedKey128Names.end()) {
|
||||
size_t index{std::stoul(std::string(keyName.substr(it->first.size())), nullptr, 16)};
|
||||
it->second[index] = util::HexStringToArray<16>(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
59
app/src/main/cpp/skyline/crypto/key_store.h
Normal file
59
app/src/main/cpp/skyline/crypto/key_store.h
Normal file
@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vfs/backing.h>
|
||||
#include "common.h"
|
||||
|
||||
namespace skyline::crypto {
|
||||
/**
|
||||
* @brief The KeyStore class looks for title.keys and prod.keys files in rootPath
|
||||
* @note Both files are created on kotlin side, prod.keys contains keys that are used to decrypt ROMs and title key, decrypted title keys are used for ctr backing.
|
||||
*/
|
||||
class KeyStore {
|
||||
public:
|
||||
KeyStore(const std::string &rootPath);
|
||||
|
||||
using Key128 = std::array<u8, 16>;
|
||||
using Key256 = std::array<u8, 32>;
|
||||
using IndexedKeys128 = std::array<std::optional<Key128>, 20>;
|
||||
|
||||
std::optional<Key256> headerKey;
|
||||
|
||||
IndexedKeys128 titleKek;
|
||||
IndexedKeys128 areaKeyApplication;
|
||||
IndexedKeys128 areaKeyOcean;
|
||||
IndexedKeys128 areaKeySystem;
|
||||
private:
|
||||
std::map<Key128, Key128> titleKeys;
|
||||
|
||||
std::unordered_map<std::string_view, std::optional<Key256> &> key256Names{
|
||||
{"header_key", headerKey}
|
||||
};
|
||||
|
||||
std::unordered_map<std::string_view, IndexedKeys128 &> indexedKey128Names{
|
||||
{"titlekek_", titleKek},
|
||||
{"key_area_key_application_", areaKeyApplication},
|
||||
{"key_area_key_ocean_", areaKeyOcean},
|
||||
{"key_area_key_system_", areaKeySystem}
|
||||
};
|
||||
|
||||
using ReadPairsCallback = void (skyline::crypto::KeyStore::*)(std::string_view, std::string_view);
|
||||
|
||||
void ReadPairs(const std::shared_ptr<vfs::Backing> &backing, ReadPairsCallback callback);
|
||||
|
||||
void PopulateTitleKeys(std::string_view keyName, std::string_view value);
|
||||
|
||||
void PopulateKeys(std::string_view keyName, std::string_view value);
|
||||
|
||||
public:
|
||||
inline std::optional<Key128> GetTitleKey(const Key128 &title) {
|
||||
auto it{titleKeys.find(title)};
|
||||
if (it == titleKeys.end())
|
||||
return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
};
|
||||
}
|
@ -20,6 +20,29 @@ namespace skyline::loader {
|
||||
NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This enumerates all possible results when parsing ROM files
|
||||
* @note This needs to be synchronized with emu.skyline.loader.LoaderResult
|
||||
*/
|
||||
enum class LoaderResult : int8_t {
|
||||
Success,
|
||||
ParsingError,
|
||||
MissingHeaderKey,
|
||||
MissingTitleKey,
|
||||
MissingTitleKek,
|
||||
MissingKeyArea
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief An exception used specifically for errors related to loaders, it's used to communicate errors to the Kotlin-side of the loader
|
||||
*/
|
||||
class loader_exception : public exception {
|
||||
public:
|
||||
const LoaderResult error;
|
||||
|
||||
loader_exception(LoaderResult error, const std::string &message = "No message") : exception("Loader exception {}: {}", error, message), error(error) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The Loader class provides an abstract interface for ROM loaders
|
||||
*/
|
||||
|
@ -8,7 +8,7 @@
|
||||
#include "nca.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
NcaLoader::NcaLoader(const std::shared_ptr<vfs::Backing> &backing) : nca(backing) {
|
||||
NcaLoader::NcaLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore) : nca(backing, keyStore) {
|
||||
if (nca.exeFs == nullptr)
|
||||
throw exception("Only NCAs with an ExeFS can be loaded directly");
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include <common.h>
|
||||
#include <vfs/nca.h>
|
||||
#include <crypto/key_store.h>
|
||||
#include "loader.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
@ -16,7 +17,7 @@ namespace skyline::loader {
|
||||
vfs::NCA nca; //!< The backing NCA of the loader
|
||||
|
||||
public:
|
||||
NcaLoader(const std::shared_ptr<vfs::Backing> &backing);
|
||||
NcaLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore);
|
||||
|
||||
/**
|
||||
* @brief This loads an ExeFS into memory
|
||||
|
@ -5,20 +5,22 @@
|
||||
#include "nsp.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
NspLoader::NspLoader(const std::shared_ptr<vfs::Backing> &backing) : nsp(std::make_shared<vfs::PartitionFileSystem>(backing)) {
|
||||
auto root = nsp->OpenDirectory("", {false, true});
|
||||
NspLoader::NspLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore) : nsp(std::make_shared<vfs::PartitionFileSystem>(backing)) {
|
||||
auto root{nsp->OpenDirectory("", {false, true})};
|
||||
|
||||
for (const auto &entry : root->Read()) {
|
||||
if (entry.name.substr(entry.name.find_last_of(".") + 1) != "nca")
|
||||
continue;
|
||||
|
||||
try {
|
||||
auto nca = vfs::NCA(nsp->OpenFile(entry.name));
|
||||
auto nca{vfs::NCA(nsp->OpenFile(entry.name), keyStore)};
|
||||
|
||||
if (nca.contentType == vfs::NcaContentType::Program && nca.romFs != nullptr && nca.exeFs != nullptr)
|
||||
programNca = std::move(nca);
|
||||
else if (nca.contentType == vfs::NcaContentType::Control && nca.romFs != nullptr)
|
||||
controlNca = std::move(nca);
|
||||
} catch (const loader_exception &e) {
|
||||
throw loader_exception(e.error);
|
||||
} catch (const std::exception &e) {
|
||||
continue;
|
||||
}
|
||||
@ -40,7 +42,7 @@ namespace skyline::loader {
|
||||
if (romFs == nullptr)
|
||||
return std::vector<u8>();
|
||||
|
||||
auto root = controlRomFs->OpenDirectory("", {false, true});
|
||||
auto root{controlRomFs->OpenDirectory("", {false, true})};
|
||||
std::shared_ptr<vfs::Backing> icon;
|
||||
|
||||
// Use the first icon file available
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <vfs/nca.h>
|
||||
#include <vfs/rom_filesystem.h>
|
||||
#include <vfs/partition_filesystem.h>
|
||||
#include <crypto/key_store.h>
|
||||
#include "loader.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
@ -21,7 +22,7 @@ namespace skyline::loader {
|
||||
std::optional<vfs::NCA> controlNca; //!< The main control NCA within the NSP
|
||||
|
||||
public:
|
||||
NspLoader(const std::shared_ptr<vfs::Backing> &backing);
|
||||
NspLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore);
|
||||
|
||||
std::vector<u8> GetIcon();
|
||||
|
||||
|
@ -13,16 +13,17 @@ namespace skyline::kernel {
|
||||
OS::OS(std::shared_ptr<JvmManager> &jvmManager, std::shared_ptr<Logger> &logger, std::shared_ptr<Settings> &settings, const std::string &appFilesPath) : state(this, process, jvmManager, settings, logger), memory(state), serviceManager(state), appFilesPath(appFilesPath) {}
|
||||
|
||||
void OS::Execute(int romFd, loader::RomFormat romType) {
|
||||
auto romFile = std::make_shared<vfs::OsBacking>(romFd);
|
||||
auto romFile{std::make_shared<vfs::OsBacking>(romFd)};
|
||||
auto keyStore{std::make_shared<crypto::KeyStore>(appFilesPath)};
|
||||
|
||||
if (romType == loader::RomFormat::NRO) {
|
||||
state.loader = std::make_shared<loader::NroLoader>(romFile);
|
||||
} else if (romType == loader::RomFormat::NSO) {
|
||||
state.loader = std::make_shared<loader::NsoLoader>(romFile);
|
||||
} else if (romType == loader::RomFormat::NCA) {
|
||||
state.loader = std::make_shared<loader::NcaLoader>(romFile);
|
||||
state.loader = std::make_shared<loader::NcaLoader>(romFile, keyStore);
|
||||
} else if (romType == loader::RomFormat::NSP) {
|
||||
state.loader = std::make_shared<loader::NspLoader>(romFile);
|
||||
state.loader = std::make_shared<loader::NspLoader>(romFile, keyStore);
|
||||
} else {
|
||||
throw exception("Unsupported ROM extension.");
|
||||
}
|
||||
@ -36,16 +37,16 @@ namespace skyline::kernel {
|
||||
}
|
||||
|
||||
std::shared_ptr<type::KProcess> OS::CreateProcess(u64 entry, u64 argument, size_t stackSize) {
|
||||
auto stack = std::make_shared<type::KSharedMemory>(state, memory.stack.address, stackSize, memory::Permission{true, true, false}, memory::states::Stack, MAP_NORESERVE | MAP_STACK, true);
|
||||
auto stack{std::make_shared<type::KSharedMemory>(state, memory.stack.address, stackSize, memory::Permission{true, true, false}, memory::states::Stack, MAP_NORESERVE | MAP_STACK, true)};
|
||||
stack->guest = stack->kernel;
|
||||
|
||||
if (mprotect(reinterpret_cast<void *>(stack->guest.address), PAGE_SIZE, PROT_NONE))
|
||||
throw exception("Failed to create guard pages");
|
||||
|
||||
auto tlsMem = std::make_shared<type::KSharedMemory>(state, 0, (sizeof(ThreadContext) + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1), memory::Permission{true, true, false}, memory::states::Reserved);
|
||||
auto tlsMem{std::make_shared<type::KSharedMemory>(state, 0, (sizeof(ThreadContext) + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1), memory::Permission{true, true, false}, memory::states::Reserved)};
|
||||
tlsMem->guest = tlsMem->kernel;
|
||||
|
||||
auto pid = clone(reinterpret_cast<int (*)(void *)>(&guest::GuestEntry), reinterpret_cast<void *>(stack->guest.address + stackSize), CLONE_FILES | CLONE_FS | CLONE_SETTLS | SIGCHLD, reinterpret_cast<void *>(entry), nullptr, reinterpret_cast<void *>(tlsMem->guest.address));
|
||||
auto pid{clone(reinterpret_cast<int (*)(void *)>(&guest::GuestEntry), reinterpret_cast<void *>(stack->guest.address + stackSize), CLONE_FILES | CLONE_FS | CLONE_SETTLS | SIGCHLD, reinterpret_cast<void *>(entry), nullptr, reinterpret_cast<void *>(tlsMem->guest.address))};
|
||||
if (pid == -1)
|
||||
throw exception("Call to clone() has failed: {}", strerror(errno));
|
||||
|
||||
|
48
app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.cpp
Normal file
48
app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include "ctr_encrypted_backing.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
constexpr size_t SectorSize = 0x10;
|
||||
|
||||
CtrEncryptedBacking::CtrEncryptedBacking(crypto::KeyStore::Key128 &ctr, crypto::KeyStore::Key128 &key, const std::shared_ptr<Backing> &backing, size_t baseOffset) : Backing({true, false, false}), ctr(ctr), cipher(key, MBEDTLS_CIPHER_AES_128_CTR), backing(backing), baseOffset(baseOffset) {}
|
||||
|
||||
void CtrEncryptedBacking::UpdateCtr(u64 offset) {
|
||||
offset >>= 4;
|
||||
size_t le{__builtin_bswap64(offset)};
|
||||
std::memcpy(ctr.data() + 8, &le, 8);
|
||||
cipher.SetIV(ctr);
|
||||
}
|
||||
|
||||
size_t CtrEncryptedBacking::Read(u8 *output, size_t offset, size_t size) {
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
size_t sectorOffset{offset % SectorSize};
|
||||
if (sectorOffset == 0) {
|
||||
UpdateCtr(baseOffset + offset);
|
||||
size_t read{backing->Read(output, offset, size)};
|
||||
if (read != size)
|
||||
return 0;
|
||||
cipher.Decrypt({output, size});
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t sectorStart{offset - sectorOffset};
|
||||
std::vector<u8> blockBuf(SectorSize);
|
||||
size_t read{backing->Read(blockBuf.data(), sectorStart, SectorSize)};
|
||||
if (read != SectorSize)
|
||||
return 0;
|
||||
UpdateCtr(baseOffset + sectorStart);
|
||||
cipher.Decrypt(blockBuf);
|
||||
if (size + sectorOffset < SectorSize) {
|
||||
std::memcpy(output, blockBuf.data() + sectorOffset, size);
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t readInBlock{SectorSize - sectorOffset};
|
||||
std::memcpy(output, blockBuf.data() + sectorOffset, readInBlock);
|
||||
return readInBlock + Read(output + readInBlock, offset + readInBlock, size - readInBlock);
|
||||
}
|
||||
}
|
37
app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.h
Normal file
37
app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.h
Normal file
@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <crypto/aes_cipher.h>
|
||||
#include <crypto/key_store.h>
|
||||
#include "backing.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
/**
|
||||
* @brief This backing is used to decrypt AES-CTR data
|
||||
*/
|
||||
class CtrEncryptedBacking : public Backing {
|
||||
private:
|
||||
crypto::KeyStore::Key128 ctr;
|
||||
|
||||
crypto::AesCipher cipher;
|
||||
|
||||
std::shared_ptr<Backing> backing;
|
||||
|
||||
/**
|
||||
* @brief Offset of file is used to calculate the IV
|
||||
*/
|
||||
size_t baseOffset;
|
||||
|
||||
/**
|
||||
* @brief Calculates IV based on the offset
|
||||
*/
|
||||
void UpdateCtr(u64 offset);
|
||||
|
||||
public:
|
||||
CtrEncryptedBacking(crypto::KeyStore::Key128 &ctr, crypto::KeyStore::Key128 &key, const std::shared_ptr<Backing> &backing, size_t baseOffset);
|
||||
|
||||
size_t Read(u8 *output, size_t offset, size_t size) override;
|
||||
};
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <crypto/aes_cipher.h>
|
||||
#include <loader/loader.h>
|
||||
#include "ctr_encrypted_backing.h"
|
||||
#include "region_backing.h"
|
||||
#include "partition_filesystem.h"
|
||||
#include "nca.h"
|
||||
@ -8,17 +11,31 @@
|
||||
#include "directory.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
NCA::NCA(const std::shared_ptr<vfs::Backing> &backing) : backing(backing) {
|
||||
using namespace loader;
|
||||
|
||||
NCA::NCA(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore) : backing(backing), keyStore(keyStore) {
|
||||
backing->Read(&header);
|
||||
|
||||
if (header.magic != util::MakeMagic<u32>("NCA3"))
|
||||
throw exception("Attempting to load an encrypted or invalid NCA");
|
||||
if (header.magic != util::MakeMagic<u32>("NCA3")) {
|
||||
if (!keyStore->headerKey)
|
||||
throw loader_exception(LoaderResult::MissingHeaderKey);
|
||||
|
||||
crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS);
|
||||
|
||||
cipher.XtsDecrypt({reinterpret_cast<u8 *>(&header), sizeof(NcaHeader)}, 0, 0x200);
|
||||
|
||||
// Check if decryption was successful
|
||||
if (header.magic != util::MakeMagic<u32>("NCA3"))
|
||||
throw loader_exception(LoaderResult::ParsingError);
|
||||
encrypted = true;
|
||||
}
|
||||
|
||||
contentType = header.contentType;
|
||||
rightsIdEmpty = header.rightsId == crypto::KeyStore::Key128{};
|
||||
|
||||
for (size_t i = 0; i < header.sectionHeaders.size(); i++) {
|
||||
auto §ionHeader = header.sectionHeaders.at(i);
|
||||
auto §ionEntry = header.fsEntries.at(i);
|
||||
for (size_t i{}; i < header.sectionHeaders.size(); i++) {
|
||||
auto §ionHeader{header.sectionHeaders.at(i)};
|
||||
auto §ionEntry{header.fsEntries.at(i)};
|
||||
|
||||
if (sectionHeader.fsType == NcaSectionFsType::PFS0 && sectionHeader.hashType == NcaSectionHashType::HierarchicalSha256)
|
||||
ReadPfs0(sectionHeader, sectionEntry);
|
||||
@ -27,11 +44,11 @@ namespace skyline::vfs {
|
||||
}
|
||||
}
|
||||
|
||||
void NCA::ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry) {
|
||||
size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.sha256HashInfo.pfs0Offset;
|
||||
size_t size = constant::MediaUnitSize * static_cast<size_t>(entry.endOffset - entry.startOffset);
|
||||
void NCA::ReadPfs0(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry) {
|
||||
size_t offset{static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + sectionHeader.sha256HashInfo.pfs0Offset};
|
||||
size_t size{constant::MediaUnitSize * static_cast<size_t>(entry.endOffset - entry.startOffset)};
|
||||
|
||||
auto pfs = std::make_shared<PartitionFileSystem>(std::make_shared<RegionBacking>(backing, offset, size));
|
||||
auto pfs{std::make_shared<PartitionFileSystem>(CreateBacking(sectionHeader, std::make_shared<RegionBacking>(backing, offset, size), offset))};
|
||||
|
||||
if (contentType == NcaContentType::Program) {
|
||||
// An ExeFS must always contain an NPDM and a main NSO, whereas the logo section will always contain a logo and a startup movie
|
||||
@ -44,10 +61,95 @@ namespace skyline::vfs {
|
||||
}
|
||||
}
|
||||
|
||||
void NCA::ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry) {
|
||||
size_t offset = static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + header.integrityHashInfo.levels.back().offset;
|
||||
size_t size = header.integrityHashInfo.levels.back().size;
|
||||
void NCA::ReadRomFs(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry) {
|
||||
size_t offset{static_cast<size_t>(entry.startOffset) * constant::MediaUnitSize + sectionHeader.integrityHashInfo.levels.back().offset};
|
||||
size_t size{sectionHeader.integrityHashInfo.levels.back().size};
|
||||
|
||||
romFs = std::make_shared<RegionBacking>(backing, offset, size);
|
||||
romFs = CreateBacking(sectionHeader, std::make_shared<RegionBacking>(backing, offset, size), offset);
|
||||
}
|
||||
|
||||
std::shared_ptr<Backing> NCA::CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr<Backing> rawBacking, size_t offset) {
|
||||
if (!encrypted)
|
||||
return rawBacking;
|
||||
|
||||
switch (sectionHeader.encryptionType) {
|
||||
case NcaSectionEncryptionType::None:
|
||||
return rawBacking;
|
||||
case NcaSectionEncryptionType::CTR:
|
||||
case NcaSectionEncryptionType::BKTR: {
|
||||
auto key{!rightsIdEmpty ? GetTitleKey() : GetKeyAreaKey(sectionHeader.encryptionType)};
|
||||
|
||||
std::array<u8, 0x10> ctr{};
|
||||
u32 secureValueLE{__builtin_bswap32(sectionHeader.secureValue)};
|
||||
u32 generationLE{__builtin_bswap32(sectionHeader.generation)};
|
||||
std::memcpy(ctr.data(), &secureValueLE, 4);
|
||||
std::memcpy(ctr.data() + 4, &generationLE, 4);
|
||||
|
||||
return std::make_shared<CtrEncryptedBacking>(ctr, key, std::move(rawBacking), offset);
|
||||
}
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
u8 NCA::GetKeyGeneration() {
|
||||
u8 legacyGen{static_cast<u8>(header.legacyKeyGenerationType)};
|
||||
u8 gen{static_cast<u8>(header.keyGenerationType)};
|
||||
gen = std::max<u8>(legacyGen, gen);
|
||||
return gen > 0 ? gen - 1 : gen;
|
||||
}
|
||||
|
||||
crypto::KeyStore::Key128 NCA::GetTitleKey() {
|
||||
u8 keyGeneration{GetKeyGeneration()};
|
||||
|
||||
auto titleKey{keyStore->GetTitleKey(header.rightsId)};
|
||||
auto &titleKek{keyStore->titleKek[keyGeneration]};
|
||||
|
||||
if (!titleKey)
|
||||
throw loader_exception(LoaderResult::MissingTitleKey);
|
||||
if (!titleKek)
|
||||
throw loader_exception(LoaderResult::MissingTitleKek);
|
||||
|
||||
crypto::AesCipher cipher(*titleKek, MBEDTLS_CIPHER_AES_128_ECB);
|
||||
cipher.Decrypt(*titleKey);
|
||||
return *titleKey;
|
||||
}
|
||||
|
||||
crypto::KeyStore::Key128 NCA::GetKeyAreaKey(NCA::NcaSectionEncryptionType type) {
|
||||
auto keyArea{[&](crypto::KeyStore::IndexedKeys128 &keys) {
|
||||
u8 keyGeneration{GetKeyGeneration()};
|
||||
|
||||
auto &keyArea{keys[keyGeneration]};
|
||||
|
||||
if (!keyArea)
|
||||
throw loader_exception(LoaderResult::MissingKeyArea);
|
||||
|
||||
size_t keyAreaIndex;
|
||||
switch (type) {
|
||||
case NcaSectionEncryptionType::XTS:
|
||||
keyAreaIndex = 0;
|
||||
break;
|
||||
case NcaSectionEncryptionType::CTR:
|
||||
case NcaSectionEncryptionType::BKTR:
|
||||
keyAreaIndex = 2;
|
||||
break;
|
||||
default:
|
||||
throw exception("Unsupported NcaSectionEncryptionType");
|
||||
}
|
||||
|
||||
crypto::KeyStore::Key128 decryptedKeyArea;
|
||||
crypto::AesCipher cipher(*keyArea, MBEDTLS_CIPHER_AES_128_ECB);
|
||||
cipher.Decrypt(decryptedKeyArea.data(), header.encryptedKeyArea[keyAreaIndex].data(), decryptedKeyArea.size());
|
||||
return decryptedKeyArea;
|
||||
}};
|
||||
|
||||
switch (header.keyAreaEncryptionKeyType) {
|
||||
case NcaKeyAreaEncryptionKeyType::Application:
|
||||
return keyArea(keyStore->areaKeyApplication);
|
||||
case NcaKeyAreaEncryptionKeyType::Ocean:
|
||||
return keyArea(keyStore->areaKeyOcean);
|
||||
case NcaKeyAreaEncryptionKeyType::System:
|
||||
return keyArea(keyStore->areaKeySystem);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <crypto/key_store.h>
|
||||
#include <crypto/aes_cipher.h>
|
||||
#include "filesystem.h"
|
||||
|
||||
namespace skyline {
|
||||
@ -195,10 +197,21 @@ namespace skyline {
|
||||
static_assert(sizeof(NcaHeader) == 0xC00);
|
||||
|
||||
std::shared_ptr<Backing> backing; //!< The backing for the NCA
|
||||
std::shared_ptr<crypto::KeyStore> keyStore;
|
||||
bool encrypted{false};
|
||||
bool rightsIdEmpty;
|
||||
|
||||
void ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry);
|
||||
void ReadPfs0(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry);
|
||||
|
||||
void ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry);
|
||||
void ReadRomFs(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry);
|
||||
|
||||
std::shared_ptr<Backing> CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr<Backing> rawBacking, size_t offset);
|
||||
|
||||
u8 GetKeyGeneration();
|
||||
|
||||
crypto::KeyStore::Key128 GetTitleKey();
|
||||
|
||||
crypto::KeyStore::Key128 GetKeyAreaKey(NcaSectionEncryptionType type);
|
||||
|
||||
public:
|
||||
std::shared_ptr<FileSystem> exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section
|
||||
@ -207,7 +220,7 @@ namespace skyline {
|
||||
std::shared_ptr<Backing> romFs; //!< The backing for this NCA's RomFS section
|
||||
NcaContentType contentType; //!< The content type of the NCA
|
||||
|
||||
NCA(const std::shared_ptr<vfs::Backing> &backing);
|
||||
NCA(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore);
|
||||
};
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
#include "os_filesystem.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
OsFileSystem::OsFileSystem(const std::string &basePath) : FileSystem(), basePath(std::move(basePath)) {
|
||||
OsFileSystem::OsFileSystem(const std::string &basePath) : FileSystem(), basePath(basePath) {
|
||||
if (!DirectoryExists(basePath))
|
||||
if (!CreateDirectory(basePath, true))
|
||||
throw exception("Error creating the OS filesystem backing directory");
|
||||
|
@ -19,12 +19,13 @@ namespace skyline::vfs {
|
||||
size_t stringTableOffset = sizeof(FsHeader) + (header.numFiles * entrySize);
|
||||
fileDataOffset = stringTableOffset + header.stringTableSize;
|
||||
|
||||
std::vector<char> stringTable(header.stringTableSize);
|
||||
std::vector<char> stringTable(header.stringTableSize + 1);
|
||||
backing->Read(stringTable.data(), stringTableOffset, header.stringTableSize);
|
||||
stringTable[header.stringTableSize] = 0;
|
||||
|
||||
for (u32 i = 0; i < header.numFiles; i++) {
|
||||
PartitionFileEntry entry{};
|
||||
backing->Read(&entry, sizeof(FsHeader) + i * entrySize);
|
||||
for (u32 entryOffset{sizeof(FsHeader)}; entryOffset < header.numFiles * entrySize; entryOffset += entrySize) {
|
||||
PartitionFileEntry entry;
|
||||
backing->Read(&entry, entryOffset);
|
||||
|
||||
std::string name(&stringTable[entry.stringTableOffset]);
|
||||
fileMap.emplace(name, std::move(entry));
|
||||
|
@ -12,7 +12,7 @@ namespace skyline::vfs {
|
||||
class RegionBacking : public Backing {
|
||||
private:
|
||||
std::shared_ptr<vfs::Backing> backing; //!< The parent backing
|
||||
size_t offset; //!< The offset of the region in the parent backing
|
||||
size_t baseOffset; //!< The offset of the region in the parent backing
|
||||
|
||||
public:
|
||||
/**
|
||||
@ -20,7 +20,7 @@ namespace skyline::vfs {
|
||||
* @param offset The offset of the region start within the parent backing
|
||||
* @param size The size of the region in the parent backing
|
||||
*/
|
||||
RegionBacking(const std::shared_ptr<vfs::Backing> &backing, size_t offset, size_t size, Mode mode = {true, false, false}) : Backing(mode, size), backing(backing), offset(offset) {};
|
||||
RegionBacking(const std::shared_ptr<vfs::Backing> &backing, size_t offset, size_t size, Mode mode = {true, false, false}) : Backing(mode, size), backing(backing), baseOffset(offset) {};
|
||||
|
||||
inline size_t Read(u8 *output, size_t offset, size_t size) {
|
||||
if (!mode.read)
|
||||
@ -28,7 +28,7 @@ namespace skyline::vfs {
|
||||
|
||||
size = std::min(offset + size, this->size) - offset;
|
||||
|
||||
return backing->Read(output, this->offset + offset, size);
|
||||
return backing->Read(output, baseOffset + offset, size);
|
||||
}
|
||||
};
|
||||
}
|
@ -20,14 +20,28 @@ import androidx.core.graphics.drawable.toBitmap
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import kotlinx.android.synthetic.main.app_dialog.*
|
||||
|
||||
/**
|
||||
* This dialog is used to show extra game metadata and provide extra options such as pinning the game to the home screen
|
||||
*
|
||||
* @param item This is used to hold the [AppItem] between instances
|
||||
*/
|
||||
class AppDialog(val item : AppItem) : BottomSheetDialogFragment() {
|
||||
class AppDialog : BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
/**
|
||||
* @param item This is used to hold the [AppItem] between instances
|
||||
*/
|
||||
fun newInstance(item : AppItem) : AppDialog {
|
||||
val args = Bundle()
|
||||
args.putSerializable("item", item)
|
||||
|
||||
val fragment = AppDialog()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var item : AppItem
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
@ -36,6 +50,12 @@ class AppDialog(val item : AppItem) : BottomSheetDialogFragment() {
|
||||
return requireActivity().layoutInflater.inflate(R.layout.app_dialog, container)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
item = arguments!!.getSerializable("item") as AppItem
|
||||
}
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible and map the B button to back
|
||||
*/
|
||||
@ -64,13 +84,11 @@ class AppDialog(val item : AppItem) : BottomSheetDialogFragment() {
|
||||
|
||||
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
game_title.text = item.title
|
||||
game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing)
|
||||
game_subtitle.text = item.subTitle ?: item.loaderResultString(requireContext())
|
||||
|
||||
game_play.isEnabled = item.loaderResult == LoaderResult.Success
|
||||
game_play.setOnClickListener {
|
||||
val intent = Intent(activity, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
|
||||
startActivity(intent)
|
||||
startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri })
|
||||
}
|
||||
|
||||
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
|
||||
|
@ -21,6 +21,10 @@ import java.io.File
|
||||
import kotlin.math.abs
|
||||
|
||||
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
||||
companion object {
|
||||
private val Tag = EmulationActivity::class.java.name
|
||||
}
|
||||
|
||||
init {
|
||||
System.loadLibrary("skyline") // libskyline.so
|
||||
}
|
||||
@ -287,7 +291,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
* This sets [surface] to [holder].surface and passes it into libskyline
|
||||
*/
|
||||
override fun surfaceCreated(holder : SurfaceHolder) {
|
||||
Log.d("surfaceCreated", "Holder: $holder")
|
||||
Log.d(Tag, "surfaceCreated Holder: $holder")
|
||||
surface = holder.surface
|
||||
setSurface(surface)
|
||||
surfaceReady.open()
|
||||
@ -297,14 +301,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
* This is purely used for debugging surface changes
|
||||
*/
|
||||
override fun surfaceChanged(holder : SurfaceHolder, format : Int, width : Int, height : Int) {
|
||||
Log.d("surfaceChanged", "Holder: $holder, Format: $format, Width: $width, Height: $height")
|
||||
Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height")
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets [surface] to null and passes it into libskyline
|
||||
*/
|
||||
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
||||
Log.d("surfaceDestroyed", "Holder: $holder")
|
||||
Log.d(Tag, "surfaceDestroyed Holder: $holder")
|
||||
surfaceReady.close()
|
||||
surface = null
|
||||
setSurface(surface)
|
||||
|
86
app/src/main/java/emu/skyline/KeyReader.kt
Normal file
86
app/src/main/java/emu/skyline/KeyReader.kt
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
|
||||
object KeyReader {
|
||||
private val Tag = KeyReader::class.java.simpleName
|
||||
|
||||
enum class KeyType(val keyName : String, val fileName : String) {
|
||||
Title("title_keys", "title.keys"), Prod("prod_keys", "prod.keys");
|
||||
|
||||
companion object {
|
||||
fun parse(keyName : String) = values().first { it.keyName == keyName }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads keys file, trims and writes to internal app data storage, it makes sure file is properly formatted
|
||||
*/
|
||||
fun import(context : Context, uri : Uri, keyType : KeyType) : Boolean {
|
||||
Log.i(Tag, "Parsing ${keyType.name}")
|
||||
|
||||
if (!DocumentFile.isDocumentUri(context, uri))
|
||||
return false
|
||||
|
||||
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
|
||||
if ("keys" != fileName?.substringAfterLast('.')) return false
|
||||
|
||||
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
|
||||
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val outputStream = tmpOutputFile.bufferedWriter()
|
||||
|
||||
val valid = inputStream!!.bufferedReader().useLines {
|
||||
for (line in it) {
|
||||
val pair = line.split("=")
|
||||
if (pair.size != 2)
|
||||
return@useLines false
|
||||
|
||||
val key = pair[0].trim()
|
||||
val value = pair[1].trim()
|
||||
when (keyType) {
|
||||
KeyType.Title -> {
|
||||
if (key.length != 32 && !isHexString(key))
|
||||
return@useLines false
|
||||
if (value.length != 32 && !isHexString(value))
|
||||
return@useLines false
|
||||
}
|
||||
KeyType.Prod -> {
|
||||
if (!key.contains("_"))
|
||||
return@useLines false
|
||||
if (!isHexString(value))
|
||||
return@useLines false
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.append("$key=$value\n")
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
if (valid)
|
||||
tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
|
||||
return valid
|
||||
}
|
||||
|
||||
private fun isHexString(str : String) : Boolean {
|
||||
for (c in str) {
|
||||
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -30,16 +30,18 @@ import emu.skyline.adapter.AppAdapter
|
||||
import emu.skyline.adapter.GridLayoutSpan
|
||||
import emu.skyline.adapter.LayoutType
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.RomFormat
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.ceil
|
||||
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
/**
|
||||
* This is used to get/set shared preferences
|
||||
*/
|
||||
@ -50,8 +52,10 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
*/
|
||||
private lateinit var adapter : AppAdapter
|
||||
|
||||
private var reloading = AtomicBoolean()
|
||||
|
||||
/**
|
||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata
|
||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
|
||||
*/
|
||||
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
|
||||
var foundCurrent = found
|
||||
@ -61,25 +65,16 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
|
||||
} else {
|
||||
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
|
||||
val romFd = contentResolver.openFileDescriptor(file.uri, "r")!!
|
||||
val romFile = RomFile(this, romFormat, romFd)
|
||||
RomFile(this, romFormat, file.uri).let { romFile ->
|
||||
val finalFoundCurrent = foundCurrent
|
||||
runOnUiThread {
|
||||
if (!finalFoundCurrent) adapter.addHeader(romFormat.name)
|
||||
|
||||
if (romFile.valid()) {
|
||||
romFile.use {
|
||||
val entry = romFile.getAppEntry(file.uri)
|
||||
|
||||
val finalFoundCurrent = foundCurrent
|
||||
runOnUiThread {
|
||||
if (!finalFoundCurrent) adapter.addHeader(romFormat.name)
|
||||
|
||||
adapter.addItem(AppItem(entry))
|
||||
}
|
||||
|
||||
foundCurrent = true
|
||||
adapter.addItem(AppItem(romFile.appEntry))
|
||||
}
|
||||
}
|
||||
|
||||
romFd.close()
|
||||
foundCurrent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,6 +97,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
}
|
||||
}
|
||||
|
||||
if (reloading.getAndSet(true)) return
|
||||
thread(start = true) {
|
||||
val snackbar = Snackbar.make(coordinatorLayout, getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE)
|
||||
runOnUiThread {
|
||||
@ -148,6 +144,8 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
snackbar.dismiss()
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
reloading.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,10 +169,18 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
|
||||
})
|
||||
|
||||
refresh_fab.setOnClickListener(this)
|
||||
settings_fab.setOnClickListener(this)
|
||||
open_fab.setOnClickListener(this)
|
||||
log_fab.setOnClickListener(this)
|
||||
refresh_fab.setOnClickListener { refreshAdapter(false) }
|
||||
|
||||
settings_fab.setOnClickListener { startActivityForResult(Intent(this, SettingsActivity::class.java), 3) }
|
||||
|
||||
open_fab.setOnClickListener { startActivity(Intent(this, LogActivity::class.java)) }
|
||||
|
||||
log_fab.setOnClickListener {
|
||||
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}, 2)
|
||||
}
|
||||
|
||||
setupAppList()
|
||||
|
||||
@ -217,7 +223,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
|
||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
|
||||
adapter = AppAdapter(layoutType = layoutType, gridSpan = gridSpan, onClick = selectStartGame, onLongClick = selectShowGameDialog)
|
||||
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||
app_list.adapter = adapter
|
||||
|
||||
app_list.layoutManager = when (layoutType) {
|
||||
@ -263,37 +269,15 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles on-click interaction with [R.id.refresh_fab], [R.id.settings_fab], [R.id.log_fab], [R.id.open_fab]
|
||||
*/
|
||||
override fun onClick(view : View) {
|
||||
when (view.id) {
|
||||
R.id.refresh_fab -> refreshAdapter(false)
|
||||
|
||||
R.id.settings_fab -> startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
|
||||
|
||||
R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java))
|
||||
|
||||
R.id.open_fab -> {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "*/*"
|
||||
|
||||
startActivityForResult(intent, 2)
|
||||
}
|
||||
}
|
||||
private fun selectStartGame(appItem : AppItem) {
|
||||
if (sharedPreferences.getBoolean("select_action", false))
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
else if (appItem.loaderResult == LoaderResult.Success)
|
||||
startActivity(Intent(this, EmulationActivity::class.java).apply { data = appItem.uri })
|
||||
}
|
||||
|
||||
private val selectStartGame : (appItem : AppItem) -> Unit = {
|
||||
if (sharedPreferences.getBoolean("select_action", false)) {
|
||||
AppDialog(it).show(supportFragmentManager, "game")
|
||||
} else {
|
||||
startActivity(Intent(this, EmulationActivity::class.java).apply { data = it.uri })
|
||||
}
|
||||
}
|
||||
|
||||
private val selectShowGameDialog : (appItem : AppItem) -> Unit = {
|
||||
AppDialog(it).show(supportFragmentManager, "game")
|
||||
private fun selectShowGameDialog(appItem : AppItem) {
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -363,8 +347,8 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
|
||||
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)
|
||||
when (layoutType) {
|
||||
LayoutType.List -> app_list.setPadding(0, 0, 0, 0)
|
||||
LayoutType.Grid, LayoutType.GridCompact -> app_list.setPadding(gridCardMagin, 0, gridCardMagin, 0)
|
||||
LayoutType.List -> app_list.post { app_list.setPadding(0, 0, 0, fab_parent.height) }
|
||||
LayoutType.Grid, LayoutType.GridCompact -> app_list.post { app_list.setPadding(gridCardMagin, 0, gridCardMagin, fab_parent.height) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,13 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.preference.ActivityResultDelegate
|
||||
import emu.skyline.preference.ControllerPreference
|
||||
import emu.skyline.preference.DocumentActivity
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
@ -26,11 +29,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
*/
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* The key of the element to force a refresh when [onActivityResult] is called
|
||||
*/
|
||||
var refreshKey : String? = null
|
||||
|
||||
/**
|
||||
* This initializes all of the elements in the activity and displays the settings fragment
|
||||
*/
|
||||
@ -51,30 +49,27 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to refresh the preferences after [emu.skyline.preference.FolderActivity] or [emu.skyline.input.ControllerActivity] has returned
|
||||
* This is used to refresh the preferences after [DocumentActivity] or [emu.skyline.input.ControllerActivity] has returned
|
||||
*/
|
||||
public override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
refreshKey?.let {
|
||||
inputManager.syncObjects()
|
||||
preferenceFragment.refreshPreference(refreshKey!!)
|
||||
preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
|
||||
|
||||
refreshKey = null
|
||||
}
|
||||
settings
|
||||
}
|
||||
|
||||
/**
|
||||
* This fragment is used to display all of the preferences and handle refreshing the preferences
|
||||
* This fragment is used to display all of the preferences
|
||||
*/
|
||||
class PreferenceFragment : PreferenceFragmentCompat() {
|
||||
private var requestCodeCounter = 0
|
||||
|
||||
/**
|
||||
* This forces refreshing a certain preference by indirectly calling [Preference.notifyChanged]
|
||||
* Delegates activity result to all preferences which implement [ActivityResultDelegate]
|
||||
*/
|
||||
fun refreshPreference(key : String) {
|
||||
val preference = preferenceManager.findPreference<Preference>(key)!!
|
||||
preference.isSelectable = !preference.isSelectable
|
||||
preference.isSelectable = !preference.isSelectable
|
||||
fun delegateActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
preferenceScreen.delegateActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,6 +77,25 @@ class SettingsActivity : AppCompatActivity() {
|
||||
*/
|
||||
override fun onCreatePreferences(savedInstanceState : Bundle?, rootKey : String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
preferenceScreen.assignActivityRequestCode()
|
||||
}
|
||||
|
||||
private fun PreferenceGroup.assignActivityRequestCode() {
|
||||
for (i in 0 until preferenceCount) {
|
||||
when (val pref = getPreference(i)) {
|
||||
is PreferenceGroup -> pref.assignActivityRequestCode()
|
||||
is ActivityResultDelegate -> pref.requestCode = requestCodeCounter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PreferenceGroup.delegateActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
for (i in 0 until preferenceCount) {
|
||||
when (val pref = getPreference(i)) {
|
||||
is PreferenceGroup -> pref.delegateActivityResult(requestCode, resultCode, data)
|
||||
is ActivityResultDelegate -> pref.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,9 @@ private typealias InteractionFunction = (appItem : AppItem) -> Unit
|
||||
/**
|
||||
* This adapter is used to display all found applications using their metadata
|
||||
*/
|
||||
internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : Int, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
internal class AppAdapter(val layoutType : LayoutType, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
|
||||
private lateinit var context : Context
|
||||
private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) }
|
||||
private val missingString by lazy { context.getString(R.string.metadata_missing) }
|
||||
|
||||
/**
|
||||
* This adds a header to the view with the contents of [string]
|
||||
@ -105,7 +104,7 @@ internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : In
|
||||
|
||||
if (item is AppItem && holder is ItemViewHolder) {
|
||||
holder.title.text = item.title
|
||||
holder.subtitle.text = item.subTitle ?: missingString
|
||||
holder.subtitle.text = item.subTitle ?: item.loaderResultString(holder.subtitle.context)
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
|
@ -5,9 +5,12 @@
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import emu.skyline.R
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.loader.LoaderResult
|
||||
|
||||
/**
|
||||
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
|
||||
@ -43,6 +46,20 @@ class AppItem(val meta : AppEntry) : BaseItem() {
|
||||
private val type : String
|
||||
get() = meta.format.name
|
||||
|
||||
val loaderResult get() = meta.loaderResult
|
||||
|
||||
fun loaderResultString(context : Context) = context.getString(when (meta.loaderResult) {
|
||||
LoaderResult.Success -> R.string.metadata_missing
|
||||
|
||||
LoaderResult.ParsingError -> R.string.invalid_file
|
||||
|
||||
LoaderResult.MissingTitleKey -> R.string.missing_title_key
|
||||
|
||||
LoaderResult.MissingHeaderKey,
|
||||
LoaderResult.MissingTitleKek,
|
||||
LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys
|
||||
})
|
||||
|
||||
/**
|
||||
* The name and author is used as the key
|
||||
*/
|
||||
|
@ -61,23 +61,19 @@ class InputManager constructor(val context : Context) {
|
||||
* This function syncs the class with data from [file]
|
||||
*/
|
||||
fun syncObjects() {
|
||||
val fileInput = FileInputStream(file)
|
||||
val objectInput = ObjectInputStream(fileInput)
|
||||
ObjectInputStream(FileInputStream(file)).use {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
controllers = it.readObject() as HashMap<Int, Controller>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
controllers = objectInput.readObject() as HashMap<Int, Controller>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
eventMap = objectInput.readObject() as HashMap<HostEvent?, GuestEvent?>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
eventMap = it.readObject() as HashMap<HostEvent?, GuestEvent?>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function syncs [file] with data from the class and eliminates unused value from the map
|
||||
*/
|
||||
fun syncFile() {
|
||||
val fileOutput = FileOutputStream(file)
|
||||
val objectOutput = ObjectOutputStream(fileOutput)
|
||||
|
||||
for (controller in controllers.values) {
|
||||
for (button in ButtonId.values()) {
|
||||
if (button != ButtonId.Menu && !(controller.type.buttons.contains(button) || controller.type.sticks.any { it.button == button })) {
|
||||
@ -100,9 +96,11 @@ class InputManager constructor(val context : Context) {
|
||||
}
|
||||
}
|
||||
|
||||
objectOutput.writeObject(controllers)
|
||||
objectOutput.writeObject(eventMap)
|
||||
ObjectOutputStream(FileOutputStream(file)).use {
|
||||
it.writeObject(controllers)
|
||||
it.writeObject(eventMap)
|
||||
|
||||
objectOutput.flush()
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,7 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
@ -46,166 +41,81 @@ fun getRomFormat(uri : Uri, contentResolver : ContentResolver) : RomFormat {
|
||||
return RomFormat.valueOf(uriStr.substring(uriStr.lastIndexOf(".") + 1).toUpperCase(Locale.ROOT))
|
||||
}
|
||||
|
||||
/**
|
||||
* An enumeration of all possible results when populating [RomFile]
|
||||
*/
|
||||
enum class LoaderResult(val value : Int) {
|
||||
Success(0),
|
||||
ParsingError(1),
|
||||
MissingHeaderKey(2),
|
||||
MissingTitleKey(3),
|
||||
MissingTitleKek(4),
|
||||
MissingKeyArea(5);
|
||||
|
||||
companion object {
|
||||
fun get(value : Int) = values().first { value == it.value }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used to hold an application's metadata in a serializable way
|
||||
*/
|
||||
class AppEntry : Serializable {
|
||||
/**
|
||||
* The name of the application
|
||||
*/
|
||||
var name : String
|
||||
|
||||
/**
|
||||
* The author of the application, if it can be extracted from the metadata
|
||||
*/
|
||||
var author : String? = null
|
||||
|
||||
var icon : Bitmap? = null
|
||||
|
||||
/**
|
||||
* The format of the application ROM
|
||||
*/
|
||||
var format : RomFormat
|
||||
|
||||
/**
|
||||
* The URI of the application ROM
|
||||
*/
|
||||
var uri : Uri
|
||||
|
||||
constructor(name : String, author : String, format : RomFormat, uri : Uri, icon : Bitmap?) {
|
||||
this.name = name
|
||||
this.author = author
|
||||
this.icon = icon
|
||||
this.format = format
|
||||
this.uri = uri
|
||||
}
|
||||
|
||||
constructor(context : Context, format : RomFormat, uri : Uri) {
|
||||
this.name = context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
cursor.getString(nameIndex)
|
||||
}!!.dropLast(format.name.length + 1)
|
||||
this.format = format
|
||||
this.uri = uri
|
||||
}
|
||||
|
||||
/**
|
||||
* This serializes this object into an OutputStream
|
||||
*
|
||||
* @param output The stream to which the object is written into
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
private fun writeObject(output : ObjectOutputStream) {
|
||||
output.writeUTF(name)
|
||||
output.writeObject(format)
|
||||
output.writeUTF(uri.toString())
|
||||
output.writeBoolean(author != null)
|
||||
if (author != null)
|
||||
output.writeUTF(author)
|
||||
output.writeBoolean(icon != null)
|
||||
if (icon != null) {
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
icon!!.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, output)
|
||||
else
|
||||
icon!!.compress(Bitmap.CompressFormat.WEBP, 100, output)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This initializes the object from an InputStream
|
||||
*
|
||||
* @param input The stream from which the object data is retrieved from
|
||||
*/
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
private fun readObject(input : ObjectInputStream) {
|
||||
name = input.readUTF()
|
||||
format = input.readObject() as RomFormat
|
||||
uri = Uri.parse(input.readUTF())
|
||||
if (input.readBoolean())
|
||||
author = input.readUTF()
|
||||
if (input.readBoolean())
|
||||
icon = BitmapFactory.decodeStream(input)
|
||||
}
|
||||
class AppEntry(val name : String, val author : String?, val icon : Bitmap?, val format : RomFormat, val uri : Uri, val loaderResult : LoaderResult) : Serializable {
|
||||
constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
cursor.getString(nameIndex)
|
||||
}!!.dropLast(format.name.length + 1), null, null, format, uri, loaderResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is used as interface between libskyline and Kotlin for loaders
|
||||
*/
|
||||
internal class RomFile(val context : Context, val format : RomFormat, val file : ParcelFileDescriptor) : AutoCloseable {
|
||||
internal class RomFile(context : Context, format : RomFormat, uri : Uri) {
|
||||
/**
|
||||
* This is a pointer to the corresponding C++ Loader class
|
||||
* @note This field is filled in by native code
|
||||
*/
|
||||
var instance : Long
|
||||
private var applicationName : String? = null
|
||||
|
||||
/**
|
||||
* @note This field is filled in by native code
|
||||
*/
|
||||
private var applicationAuthor : String? = null
|
||||
|
||||
/**
|
||||
* @note This field is filled in by native code
|
||||
*/
|
||||
private var rawIcon : ByteArray? = null
|
||||
|
||||
val appEntry : AppEntry
|
||||
|
||||
var result = LoaderResult.Success
|
||||
|
||||
val valid : Boolean
|
||||
get() = result == LoaderResult.Success
|
||||
|
||||
init {
|
||||
System.loadLibrary("skyline")
|
||||
|
||||
instance = initialize(format.ordinal, file.fd)
|
||||
context.contentResolver.openFileDescriptor(uri, "r")!!.use {
|
||||
result = LoaderResult.get(populate(format.ordinal, it.fd, context.filesDir.canonicalPath + "/"))
|
||||
}
|
||||
|
||||
appEntry = applicationName?.let { name ->
|
||||
applicationAuthor?.let { author ->
|
||||
rawIcon?.let { icon ->
|
||||
AppEntry(name, author, BitmapFactory.decodeByteArray(icon, 0, icon.size), format, uri, result)
|
||||
}
|
||||
}
|
||||
} ?: AppEntry(context, format, uri, result)
|
||||
}
|
||||
|
||||
/**
|
||||
* This allocates and initializes a new loader object
|
||||
* Parses ROM and writes its metadata to [applicationName], [applicationAuthor] and [rawIcon]
|
||||
* @param format The format of the ROM
|
||||
* @param romFd A file descriptor of the ROM
|
||||
* @param appFilesPath Path to internal app data storage, needed to read imported keys
|
||||
* @return A pointer to the newly allocated object, or 0 if the ROM is invalid
|
||||
*/
|
||||
private external fun initialize(format : Int, romFd : Int) : Long
|
||||
|
||||
/**
|
||||
* @return Whether the ROM contains assets, such as an icon or author information
|
||||
*/
|
||||
private external fun hasAssets(instance : Long) : Boolean
|
||||
|
||||
/**
|
||||
* @return A ByteArray containing the application's icon as a bitmap
|
||||
*/
|
||||
private external fun getIcon(instance : Long) : ByteArray
|
||||
|
||||
/**
|
||||
* @return A String containing the name of the application
|
||||
*/
|
||||
private external fun getApplicationName(instance : Long) : String
|
||||
|
||||
/**
|
||||
* @return A String containing the publisher of the application
|
||||
*/
|
||||
private external fun getApplicationPublisher(instance : Long) : String
|
||||
|
||||
/**
|
||||
* This destroys an existing loader object and frees it's resources
|
||||
*/
|
||||
private external fun destroy(instance : Long)
|
||||
|
||||
/**
|
||||
* This is used to get the [AppEntry] for the specified ROM
|
||||
*/
|
||||
fun getAppEntry(uri : Uri) : AppEntry {
|
||||
return if (hasAssets(instance)) {
|
||||
val rawIcon = getIcon(instance)
|
||||
val icon = if (rawIcon.isNotEmpty()) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null
|
||||
|
||||
AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon)
|
||||
} else {
|
||||
AppEntry(context, format, uri)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This checks if the currently loaded ROM is valid
|
||||
*/
|
||||
fun valid() : Boolean {
|
||||
return instance != 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* This destroys the C++ loader object
|
||||
*/
|
||||
override fun close() {
|
||||
if (valid()) {
|
||||
destroy(instance)
|
||||
instance = 0
|
||||
}
|
||||
}
|
||||
private external fun populate(format : Int, romFd : Int, appFilesPath : String) : Int
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Some preferences need results from activities, this delegates the results to them
|
||||
*/
|
||||
interface ActivityResultDelegate {
|
||||
var requestCode : Int
|
||||
|
||||
fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?)
|
||||
}
|
@ -14,17 +14,22 @@ import androidx.preference.Preference.SummaryProvider
|
||||
import emu.skyline.R
|
||||
import emu.skyline.SettingsActivity
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
/**
|
||||
* This preference is used to launch [ControllerActivity] using a preference
|
||||
*/
|
||||
class ControllerPreference : Preference {
|
||||
class ControllerPreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
|
||||
/**
|
||||
* The index of the controller this preference manages
|
||||
*/
|
||||
private var index : Int = -1
|
||||
private var index = -1
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr) {
|
||||
private var inputManager : InputManager? = null
|
||||
|
||||
override var requestCode = 0
|
||||
|
||||
init {
|
||||
for (i in 0 until attrs!!.attributeCount) {
|
||||
val attr = attrs.getAttributeName(i)
|
||||
|
||||
@ -42,23 +47,23 @@ class ControllerPreference : Preference {
|
||||
|
||||
title = "${context?.getString(R.string.config_controller)} #${index + 1}"
|
||||
|
||||
if (context is SettingsActivity)
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { _ -> context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
|
||||
if (context is SettingsActivity) {
|
||||
inputManager = context.inputManager
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.preferenceStyle)
|
||||
|
||||
constructor(context : Context?) : this(context, null)
|
||||
|
||||
/**
|
||||
* This launches [ControllerActivity] on click to configure the controller
|
||||
*/
|
||||
override fun onClick() {
|
||||
if (context is SettingsActivity)
|
||||
(context as SettingsActivity).refreshKey = key
|
||||
(context as Activity).startActivityForResult(Intent(context, ControllerActivity::class.java).apply { putExtra("index", index) }, requestCode)
|
||||
}
|
||||
|
||||
val intent = Intent(context, ControllerActivity::class.java)
|
||||
intent.putExtra("index", index)
|
||||
(context as Activity).startActivityForResult(intent, 0)
|
||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
if (this.requestCode == requestCode) {
|
||||
inputManager?.syncObjects()
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
app/src/main/java/emu/skyline/preference/DocumentActivity.kt
Normal file
55
app/src/main/java/emu/skyline/preference/DocumentActivity.kt
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
/**
|
||||
* This activity is used to launch a document picker and saves the result to preferences
|
||||
*/
|
||||
abstract class DocumentActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val KEY_NAME = "key_name"
|
||||
}
|
||||
|
||||
private lateinit var keyName : String
|
||||
|
||||
protected abstract val actionIntent : Intent
|
||||
|
||||
/**
|
||||
* This launches the [Intent.ACTION_OPEN_DOCUMENT_TREE] intent on creation
|
||||
*/
|
||||
override fun onCreate(state : Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
keyName = intent.getStringExtra(KEY_NAME)!!
|
||||
|
||||
this.startActivityForResult(actionIntent, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* This changes the search location preference if the [Intent.ACTION_OPEN_DOCUMENT_TREE] has returned and [finish]es the activity
|
||||
*/
|
||||
public override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && requestCode == 1) {
|
||||
val uri = data!!.data!!
|
||||
|
||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(keyName, uri.toString())
|
||||
.putBoolean("refresh_required", true)
|
||||
.apply()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
18
app/src/main/java/emu/skyline/preference/FileActivity.kt
Normal file
18
app/src/main/java/emu/skyline/preference/FileActivity.kt
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Launches document picker to select one file
|
||||
*/
|
||||
class FileActivity : DocumentActivity() {
|
||||
override val actionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
}
|
41
app/src/main/java/emu/skyline/preference/FilePreference.kt
Normal file
41
app/src/main/java/emu/skyline/preference/FilePreference.kt
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.KeyReader
|
||||
import emu.skyline.R
|
||||
import emu.skyline.SettingsActivity
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
|
||||
/**
|
||||
* Launches [FileActivity] and process the selected file for key import
|
||||
*/
|
||||
class FilePreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = androidx.preference.R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
|
||||
override var requestCode = 0
|
||||
|
||||
override fun onClick() = (context as Activity).startActivityForResult(Intent(context, FileActivity::class.java).apply { putExtra(DocumentActivity.KEY_NAME, key) }, requestCode)
|
||||
|
||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
if (this.requestCode == requestCode) {
|
||||
if (key == "prod_keys" || key == "title_keys") {
|
||||
val success = KeyReader.import(
|
||||
context,
|
||||
Uri.parse(PreferenceManager.getDefaultSharedPreferences(context).getString(key, "")),
|
||||
KeyReader.KeyType.parse(key)
|
||||
)
|
||||
Snackbar.make((context as SettingsActivity).settings, if (success) R.string.import_keys_success else R.string.import_keys_failed, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,45 +5,11 @@
|
||||
|
||||
package emu.skyline.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
/**
|
||||
* This activity is used to select a new search location and set preferences to reflect that
|
||||
* Launches document picker to select a folder
|
||||
*/
|
||||
class FolderActivity : AppCompatActivity() {
|
||||
/**
|
||||
* This launches the [Intent.ACTION_OPEN_DOCUMENT_TREE] intent on creation
|
||||
*/
|
||||
override fun onCreate(state : Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
this.startActivityForResult(intent, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* This changes the search location preference if the [Intent.ACTION_OPEN_DOCUMENT_TREE] has returned and [finish]es the activity
|
||||
*/
|
||||
public override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == 1) {
|
||||
val uri = data!!.data!!
|
||||
|
||||
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString("search_location", uri.toString())
|
||||
.putBoolean("refresh_required", true)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
class FolderActivity : DocumentActivity() {
|
||||
override val actionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
}
|
||||
|
@ -13,43 +13,27 @@ import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.SummaryProvider
|
||||
import androidx.preference.R
|
||||
import emu.skyline.SettingsActivity
|
||||
|
||||
/**
|
||||
* This preference shows the decoded URI of it's preference and launches [FolderActivity]
|
||||
* This preference shows the decoded URI of it's preference and launches [DocumentActivity]
|
||||
*/
|
||||
class FolderPreference : Preference {
|
||||
/**
|
||||
* The directory the preference is currently set to
|
||||
*/
|
||||
private var mDirectory : String? = null
|
||||
class FolderPreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
|
||||
override var requestCode = 0
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr) {
|
||||
init {
|
||||
summaryProvider = SummaryProvider<FolderPreference> { preference ->
|
||||
preference.onSetInitialValue(null)
|
||||
Uri.decode(preference.mDirectory) ?: ""
|
||||
Uri.decode(preference.getPersistedString(""))
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.preferenceStyle)
|
||||
|
||||
constructor(context : Context?) : this(context, null)
|
||||
|
||||
/**
|
||||
* This launches [FolderActivity] on click to change the directory
|
||||
* This launches [DocumentActivity] on click to change the directory
|
||||
*/
|
||||
override fun onClick() {
|
||||
if (context is SettingsActivity)
|
||||
(context as SettingsActivity).refreshKey = key
|
||||
|
||||
val intent = Intent(context, FolderActivity::class.java)
|
||||
(context as Activity).startActivityForResult(intent, 0)
|
||||
(context as Activity).startActivityForResult(Intent(context, FolderActivity::class.java).apply { putExtra(DocumentActivity.KEY_NAME, key) }, requestCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets the initial value of [mDirectory]
|
||||
*/
|
||||
override fun onSetInitialValue(defaultValue : Any?) {
|
||||
mDirectory = getPersistedString(defaultValue as String?)
|
||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
if (requestCode == requestCode) notifyChanged()
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
@ -13,7 +14,8 @@
|
||||
android:layout_height="150dp"
|
||||
android:contentDescription="@string/icon"
|
||||
android:focusable="false"
|
||||
app:shapeAppearanceOverlay="@style/roundedAppImage" />
|
||||
app:shapeAppearanceOverlay="@style/roundedAppImage"
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
@ -26,7 +28,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="18sp"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/game_subtitle"
|
||||
@ -34,7 +37,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp"
|
||||
tools:text="Subtitle" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
|
@ -51,8 +51,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:paddingBottom="15dp"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
|
9
app/src/main/res/layout/loader_error_dialog.xml
Normal file
9
app/src/main/res/layout/loader_error_dialog.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/errorRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
35
app/src/main/res/layout/loader_error_item.xml
Normal file
35
app/src/main/res/layout/loader_error_item.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:cardElevation="0dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="?attr/colorOnSurface"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="#F0FF0000"
|
||||
android:textStyle="bold"
|
||||
tools:text="File" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fileText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="Error" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
|
||||
@ -18,6 +17,7 @@
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<emu.skyline.views.CustomLinearLayout
|
||||
android:id="@+id/fab_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
@ -30,7 +30,8 @@
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
android:translationX="72dp"
|
||||
android:visibility="gone">
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/refresh_fab"
|
||||
|
@ -14,6 +14,9 @@
|
||||
<string name="pin">Pin</string>
|
||||
<string name="play">Play</string>
|
||||
<string name="searching_roms">Searching for ROMs</string>
|
||||
<string name="invalid_file">Invalid file</string>
|
||||
<string name="missing_title_key">Missing title key</string>
|
||||
<string name="incomplete_prod_keys">Incomplete production keys</string>
|
||||
<!-- Toolbar Logger -->
|
||||
<string name="clear">Clear</string>
|
||||
<string name="share">Share</string>
|
||||
@ -42,6 +45,11 @@
|
||||
<string name="docked_enabled">The system will emulate being in docked mode</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="username_default">@string/app_name</string>
|
||||
<string name="keys">Keys</string>
|
||||
<string name="prod_keys">Production Keys</string>
|
||||
<string name="title_keys">Title Keys</string>
|
||||
<string name="import_keys_success">Successfully imported keys</string>
|
||||
<string name="import_keys_failed">Failed to import keys</string>
|
||||
<!-- Input -->
|
||||
<string name="input">Input</string>
|
||||
<string name="show_osc">Show On-Screen Controls</string>
|
||||
|
@ -51,6 +51,18 @@
|
||||
app:limit="31"
|
||||
app:title="@string/username" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="category_keys"
|
||||
android:title="@string/keys">
|
||||
<emu.skyline.preference.FilePreference
|
||||
app:key="prod_keys"
|
||||
app:title="@string/prod_keys"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<emu.skyline.preference.FilePreference
|
||||
app:key="title_keys"
|
||||
app:title="@string/title_keys"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="category_system"
|
||||
android:title="@string/system">
|
||||
|
@ -19,3 +19,7 @@ org.gradle.daemon=true
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
# Enables Prefab
|
||||
android.enablePrefab=true
|
||||
android.prefabVersion=1.1.0
|
||||
|
Loading…
Reference in New Issue
Block a user