diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 94562f77..ef8e679c 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -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) diff --git a/app/build.gradle b/app/build.gradle index 77600f93..2d8d9904 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' + } } diff --git a/app/libraries/mbedtls.aar b/app/libraries/mbedtls.aar new file mode 100644 index 00000000..591757d7 Binary files /dev/null and b/app/libraries/mbedtls.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 53be8ac0..477bb5b3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d70fbe59..af3091ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="emu.skyline"> - + + + diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp index 6e6bb942..48fbcb1d 100644 --- a/app/src/main/cpp/loader_jni.cpp +++ b/app/src/main/cpp/loader_jni.cpp @@ -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(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(jformat)}; + auto appFilesPath{env->GetStringUTFChars(appFilesPathJstring, nullptr)}; + auto keyStore{std::make_shared(appFilesPath)}; + env->ReleaseStringUTFChars(appFilesPathJstring, appFilesPath); + + std::unique_ptr loader; try { - auto backing = std::make_shared(fd); + auto backing{std::make_shared(fd)}; switch (format) { case skyline::loader::RomFormat::NRO: - return reinterpret_cast(new skyline::loader::NroLoader(backing)); + loader = std::make_unique(backing); + break; case skyline::loader::RomFormat::NSO: - return reinterpret_cast(new skyline::loader::NsoLoader(backing)); + loader = std::make_unique(backing); + break; case skyline::loader::RomFormat::NCA: - return reinterpret_cast(new skyline::loader::NcaLoader(backing)); + loader = std::make_unique(backing, keyStore); + break; case skyline::loader::RomFormat::NSP: - return reinterpret_cast(new skyline::loader::NspLoader(backing)); + loader = std::make_unique(backing, keyStore); + break; default: - return 0; + return static_cast(skyline::loader::LoaderResult::ParsingError); } + } catch (const skyline::loader::loader_exception &e) { + return static_cast(e.error); } catch (const std::exception &e) { - return 0; + return static_cast(skyline::loader::LoaderResult::ParsingError); } -} - -extern "C" JNIEXPORT jboolean JNICALL Java_emu_skyline_loader_RomFile_hasAssets(JNIEnv *env, jobject thiz, jlong instance) { - return reinterpret_cast(instance)->nacp != nullptr; -} - -extern "C" JNIEXPORT jbyteArray JNICALL Java_emu_skyline_loader_RomFile_getIcon(JNIEnv *env, jobject thiz, jlong instance) { - std::vector buffer = reinterpret_cast(instance)->GetIcon(); - - jbyteArray result = env->NewByteArray(buffer.size()); - env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast(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(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(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(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(icon.data())); + env->SetObjectField(thiz, rawIconField, iconByteArray); + } + + return static_cast(skyline::loader::LoaderResult::Success); } diff --git a/app/src/main/cpp/skyline/common.h b/app/src/main/cpp/skyline/common.h index d35db0ea..c692d3cf 100644 --- a/app/src/main/cpp/skyline/common.h +++ b/app/src/main/cpp/skyline/common.h @@ -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 + 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 + constexpr std::array HexStringToArray(const std::string_view &hexString) { + if (hexString.size() != Size * 2) + throw exception("Invalid size"); + std::array 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); }; - /** - * @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 - inline exception(const S &formatStr, Args &&... args) : runtime_error(fmt::format(formatStr, args...)) {} - }; - class NCE; class JvmManager; namespace gpu { diff --git a/app/src/main/cpp/skyline/crypto/aes_cipher.cpp b/app/src/main/cpp/skyline/crypto/aes_cipher.cpp new file mode 100644 index 00000000..deba988d --- /dev/null +++ b/app/src/main/cpp/skyline/crypto/aes_cipher.cpp @@ -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 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 &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> 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); + } + } +} diff --git a/app/src/main/cpp/skyline/crypto/aes_cipher.h b/app/src/main/cpp/skyline/crypto/aes_cipher.h new file mode 100644 index 00000000..3b75d1f1 --- /dev/null +++ b/app/src/main/cpp/skyline/crypto/aes_cipher.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include +#include +#include + +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 buffer; + + /** + * @brief Calculates IV for XTS, basically just big to little endian conversion. + */ + inline static std::array GetTweak(size_t sector) { + std::array tweak{}; + size_t le{__builtin_bswap64(sector)}; + std::memcpy(tweak.data() + 8, &le, 8); + return tweak; + } + + public: + AesCipher(std::span key, mbedtls_cipher_type_t type); + + ~AesCipher(); + + /** + * @brief Sets initilization vector + */ + void SetIV(const std::array &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 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 data, size_t sector, size_t sectorSize) { + XtsDecrypt(data.data(), data.data(), data.size(), sector, sectorSize); + } + }; +} diff --git a/app/src/main/cpp/skyline/crypto/key_store.cpp b/app/src/main/cpp/skyline/crypto/key_store.cpp new file mode 100644 index 00000000..79ca1d4d --- /dev/null +++ b/app/src/main/cpp/skyline/crypto/key_store.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include +#include +#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 &backing, ReadPairsCallback callback) { + std::vector fileContent(backing->size); + backing->Read(fileContent.data(), 0, fileContent.size()); + + auto lineStart{fileContent.begin()}; + std::vector::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); + } + } + } +} diff --git a/app/src/main/cpp/skyline/crypto/key_store.h b/app/src/main/cpp/skyline/crypto/key_store.h new file mode 100644 index 00000000..74d69454 --- /dev/null +++ b/app/src/main/cpp/skyline/crypto/key_store.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include +#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; + using Key256 = std::array; + using IndexedKeys128 = std::array, 20>; + + std::optional headerKey; + + IndexedKeys128 titleKek; + IndexedKeys128 areaKeyApplication; + IndexedKeys128 areaKeyOcean; + IndexedKeys128 areaKeySystem; + private: + std::map titleKeys; + + std::unordered_map &> key256Names{ + {"header_key", headerKey} + }; + + std::unordered_map 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 &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 GetTitleKey(const Key128 &title) { + auto it{titleKeys.find(title)}; + if (it == titleKeys.end()) + return std::nullopt; + return it->second; + } + }; +} diff --git a/app/src/main/cpp/skyline/loader/loader.h b/app/src/main/cpp/skyline/loader/loader.h index 9ed7df7b..a62cc151 100644 --- a/app/src/main/cpp/skyline/loader/loader.h +++ b/app/src/main/cpp/skyline/loader/loader.h @@ -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 */ diff --git a/app/src/main/cpp/skyline/loader/nca.cpp b/app/src/main/cpp/skyline/loader/nca.cpp index d61d11b3..f4a731fa 100644 --- a/app/src/main/cpp/skyline/loader/nca.cpp +++ b/app/src/main/cpp/skyline/loader/nca.cpp @@ -8,7 +8,7 @@ #include "nca.h" namespace skyline::loader { - NcaLoader::NcaLoader(const std::shared_ptr &backing) : nca(backing) { + NcaLoader::NcaLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore) : nca(backing, keyStore) { if (nca.exeFs == nullptr) throw exception("Only NCAs with an ExeFS can be loaded directly"); } diff --git a/app/src/main/cpp/skyline/loader/nca.h b/app/src/main/cpp/skyline/loader/nca.h index 71bcd90c..401b81f1 100644 --- a/app/src/main/cpp/skyline/loader/nca.h +++ b/app/src/main/cpp/skyline/loader/nca.h @@ -5,6 +5,7 @@ #include #include +#include #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 &backing); + NcaLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore); /** * @brief This loads an ExeFS into memory diff --git a/app/src/main/cpp/skyline/loader/nsp.cpp b/app/src/main/cpp/skyline/loader/nsp.cpp index c15ce667..8aeaa8ef 100644 --- a/app/src/main/cpp/skyline/loader/nsp.cpp +++ b/app/src/main/cpp/skyline/loader/nsp.cpp @@ -5,20 +5,22 @@ #include "nsp.h" namespace skyline::loader { - NspLoader::NspLoader(const std::shared_ptr &backing) : nsp(std::make_shared(backing)) { - auto root = nsp->OpenDirectory("", {false, true}); + NspLoader::NspLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore) : nsp(std::make_shared(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(); - auto root = controlRomFs->OpenDirectory("", {false, true}); + auto root{controlRomFs->OpenDirectory("", {false, true})}; std::shared_ptr icon; // Use the first icon file available diff --git a/app/src/main/cpp/skyline/loader/nsp.h b/app/src/main/cpp/skyline/loader/nsp.h index afa21744..eb60a4f3 100644 --- a/app/src/main/cpp/skyline/loader/nsp.h +++ b/app/src/main/cpp/skyline/loader/nsp.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "loader.h" namespace skyline::loader { @@ -21,7 +22,7 @@ namespace skyline::loader { std::optional controlNca; //!< The main control NCA within the NSP public: - NspLoader(const std::shared_ptr &backing); + NspLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore); std::vector GetIcon(); diff --git a/app/src/main/cpp/skyline/os.cpp b/app/src/main/cpp/skyline/os.cpp index c4ec4dd9..6f1aab08 100644 --- a/app/src/main/cpp/skyline/os.cpp +++ b/app/src/main/cpp/skyline/os.cpp @@ -13,16 +13,17 @@ namespace skyline::kernel { OS::OS(std::shared_ptr &jvmManager, std::shared_ptr &logger, std::shared_ptr &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(romFd); + auto romFile{std::make_shared(romFd)}; + auto keyStore{std::make_shared(appFilesPath)}; if (romType == loader::RomFormat::NRO) { state.loader = std::make_shared(romFile); } else if (romType == loader::RomFormat::NSO) { state.loader = std::make_shared(romFile); } else if (romType == loader::RomFormat::NCA) { - state.loader = std::make_shared(romFile); + state.loader = std::make_shared(romFile, keyStore); } else if (romType == loader::RomFormat::NSP) { - state.loader = std::make_shared(romFile); + state.loader = std::make_shared(romFile, keyStore); } else { throw exception("Unsupported ROM extension."); } @@ -36,16 +37,16 @@ namespace skyline::kernel { } std::shared_ptr OS::CreateProcess(u64 entry, u64 argument, size_t stackSize) { - auto stack = std::make_shared(state, memory.stack.address, stackSize, memory::Permission{true, true, false}, memory::states::Stack, MAP_NORESERVE | MAP_STACK, true); + auto stack{std::make_shared(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(stack->guest.address), PAGE_SIZE, PROT_NONE)) throw exception("Failed to create guard pages"); - auto tlsMem = std::make_shared(state, 0, (sizeof(ThreadContext) + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1), memory::Permission{true, true, false}, memory::states::Reserved); + auto tlsMem{std::make_shared(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(&guest::GuestEntry), reinterpret_cast(stack->guest.address + stackSize), CLONE_FILES | CLONE_FS | CLONE_SETTLS | SIGCHLD, reinterpret_cast(entry), nullptr, reinterpret_cast(tlsMem->guest.address)); + auto pid{clone(reinterpret_cast(&guest::GuestEntry), reinterpret_cast(stack->guest.address + stackSize), CLONE_FILES | CLONE_FS | CLONE_SETTLS | SIGCHLD, reinterpret_cast(entry), nullptr, reinterpret_cast(tlsMem->guest.address))}; if (pid == -1) throw exception("Call to clone() has failed: {}", strerror(errno)); diff --git a/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.cpp b/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.cpp new file mode 100644 index 00000000..d1e45402 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.cpp @@ -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, 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 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); + } +} diff --git a/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.h b/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.h new file mode 100644 index 00000000..c926e499 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/ctr_encrypted_backing.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include +#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; + + /** + * @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, size_t baseOffset); + + size_t Read(u8 *output, size_t offset, size_t size) override; + }; +} diff --git a/app/src/main/cpp/skyline/vfs/nca.cpp b/app/src/main/cpp/skyline/vfs/nca.cpp index fec41842..4ebbafa0 100644 --- a/app/src/main/cpp/skyline/vfs/nca.cpp +++ b/app/src/main/cpp/skyline/vfs/nca.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) +#include +#include +#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 &backing) : backing(backing) { + using namespace loader; + + NCA::NCA(const std::shared_ptr &backing, const std::shared_ptr &keyStore) : backing(backing), keyStore(keyStore) { backing->Read(&header); - if (header.magic != util::MakeMagic("NCA3")) - throw exception("Attempting to load an encrypted or invalid NCA"); + if (header.magic != util::MakeMagic("NCA3")) { + if (!keyStore->headerKey) + throw loader_exception(LoaderResult::MissingHeaderKey); + + crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS); + + cipher.XtsDecrypt({reinterpret_cast(&header), sizeof(NcaHeader)}, 0, 0x200); + + // Check if decryption was successful + if (header.magic != util::MakeMagic("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(entry.startOffset) * constant::MediaUnitSize + header.sha256HashInfo.pfs0Offset; - size_t size = constant::MediaUnitSize * static_cast(entry.endOffset - entry.startOffset); + void NCA::ReadPfs0(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry) { + size_t offset{static_cast(entry.startOffset) * constant::MediaUnitSize + sectionHeader.sha256HashInfo.pfs0Offset}; + size_t size{constant::MediaUnitSize * static_cast(entry.endOffset - entry.startOffset)}; - auto pfs = std::make_shared(std::make_shared(backing, offset, size)); + auto pfs{std::make_shared(CreateBacking(sectionHeader, std::make_shared(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(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(entry.startOffset) * constant::MediaUnitSize + sectionHeader.integrityHashInfo.levels.back().offset}; + size_t size{sectionHeader.integrityHashInfo.levels.back().size}; - romFs = std::make_shared(backing, offset, size); + romFs = CreateBacking(sectionHeader, std::make_shared(backing, offset, size), offset); + } + + std::shared_ptr NCA::CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr 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 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(ctr, key, std::move(rawBacking), offset); + } + default: + return nullptr; + } + } + + u8 NCA::GetKeyGeneration() { + u8 legacyGen{static_cast(header.legacyKeyGenerationType)}; + u8 gen{static_cast(header.keyGenerationType)}; + gen = std::max(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); + } } } \ No newline at end of file diff --git a/app/src/main/cpp/skyline/vfs/nca.h b/app/src/main/cpp/skyline/vfs/nca.h index 66ac126e..15cf835d 100644 --- a/app/src/main/cpp/skyline/vfs/nca.h +++ b/app/src/main/cpp/skyline/vfs/nca.h @@ -4,6 +4,8 @@ #pragma once #include +#include +#include #include "filesystem.h" namespace skyline { @@ -195,10 +197,21 @@ namespace skyline { static_assert(sizeof(NcaHeader) == 0xC00); std::shared_ptr backing; //!< The backing for the NCA + std::shared_ptr 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 CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr rawBacking, size_t offset); + + u8 GetKeyGeneration(); + + crypto::KeyStore::Key128 GetTitleKey(); + + crypto::KeyStore::Key128 GetKeyAreaKey(NcaSectionEncryptionType type); public: std::shared_ptr exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section @@ -207,7 +220,7 @@ namespace skyline { std::shared_ptr romFs; //!< The backing for this NCA's RomFS section NcaContentType contentType; //!< The content type of the NCA - NCA(const std::shared_ptr &backing); + NCA(const std::shared_ptr &backing, const std::shared_ptr &keyStore); }; } } \ No newline at end of file diff --git a/app/src/main/cpp/skyline/vfs/os_filesystem.cpp b/app/src/main/cpp/skyline/vfs/os_filesystem.cpp index bb3949b8..401d733f 100644 --- a/app/src/main/cpp/skyline/vfs/os_filesystem.cpp +++ b/app/src/main/cpp/skyline/vfs/os_filesystem.cpp @@ -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"); diff --git a/app/src/main/cpp/skyline/vfs/partition_filesystem.cpp b/app/src/main/cpp/skyline/vfs/partition_filesystem.cpp index 7d75f187..af434805 100644 --- a/app/src/main/cpp/skyline/vfs/partition_filesystem.cpp +++ b/app/src/main/cpp/skyline/vfs/partition_filesystem.cpp @@ -19,12 +19,13 @@ namespace skyline::vfs { size_t stringTableOffset = sizeof(FsHeader) + (header.numFiles * entrySize); fileDataOffset = stringTableOffset + header.stringTableSize; - std::vector stringTable(header.stringTableSize); + std::vector 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)); diff --git a/app/src/main/cpp/skyline/vfs/region_backing.h b/app/src/main/cpp/skyline/vfs/region_backing.h index 84fcfb14..b6d20a29 100644 --- a/app/src/main/cpp/skyline/vfs/region_backing.h +++ b/app/src/main/cpp/skyline/vfs/region_backing.h @@ -12,7 +12,7 @@ namespace skyline::vfs { class RegionBacking : public Backing { private: std::shared_ptr 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 &backing, size_t offset, size_t size, Mode mode = {true, false, false}) : Backing(mode, size), backing(backing), offset(offset) {}; + RegionBacking(const std::shared_ptr &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); } }; -} \ No newline at end of file +} diff --git a/app/src/main/java/emu/skyline/AppDialog.kt b/app/src/main/java/emu/skyline/AppDialog.kt index 2de7ade3..56b052f3 100644 --- a/app/src/main/java/emu/skyline/AppDialog.kt +++ b/app/src/main/java/emu/skyline/AppDialog.kt @@ -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) diff --git a/app/src/main/java/emu/skyline/EmulationActivity.kt b/app/src/main/java/emu/skyline/EmulationActivity.kt index f3903ace..fb2158c0 100644 --- a/app/src/main/java/emu/skyline/EmulationActivity.kt +++ b/app/src/main/java/emu/skyline/EmulationActivity.kt @@ -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) diff --git a/app/src/main/java/emu/skyline/KeyReader.kt b/app/src/main/java/emu/skyline/KeyReader.kt new file mode 100644 index 00000000..a01b88b9 --- /dev/null +++ b/app/src/main/java/emu/skyline/KeyReader.kt @@ -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 + } +} diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index eeddb046..00081ec5 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -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) } } } } diff --git a/app/src/main/java/emu/skyline/SettingsActivity.kt b/app/src/main/java/emu/skyline/SettingsActivity.kt index 3893b0d6..4b18b555 100644 --- a/app/src/main/java/emu/skyline/SettingsActivity.kt +++ b/app/src/main/java/emu/skyline/SettingsActivity.kt @@ -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(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) + } + } } } diff --git a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt b/app/src/main/java/emu/skyline/adapter/AppAdapter.kt index 35480f24..f5bbec35 100644 --- a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/AppAdapter.kt @@ -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() { +internal class AppAdapter(val layoutType : LayoutType, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : HeaderAdapter() { 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) diff --git a/app/src/main/java/emu/skyline/data/AppItem.kt b/app/src/main/java/emu/skyline/data/AppItem.kt index 443b51f9..be6eb917 100644 --- a/app/src/main/java/emu/skyline/data/AppItem.kt +++ b/app/src/main/java/emu/skyline/data/AppItem.kt @@ -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 */ diff --git a/app/src/main/java/emu/skyline/input/InputManager.kt b/app/src/main/java/emu/skyline/input/InputManager.kt index aef530e1..ce3fdbf7 100644 --- a/app/src/main/java/emu/skyline/input/InputManager.kt +++ b/app/src/main/java/emu/skyline/input/InputManager.kt @@ -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 - @Suppress("UNCHECKED_CAST") - controllers = objectInput.readObject() as HashMap - - @Suppress("UNCHECKED_CAST") - eventMap = objectInput.readObject() as HashMap + @Suppress("UNCHECKED_CAST") + eventMap = it.readObject() as HashMap + } } /** * 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() + } } } diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt index 095e734a..2eda6903 100644 --- a/app/src/main/java/emu/skyline/loader/RomFile.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/preference/ActivityResultDelegate.kt b/app/src/main/java/emu/skyline/preference/ActivityResultDelegate.kt new file mode 100644 index 00000000..735b1d6c --- /dev/null +++ b/app/src/main/java/emu/skyline/preference/ActivityResultDelegate.kt @@ -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?) +} diff --git a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt index caa43762..045ddbbd 100644 --- a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt +++ b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt @@ -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 { _ -> context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } } + if (context is SettingsActivity) { + inputManager = context.inputManager + summaryProvider = SummaryProvider { 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() + } } } diff --git a/app/src/main/java/emu/skyline/preference/DocumentActivity.kt b/app/src/main/java/emu/skyline/preference/DocumentActivity.kt new file mode 100644 index 00000000..a8390986 --- /dev/null +++ b/app/src/main/java/emu/skyline/preference/DocumentActivity.kt @@ -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() + } +} diff --git a/app/src/main/java/emu/skyline/preference/FileActivity.kt b/app/src/main/java/emu/skyline/preference/FileActivity.kt new file mode 100644 index 00000000..388c1e6b --- /dev/null +++ b/app/src/main/java/emu/skyline/preference/FileActivity.kt @@ -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 = "*/*" + } +} diff --git a/app/src/main/java/emu/skyline/preference/FilePreference.kt b/app/src/main/java/emu/skyline/preference/FilePreference.kt new file mode 100644 index 00000000..8c68a4f9 --- /dev/null +++ b/app/src/main/java/emu/skyline/preference/FilePreference.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/emu/skyline/preference/FolderActivity.kt b/app/src/main/java/emu/skyline/preference/FolderActivity.kt index d4c5c8c3..362b7695 100644 --- a/app/src/main/java/emu/skyline/preference/FolderActivity.kt +++ b/app/src/main/java/emu/skyline/preference/FolderActivity.kt @@ -1,49 +1,15 @@ -/* - * 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 select a new search location and set preferences to reflect that - */ -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() - } -} +/* + * 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 a folder + */ +class FolderActivity : DocumentActivity() { + override val actionIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) +} diff --git a/app/src/main/java/emu/skyline/preference/FolderPreference.kt b/app/src/main/java/emu/skyline/preference/FolderPreference.kt index 3cca893f..1c468ed3 100644 --- a/app/src/main/java/emu/skyline/preference/FolderPreference.kt +++ b/app/src/main/java/emu/skyline/preference/FolderPreference.kt @@ -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 { 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() } } diff --git a/app/src/main/res/layout/app_dialog.xml b/app/src/main/res/layout/app_dialog.xml index 1980090b..3fbc1225 100644 --- a/app/src/main/res/layout/app_dialog.xml +++ b/app/src/main/res/layout/app_dialog.xml @@ -1,6 +1,7 @@ + app:shapeAppearanceOverlay="@style/roundedAppImage" + tools:src="@drawable/default_icon" /> + android:textSize="18sp" + tools:text="Title" /> + android:textSize="14sp" + tools:text="Subtitle" /> + diff --git a/app/src/main/res/layout/loader_error_item.xml b/app/src/main/res/layout/loader_error_item.xml new file mode 100644 index 00000000..4d35d2a6 --- /dev/null +++ b/app/src/main/res/layout/loader_error_item.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 39f86e1c..38dd0da3 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -1,5 +1,4 @@ - + android:visibility="gone" + tools:visibility="visible"> Pin Play Searching for ROMs + Invalid file + Missing title key + Incomplete production keys Clear Share @@ -42,6 +45,11 @@ The system will emulate being in docked mode Username @string/app_name + Keys + Production Keys + Title Keys + Successfully imported keys + Failed to import keys Input Show On-Screen Controls diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index fa72b5cb..381c7b81 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -51,6 +51,18 @@ app:limit="31" app:title="@string/username" /> + + + + diff --git a/gradle.properties b/gradle.properties index c85e7d8d..cf4f33d7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,8 @@ org.gradle.daemon=true # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true + +# Enables Prefab +android.enablePrefab=true +android.prefabVersion=1.1.0