diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0e0993ac..e8effd35 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -25,7 +25,8 @@ set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) include_directories(${source_DIR}/skyline) add_library(skyline SHARED - ${source_DIR}/main.cpp + ${source_DIR}/emu_jni.cpp + ${source_DIR}/loader_jni.cpp ${source_DIR}/skyline/common.cpp ${source_DIR}/skyline/nce/guest.S ${source_DIR}/skyline/nce/guest.cpp diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/emu_jni.cpp similarity index 96% rename from app/src/main/cpp/main.cpp rename to app/src/main/cpp/emu_jni.cpp index 83131548..971025d2 100644 --- a/app/src/main/cpp/main.cpp +++ b/app/src/main/cpp/emu_jni.cpp @@ -3,6 +3,7 @@ #include #include +#include "skyline/loader/loader.h" #include "skyline/common.h" #include "skyline/os.h" #include "skyline/jvm.h" @@ -50,7 +51,7 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication( auto romUri = env->GetStringUTFChars(romUriJstring, nullptr); logger->Info("Launching ROM {}", romUri); env->ReleaseStringUTFChars(romUriJstring, romUri); - os.Execute(romFd, static_cast(romType)); + os.Execute(romFd, static_cast(romType)); } catch (std::exception &e) { logger->Error(e.what()); } catch (...) { @@ -85,4 +86,4 @@ extern "C" JNIEXPORT jint Java_emu_skyline_EmulationActivity_getFps(JNIEnv *env, extern "C" JNIEXPORT jfloat Java_emu_skyline_EmulationActivity_getFrametime(JNIEnv *env, jobject thiz) { return static_cast(frametime) / 100; -} +} \ No newline at end of file diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp new file mode 100644 index 00000000..76d55f8a --- /dev/null +++ b/app/src/main/cpp/loader_jni.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include "skyline/vfs/os_backing.h" +#include "skyline/loader/nro.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); + + try { + auto backing = std::make_shared(fd); + + switch (format) { + case skyline::loader::RomFormat::NRO: + return reinterpret_cast(new skyline::loader::NroLoader(backing)); + default: + return 0; + } + } catch (const std::exception &e) { + return 0; + } +} + +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); +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 935f0dce..d19d19e5 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -28,9 +28,8 @@ import emu.skyline.adapter.AppAdapter import emu.skyline.adapter.AppItem import emu.skyline.adapter.GridLayoutSpan import emu.skyline.adapter.LayoutType -import emu.skyline.loader.BaseLoader -import emu.skyline.loader.NroLoader -import emu.skyline.utility.RandomAccessDocument +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 @@ -52,31 +51,34 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick /** * This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata */ - private fun addEntries(extension : String, loader : BaseLoader, directory : DocumentFile, found : Boolean = false) : Boolean { + private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean { var foundCurrent = found directory.listFiles().forEach { file -> if (file.isDirectory) { - foundCurrent = addEntries(extension, loader, file, foundCurrent) + foundCurrent = addEntries(extension, romFormat, file, foundCurrent) } else { if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { - val document = RandomAccessDocument(this, file) + val romFd = contentResolver.openFileDescriptor(file.uri, "r")!! + val romFile = RomFile(this, romFormat, romFd) - if (loader.verifyFile(document)) { - val entry = loader.getAppEntry(document, file.uri) + if (romFile.valid()) { + romFile.use { + val entry = romFile.getAppEntry(file.uri) - runOnUiThread { - if (!foundCurrent) { - adapter.addHeader(loader.format.name) + runOnUiThread { + if (!foundCurrent) { + adapter.addHeader(romFormat.name) + } + + adapter.addItem(AppItem(entry)) } - adapter.addItem(AppItem(entry)) + foundCurrent = true } - - foundCurrent = true } - document.close() + romFd.close(); } } } @@ -109,7 +111,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick try { runOnUiThread { adapter.clear() } - val foundNros = addEntries("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + val foundNros = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) runOnUiThread { if (!foundNros) diff --git a/app/src/main/java/emu/skyline/loader/NroLoader.kt b/app/src/main/java/emu/skyline/loader/NroLoader.kt deleted file mode 100644 index fdcf3853..00000000 --- a/app/src/main/java/emu/skyline/loader/NroLoader.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: MPL-2.0 - * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) - */ - -package emu.skyline.loader - -import android.content.Context -import android.graphics.BitmapFactory -import android.net.Uri -import emu.skyline.utility.RandomAccessDocument -import java.io.IOException - -/** - * This loader is used to load in NRO (Nintendo Relocatable Object) files (https://switchbrew.org/wiki/NRO) - */ -internal class NroLoader(context : Context) : BaseLoader(context, RomFormat.NRO) { - /** - * This is used to get the [AppEntry] for the specified NRO - */ - override fun getAppEntry(file : RandomAccessDocument, uri : Uri) : AppEntry { - return try { - file.seek(0x18) // Skip to NroHeader.size - - val asetOffset = Integer.reverseBytes(file.readInt()) - file.seek(asetOffset.toLong()) // Skip to the offset specified by NroHeader.size - - val buffer = ByteArray(4) - file.read(buffer) - if (String(buffer) != "ASET") throw IOException() - file.skipBytes(0x4) - - val iconOffset = java.lang.Long.reverseBytes(file.readLong()) - val iconSize = Integer.reverseBytes(file.readInt()) - if (iconOffset == 0L || iconSize == 0) throw IOException() - file.seek(asetOffset + iconOffset) - - val iconData = ByteArray(iconSize) - file.read(iconData) - val icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize) - file.seek(asetOffset + 0x18.toLong()) - - val nacpOffset = java.lang.Long.reverseBytes(file.readLong()) - val nacpSize = java.lang.Long.reverseBytes(file.readLong()) - if (nacpOffset == 0L || nacpSize == 0L) throw IOException() - file.seek(asetOffset + nacpOffset) - - val name = ByteArray(0x200) - file.read(name) - - val author = ByteArray(0x100) - file.read(author) - - AppEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), format, uri, icon) - } catch (e : IOException) { - AppEntry(context, format, uri) - } - } - - /** - * This verifies if [file] is a valid NRO file - */ - override fun verifyFile(file : RandomAccessDocument) : Boolean { - try { - file.seek(0x10) // Skip to NroHeader.magic - - val buffer = ByteArray(4) - file.read(buffer) - if (String(buffer) != "NRO0") return false - } catch (e : IOException) { - return false - } - return true - } -} diff --git a/app/src/main/java/emu/skyline/loader/BaseLoader.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt similarity index 59% rename from app/src/main/java/emu/skyline/loader/BaseLoader.kt rename to app/src/main/java/emu/skyline/loader/RomFile.kt index 811ca1fb..d27189b5 100644 --- a/app/src/main/java/emu/skyline/loader/BaseLoader.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -10,8 +10,9 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.os.ParcelFileDescriptor import android.provider.OpenableColumns -import emu.skyline.utility.RandomAccessDocument +import android.view.Surface import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -21,10 +22,10 @@ import java.util.* /** * An enumeration of all supported ROM formats */ -enum class RomFormat { - NRO, - XCI, - NSP, +enum class RomFormat(val format: Int){ + NRO(0), + XCI(1), + NSP(2), } /** @@ -69,7 +70,7 @@ class AppEntry : Serializable { */ var uri : Uri - constructor(name : String, author : String, format : RomFormat, uri : Uri, icon : Bitmap) { + constructor(name : String, author : String, format : RomFormat, uri : Uri, icon : Bitmap?) { this.name = name this.author = author this.icon = icon @@ -123,16 +124,81 @@ class AppEntry : Serializable { } /** - * This class is used as the base class for all loaders + * This class is used as interface between libskyline and Kotlin for loaders */ -internal abstract class BaseLoader(val context : Context, val format : RomFormat) { +internal class RomFile(val context : Context, val format : RomFormat, val file : ParcelFileDescriptor) : AutoCloseable { /** - * This is used to get the [AppEntry] for the specified [file] at the supplied [uri] + * This is a pointer to the corresponding C++ Loader class */ - abstract fun getAppEntry(file : RandomAccessDocument, uri : Uri) : AppEntry + var instance : Long + + init { + System.loadLibrary("skyline") + + instance = initialize(format.ordinal, file.fd) + } /** - * This returns if the supplied [file] is a valid ROM or not + * This allocates and initializes a new loader object + * @param format The format of the ROM + * @param romFd A file descriptor of the ROM + * @return A pointer to the newly allocated object, or 0 if the ROM is invalid */ - abstract fun verifyFile(file : RandomAccessDocument) : Boolean -} + 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 NRO + */ + fun getAppEntry(uri : Uri) : AppEntry { + return if (hasAssets(instance)) { + val rawIcon = getIcon(instance) + val icon = if (rawIcon.size != 0) 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt deleted file mode 100644 index 60b948bb..00000000 --- a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-License-Identifier: MPL-2.0 - * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) - */ - -package emu.skyline.utility - -import android.content.Context -import android.os.ParcelFileDescriptor -import androidx.documentfile.provider.DocumentFile -import java.nio.ByteBuffer - -/** - * This is made as a parallel to [java.io.RandomAccessFile] for [DocumentFile]s - * - * @param parcelFileDescriptor The file descriptor for the [DocumentFile] - */ -class RandomAccessDocument(private var parcelFileDescriptor : ParcelFileDescriptor) { - /** - * The actual file descriptor for the [DocumentFile] as an [FileDescriptor] object - */ - private val fileDescriptor = parcelFileDescriptor.fileDescriptor - - /** - * The current position of where the file is being read - */ - private var position : Long = 0 - - /** - * The constructor sets [parcelFileDescriptor] by opening a read-only FD to [file] - */ - constructor(context : Context, file : DocumentFile) : this(context.contentResolver.openFileDescriptor(file.uri, "r")!!) - - /** - * This reads in as many as possible bytes into [array] (Generally [array].size) - * - * @return The amount of bytes read from the file - */ - fun read(array : ByteArray) : Int { - val bytesRead = android.system.Os.pread(fileDescriptor, array, 0, array.size, position) - position += bytesRead - return bytesRead - } - - /** - * This reads in as many as possible bytes into [buffer] (Generally [buffer].array().size) - * - * @return The amount of bytes read from the file - */ - fun read(buffer : ByteBuffer) : Int { - val bytesRead = android.system.Os.pread(fileDescriptor, buffer.array(), 0, buffer.array().size, position) - position += bytesRead - return bytesRead - } - - /** - * This returns a single [Long] from the file at the current [position] - */ - fun readLong() : Long { - val buffer : ByteBuffer = ByteBuffer.allocate(Long.SIZE_BYTES) - read(buffer) - return buffer.long - } - - /** - * This returns a single [Int] from the file at the current [position] - */ - fun readInt() : Int { - val buffer : ByteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES) - read(buffer) - return buffer.int - } - - /** - * This sets [RandomAccessDocument.position] to the supplied [position] - */ - fun seek(position : Long) { - this.position = position - } - - /** - * This increments [position] by [amount] - */ - fun skipBytes(amount : Long) { - this.position += amount - } - - /** - * This closes [parcelFileDescriptor] so this class doesn't leak file descriptors - */ - fun close() { - parcelFileDescriptor.close() - } -}