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