diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index f1b42451..ec37c031 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,21 +1,9 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
+# Skyline Proguard Rules
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
+-keep class emu.skyline.loader.TitleEntry {
+ void writeObject(java.io.ObjectOutputStream);
+ void readObject(java.io.ObjectInputStream);
+}
+-keepclassmembernames class emu.skyline.GameActivity { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b4346405..431f1483 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">
-
@@ -13,13 +12,19 @@
+ tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
diff --git a/app/src/main/java/emu/skyline/loader/BaseLoader.kt b/app/src/main/java/emu/skyline/loader/BaseLoader.kt
new file mode 100644
index 00000000..890888e8
--- /dev/null
+++ b/app/src/main/java/emu/skyline/loader/BaseLoader.kt
@@ -0,0 +1,54 @@
+package emu.skyline.loader
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.provider.OpenableColumns
+import androidx.core.graphics.drawable.toBitmap
+import emu.skyline.R
+import emu.skyline.utility.RandomAccessDocument
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
+
+enum class TitleFormat {
+ NRO, XCI, NSP
+}
+
+internal class TitleEntry(var name: String, var author: String, var romType: TitleFormat, var valid: Boolean, @Transient var uri: Uri, @Transient var icon: Bitmap) : Serializable {
+ constructor(context: Context, author: String, romType: TitleFormat, valid: Boolean, uri: Uri) : this("", author, romType, valid, uri, context.resources.getDrawable(R.drawable.ic_missing_icon, context.theme).toBitmap(256, 256)) {
+ context.contentResolver.query(uri, null, null, null, null)!!.use { cursor ->
+ val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ cursor.moveToFirst()
+ name = cursor.getString(nameIndex)
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun writeObject(output: ObjectOutputStream) {
+ output.writeUTF(name)
+ output.writeUTF(author)
+ output.writeObject(romType)
+ output.writeUTF(uri.toString())
+ output.writeBoolean(valid)
+ icon.compress(Bitmap.CompressFormat.WEBP, 100, output)
+ }
+
+ @Throws(IOException::class, ClassNotFoundException::class)
+ private fun readObject(input: ObjectInputStream) {
+ name = input.readUTF()
+ author = input.readUTF()
+ romType = input.readObject() as TitleFormat
+ uri = Uri.parse(input.readUTF())
+ valid = input.readBoolean()
+ icon = BitmapFactory.decodeStream(input)
+ }
+}
+
+internal abstract class BaseLoader(val context: Context, val romType: TitleFormat) {
+ abstract fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry
+
+ abstract fun verifyFile(file: RandomAccessDocument): Boolean
+}
diff --git a/app/src/main/java/emu/skyline/loader/NroLoader.kt b/app/src/main/java/emu/skyline/loader/NroLoader.kt
new file mode 100644
index 00000000..dcd3dd1d
--- /dev/null
+++ b/app/src/main/java/emu/skyline/loader/NroLoader.kt
@@ -0,0 +1,53 @@
+package emu.skyline.loader
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import android.net.Uri
+import emu.skyline.R
+import emu.skyline.utility.RandomAccessDocument
+import java.io.IOException
+
+internal class NroLoader(context: Context) : BaseLoader(context, TitleFormat.NRO) {
+ override fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry {
+ 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)
+ TitleEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), romType, true, uri, icon)
+ } catch (e: IOException) {
+ TitleEntry(context, context.getString(R.string.aset_missing), romType, false, uri)
+ }
+ }
+
+ 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/utility/FolderActivity.kt b/app/src/main/java/emu/skyline/utility/FolderActivity.kt
new file mode 100644
index 00000000..6decf7ba
--- /dev/null
+++ b/app/src/main/java/emu/skyline/utility/FolderActivity.kt
@@ -0,0 +1,29 @@
+package emu.skyline.utility
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.PreferenceManager
+
+class FolderActivity : AppCompatActivity() {
+ override fun onCreate(state: Bundle?) {
+ super.onCreate(state)
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ this.startActivityForResult(intent, 1)
+ }
+
+ public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == 1) {
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putString("search_location", data!!.data.toString())
+ .putBoolean("refresh_required", true)
+ .apply()
+ finish()
+ }
+ } else
+ finish()
+ }
+}
diff --git a/app/src/main/java/emu/skyline/utility/FolderPreference.kt b/app/src/main/java/emu/skyline/utility/FolderPreference.kt
new file mode 100644
index 00000000..3de46325
--- /dev/null
+++ b/app/src/main/java/emu/skyline/utility/FolderPreference.kt
@@ -0,0 +1,110 @@
+package emu.skyline.utility
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.res.TypedArray
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import android.text.TextUtils
+import android.util.AttributeSet
+import androidx.preference.Preference
+
+class FolderPreference : Preference {
+ private var mDirectory: String? = null
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ summaryProvider = SimpleSummaryProvider.instance
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
+ summaryProvider = SimpleSummaryProvider.instance
+ }
+
+ constructor(context: Context?) : super(context) {
+ summaryProvider = SimpleSummaryProvider.instance
+ }
+
+ override fun onClick() {
+ val intent = Intent(context, FolderActivity::class.java)
+ (context as Activity).startActivityForResult(intent, 0)
+ }
+
+ var directory: String?
+ get() = mDirectory
+ set(directory) {
+ val changed = !TextUtils.equals(mDirectory, directory)
+ if (changed) {
+ mDirectory = directory
+ persistString(directory)
+ if (changed) {
+ notifyChanged()
+ }
+ }
+ }
+
+ override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
+ return a.getString(index)!!
+ }
+
+ override fun onSetInitialValue(defaultValue: Any?) {
+ directory = getPersistedString(defaultValue as String?)
+ }
+
+ override fun onSaveInstanceState(): Parcelable {
+ val superState = super.onSaveInstanceState()
+ if (isPersistent) {
+ return superState
+ }
+ val myState = SavedState(superState)
+ myState.mDirectory = directory
+ return myState
+ }
+
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ if (state == null || state.javaClass != SavedState::class.java) {
+ super.onRestoreInstanceState(state)
+ return
+ }
+ val myState = state as SavedState
+ super.onRestoreInstanceState(myState.superState)
+ directory = myState.mDirectory
+ }
+
+ internal class SavedState : BaseSavedState {
+ var mDirectory: String? = null
+
+ constructor(source: Parcel) : super(source) {
+ mDirectory = source.readString()
+ }
+
+ constructor(superState: Parcelable?) : super(superState)
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ super.writeToParcel(dest, flags)
+ dest.writeString(mDirectory)
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+ }
+
+ class SimpleSummaryProvider private constructor() : SummaryProvider {
+ override fun provideSummary(preference: FolderPreference): CharSequence {
+ return Uri.decode(preference.directory!!)
+ }
+
+ companion object {
+ private var sSimpleSummaryProvider: SimpleSummaryProvider? = null
+ val instance: SimpleSummaryProvider?
+ get() {
+ if (sSimpleSummaryProvider == null) {
+ sSimpleSummaryProvider = SimpleSummaryProvider()
+ }
+ return sSimpleSummaryProvider
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt
new file mode 100644
index 00000000..b1258277
--- /dev/null
+++ b/app/src/main/java/emu/skyline/utility/RandomAccessDocument.kt
@@ -0,0 +1,49 @@
+package emu.skyline.utility
+
+import android.content.Context
+import android.os.ParcelFileDescriptor
+import androidx.documentfile.provider.DocumentFile
+import java.nio.ByteBuffer
+
+class RandomAccessDocument(private var parcelFileDescriptor: ParcelFileDescriptor) {
+ constructor(context: Context, file: DocumentFile) : this(context.contentResolver.openFileDescriptor(file.uri, "r")!!)
+
+ private val fileDescriptor = parcelFileDescriptor.fileDescriptor
+ private var position: Long = 0
+
+ fun read(array: ByteArray): Int {
+ val bytesRead = android.system.Os.pread(fileDescriptor, array, 0, array.size, position)
+ position += bytesRead
+ return bytesRead
+ }
+
+ fun read(buffer: ByteBuffer): Int {
+ val bytesRead = android.system.Os.pread(fileDescriptor, buffer.array(), 0, buffer.array().size, position)
+ position += bytesRead
+ return bytesRead
+ }
+
+ fun readLong(): Long {
+ val buffer: ByteBuffer = ByteBuffer.allocate(Long.SIZE_BYTES)
+ read(buffer)
+ return buffer.long
+ }
+
+ fun readInt(): Int {
+ val buffer: ByteBuffer = ByteBuffer.allocate(Int.SIZE_BYTES)
+ read(buffer)
+ return buffer.int
+ }
+
+ fun seek(position: Long) {
+ this.position = position
+ }
+
+ fun skipBytes(position: Long) {
+ this.position += position
+ }
+
+ fun close() {
+ parcelFileDescriptor.close()
+ }
+}
diff --git a/app/src/main/res/layout/game_activity.xml b/app/src/main/res/layout/game_activity.xml
new file mode 100644
index 00000000..9a6ace7b
--- /dev/null
+++ b/app/src/main/res/layout/game_activity.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index d30d57a2..fb44746d 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -19,8 +19,7 @@
-