Refactor all Game-related Kotlin Classes/Objects

This refactors most game-related classes and objects mainly adding spacing, adding comments and making improvements if possible.
This commit is contained in:
◱ PixelyIon 2020-04-03 17:17:32 +05:30 committed by ◱ PixelyIon
parent f4968793b8
commit 55a9f8e937
12 changed files with 317 additions and 113 deletions

View File

@ -28,7 +28,9 @@ Use doxygen style comments for:
* Class/Struct Functions - Use `/**` block comments on their function with a brief, all arguments and the return value (The brief can be skipped if the function's arguments and return value alone explain what the function does) * Class/Struct Functions - Use `/**` block comments on their function with a brief, all arguments and the return value (The brief can be skipped if the function's arguments and return value alone explain what the function does)
* Enumerations - Use a `/**` block comment with a brief for the enum itself and a `//!<` single-line comment for all the individual items * Enumerations - Use a `/**` block comment with a brief for the enum itself and a `//!<` single-line comment for all the individual items
Note: The DeviceState object can be skipped from function argument documentation as well as class members in the constructor. Notes:
* The DeviceState object can be skipped from function argument documentation as well as class members in the constructor
* Any class members don't need to be redundantly documented in the constructor
### Control flow statements (if, for and while): ### Control flow statements (if, for and while):
#### If a child control-flow statement has brackets, the parent statement must as well #### If a child control-flow statement has brackets, the parent statement must as well
@ -176,4 +178,4 @@ Handle b = 0x10; // Handle is a specific type that won't be automatically assign
### Constants ### Constants
If a variable is constant at compile time use `constexpr`, if it's only used in a local function then place it in the function but if it's used throughout a class then in the corresponding header add the variable to the `skyline::constant` namespace. If a constant is used throughout the codebase, add it to `common.h`. If a variable is constant at compile time use `constexpr`, if it's only used in a local function then place it in the function but if it's used throughout a class then in the corresponding header add the variable to the `skyline::constant` namespace. If a constant is used throughout the codebase, add it to `common.h`.
In addition, try to `constexpr` as much as possible including constructors and functions so that they may be initialized at compile-time and have lesser runtime overhead during usage and certain values can be pre-calculated in advance. In addition, try to `constexpr` as much as possible including constructors and functions so that they may be initialized at compile-time and have lesser runtime overhead during usage and certain values can be pre-calculated in advance.

View File

@ -44,7 +44,7 @@
android:value="emu.skyline.SettingsActivity" /> android:value="emu.skyline.SettingsActivity" />
</activity> </activity>
<activity <activity
android:name="emu.skyline.GameActivity" android:name="emu.skyline.EmulationActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:screenOrientation="landscape"> android:screenOrientation="landscape">

View File

@ -21,7 +21,7 @@ void signalHandler(int signal) {
FaultCount++; FaultCount++;
} }
extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_executeRom(JNIEnv *env, jobject instance, jstring romJstring, jint romType, jint romFd, jint preferenceFd, jint logFd) { extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeRom(JNIEnv *env, jobject instance, jstring romUriJstring, jint romType, jint romFd, jint preferenceFd, jint logFd) {
Halt = false; Halt = false;
FaultCount = 0; FaultCount = 0;
@ -43,9 +43,9 @@ extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_executeRom(JNIEnv *env,
try { try {
skyline::kernel::OS os(jvmManager, logger, settings); skyline::kernel::OS os(jvmManager, logger, settings);
const char *romString = env->GetStringUTFChars(romJstring, nullptr); const char *romUri = env->GetStringUTFChars(romUriJstring, nullptr);
logger->Info("Launching ROM {}", romString); logger->Info("Launching ROM {}", romUri);
env->ReleaseStringUTFChars(romJstring, romString); env->ReleaseStringUTFChars(romUriJstring, romUri);
os.Execute(romFd, static_cast<skyline::TitleFormat>(romType)); os.Execute(romFd, static_cast<skyline::TitleFormat>(romType));
} catch (std::exception &e) { } catch (std::exception &e) {
logger->Error(e.what()); logger->Error(e.what());
@ -58,13 +58,13 @@ extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_executeRom(JNIEnv *env,
logger->Info("Done in: {} ms", (std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count())); logger->Info("Done in: {} ms", (std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()));
} }
extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_setHalt(JNIEnv *env, jobject instance, jboolean halt) { extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_setHalt(JNIEnv *env, jobject instance, jboolean halt) {
JniMtx.lock(skyline::GroupMutex::Group::Group2); JniMtx.lock(skyline::GroupMutex::Group::Group2);
Halt = halt; Halt = halt;
JniMtx.unlock(); JniMtx.unlock();
} }
extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_setSurface(JNIEnv *env, jobject instance, jobject surface) { extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_setSurface(JNIEnv *env, jobject instance, jobject surface) {
JniMtx.lock(skyline::GroupMutex::Group::Group2); JniMtx.lock(skyline::GroupMutex::Group::Group2);
if (!env->IsSameObject(Surface, nullptr)) if (!env->IsSameObject(Surface, nullptr))
env->DeleteGlobalRef(Surface); env->DeleteGlobalRef(Surface);

View File

@ -10,105 +10,175 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import android.view.InputQueue
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import emu.skyline.loader.getTitleFormat import emu.skyline.loader.getRomFormat
import kotlinx.android.synthetic.main.game_activity.* import kotlinx.android.synthetic.main.game_activity.*
import java.io.File import java.io.File
import java.lang.reflect.Method
class GameActivity : AppCompatActivity(), SurfaceHolder.Callback, InputQueue.Callback { /**
* This activity is used for emulation using libskyline
*/
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
init { init {
System.loadLibrary("skyline") // libskyline.so System.loadLibrary("skyline") // libskyline.so
} }
/**
* The file descriptor of the ROM
*/
private lateinit var romFd: ParcelFileDescriptor private lateinit var romFd: ParcelFileDescriptor
private lateinit var preferenceFd: ParcelFileDescriptor
private lateinit var logFd: ParcelFileDescriptor
private var surface: Surface? = null
private var inputQueue: Long = 0L
private var shouldFinish: Boolean = true
private lateinit var gameThread: Thread
private external fun executeRom(romString: String, romType: Int, romFd: Int, preferenceFd: Int, logFd: Int) /**
* The file descriptor of the application Preference XML
*/
private lateinit var preferenceFd: ParcelFileDescriptor
/**
* The file descriptor of the Log file
*/
private lateinit var logFd: ParcelFileDescriptor
/**
* The surface object used for displaying frames
*/
private var surface: Surface? = null
/**
* A boolean flag denoting if the emulation thread should call finish() or not
*/
private var shouldFinish: Boolean = true
/**
* The Kotlin thread on which emulation code executes
*/
private lateinit var emulationThread: Thread
/**
* This is the entry point into the emulation code for libskyline
*
* @param romUri The URI of the ROM as a string, used to print out in the logs
* @param romType The type of the ROM as an enum value
* @param romFd The file descriptor of the ROM object
* @param preferenceFd The file descriptor of the Preference XML
* @param logFd The file descriptor of the Log file
*/
private external fun executeRom(romUri: String, romType: Int, romFd: Int, preferenceFd: Int, logFd: Int)
/**
* This sets the halt flag in libskyline to the provided value, if set to true it causes libskyline to halt emulation
*
* @param halt The value to set halt to
*/
private external fun setHalt(halt: Boolean) private external fun setHalt(halt: Boolean)
/**
* This sets the surface object in libskyline to the provided value, emulation is halted if set to null
*
* @param surface The value to set surface to
*/
private external fun setSurface(surface: Surface?) private external fun setSurface(surface: Surface?)
fun executeRom(rom : Uri) { /**
val romType = getTitleFormat(rom, contentResolver).ordinal * This executes the specified ROM, [preferenceFd] and [logFd] are assumed to be valid beforehand
*
* @param rom The URI of the ROM to execute
*/
private fun executeRom(rom : Uri) {
val romType = getRomFormat(rom, contentResolver).ordinal
romFd = contentResolver.openFileDescriptor(rom, "r")!! romFd = contentResolver.openFileDescriptor(rom, "r")!!
gameThread = Thread {
emulationThread = Thread {
while ((surface == null)) while ((surface == null))
Thread.yield() Thread.yield()
executeRom(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd) executeRom(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd)
if (shouldFinish) if (shouldFinish)
runOnUiThread { finish() } runOnUiThread { finish() }
} }
gameThread.start()
emulationThread.start()
} }
/**
* The onCreate handler for the activity, it sets up the FDs and calls [executeRom]
*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.game_activity) setContentView(R.layout.game_activity)
val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml") val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml")
preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE) preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE)
val log = File("${applicationInfo.dataDir}/skyline.log") val log = File("${applicationInfo.dataDir}/skyline.log")
logFd = ParcelFileDescriptor.open(log, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE) logFd = ParcelFileDescriptor.open(log, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE)
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
game_view.holder.addCallback(this) game_view.holder.addCallback(this)
executeRom(intent.data!!) executeRom(intent.data!!)
} }
/**
* The onNewIntent handler is used to stop the currently executing ROM and replace it with the one specified in the new intent
*/
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
shouldFinish = false shouldFinish = false
setHalt(true) setHalt(true)
gameThread.join() emulationThread.join()
shouldFinish = true shouldFinish = true
romFd.close() romFd.close()
executeRom(intent?.data!!) executeRom(intent?.data!!)
super.onNewIntent(intent) super.onNewIntent(intent)
} }
/**
* The onDestroy handler is used to halt emulation
*/
override fun onDestroy() { override fun onDestroy() {
shouldFinish = false shouldFinish = false
setHalt(true) setHalt(true)
gameThread.join() emulationThread.join()
romFd.close() romFd.close()
preferenceFd.close() preferenceFd.close()
logFd.close() logFd.close()
super.onDestroy() super.onDestroy()
} }
/**
* The surfaceCreated handler passes in the surface to libskyline
*/
override fun surfaceCreated(holder: SurfaceHolder?) { override fun surfaceCreated(holder: SurfaceHolder?) {
Log.d("surfaceCreated", "Holder: ${holder.toString()}") Log.d("surfaceCreated", "Holder: ${holder.toString()}")
surface = holder!!.surface surface = holder!!.surface
setSurface(surface) setSurface(surface)
} }
/**
* The surfaceChanged handler is purely used for debugging purposes
*/
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
Log.d("surfaceChanged", "Holder: ${holder.toString()}, Format: $format, Width: $width, Height: $height") Log.d("surfaceChanged", "Holder: ${holder.toString()}, Format: $format, Width: $width, Height: $height")
} }
/**
* The surfaceDestroyed handler passes sets the surface to null
*/
override fun surfaceDestroyed(holder: SurfaceHolder?) { override fun surfaceDestroyed(holder: SurfaceHolder?) {
Log.d("surfaceDestroyed", "Holder: ${holder.toString()}") Log.d("surfaceDestroyed", "Holder: ${holder.toString()}")
surface = null surface = null
setSurface(surface) setSurface(surface)
} }
override fun onInputQueueCreated(queue: InputQueue?) {
Log.i("onInputQueueCreated", "InputQueue: ${queue.toString()}")
val clazz = Class.forName("android.view.InputQueue")
val method: Method = clazz.getMethod("getNativePtr")
inputQueue = method.invoke(queue)!! as Long
//setQueue(inputQueue)
}
override fun onInputQueueDestroyed(queue: InputQueue?) {
Log.d("onInputQueueDestroyed", "InputQueue: ${queue.toString()}")
inputQueue = 0L
//setQueue(inputQueue)
}
} }

View File

@ -19,8 +19,8 @@ import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.GameAdapter import emu.skyline.adapter.AppAdapter
import emu.skyline.adapter.GameItem import emu.skyline.adapter.AppItem
import emu.skyline.loader.BaseLoader import emu.skyline.loader.BaseLoader
import emu.skyline.loader.NroLoader import emu.skyline.loader.NroLoader
import emu.skyline.utility.GameDialog import emu.skyline.utility.GameDialog
@ -32,7 +32,7 @@ import kotlin.concurrent.thread
class MainActivity : AppCompatActivity(), View.OnClickListener { class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private var adapter = GameAdapter(this) private var adapter = AppAdapter(this)
private fun notifyUser(text: String) { private fun notifyUser(text: String) {
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show() Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show()
@ -49,13 +49,13 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
if (ext.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { if (ext.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
val document = RandomAccessDocument(this, file) val document = RandomAccessDocument(this, file)
if (loader.verifyFile(document)) { if (loader.verifyFile(document)) {
val entry = loader.getTitleEntry(document, file.uri) val entry = loader.getAppEntry(document, file.uri)
runOnUiThread { runOnUiThread {
if (!foundCurrent) { if (!foundCurrent) {
adapter.addHeader(getString(R.string.nro)) adapter.addHeader(loader.format.name)
foundCurrent = true foundCurrent = true
} }
adapter.addItem(GameItem(entry)) adapter.addItem(AppItem(entry))
} }
} }
document.close() document.close()
@ -75,25 +75,31 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
Log.w("refreshFiles", "Ran into exception while loading: ${e.message}") Log.w("refreshFiles", "Ran into exception while loading: ${e.message}")
} }
} }
thread(start = true) { thread(start = true) {
val snackbar = Snackbar.make(findViewById(android.R.id.content), getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE) val snackbar = Snackbar.make(findViewById(android.R.id.content), getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE)
runOnUiThread { snackbar.show() } runOnUiThread { snackbar.show() }
try { try {
runOnUiThread { adapter.clear() } runOnUiThread { adapter.clear() }
val foundNros = findFile("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) val foundNros = findFile("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
runOnUiThread { runOnUiThread {
if (!foundNros) if (!foundNros)
adapter.addHeader(getString(R.string.no_rom)) adapter.addHeader(getString(R.string.no_rom))
try { try {
adapter.save(File("${applicationInfo.dataDir}/roms.bin")) adapter.save(File("${applicationInfo.dataDir}/roms.bin"))
} catch (e: IOException) { } catch (e: IOException) {
Log.w("refreshFiles", "Ran into exception while saving: ${e.message}") Log.w("refreshFiles", "Ran into exception while saving: ${e.message}")
} }
} }
sharedPreferences.edit().putBoolean("refresh_required", false).apply() sharedPreferences.edit().putBoolean("refresh_required", false).apply()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
runOnUiThread { runOnUiThread {
sharedPreferences.edit().remove("search_location").apply() sharedPreferences.edit().remove("search_location").apply()
val intent = intent val intent = intent
finish() finish()
startActivity(intent) startActivity(intent)
@ -103,6 +109,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
notifyUser(e.message!!) notifyUser(e.message!!)
} }
} }
runOnUiThread { snackbar.dismiss() } runOnUiThread { snackbar.dismiss() }
} }
} }
@ -124,15 +131,15 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
game_list.adapter = adapter game_list.adapter = adapter
game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long -> game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long ->
val item = parent.getItemAtPosition(position) val item = parent.getItemAtPosition(position)
if (item is GameItem) { if (item is AppItem) {
val intent = Intent(this, GameActivity::class.java) val intent = Intent(this, EmulationActivity::class.java)
intent.data = item.uri intent.data = item.uri
startActivity(intent) startActivity(intent)
} }
} }
game_list.onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, _, position, _ -> game_list.onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, _, position, _ ->
val item = parent.getItemAtPosition(position) val item = parent.getItemAtPosition(position)
if (item is GameItem) { if (item is AppItem) {
val dialog = GameDialog(item) val dialog = GameDialog(item)
dialog.show(supportFragmentManager, "game") dialog.show(supportFragmentManager, "game")
} }
@ -205,7 +212,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
2 -> { 2 -> {
try { try {
val uri = (intent!!.data!!) val uri = (intent!!.data!!)
val intentGame = Intent(this, GameActivity::class.java) val intentGame = Intent(this, EmulationActivity::class.java)
intentGame.data = uri intentGame.data = uri
if (resultCode != 0) if (resultCode != 0)
startActivityForResult(intentGame, resultCode) startActivityForResult(intentGame, resultCode)

View File

@ -18,39 +18,69 @@ import android.view.Window
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.drawable.toBitmap
import emu.skyline.R import emu.skyline.R
import emu.skyline.loader.TitleEntry import emu.skyline.loader.AppEntry
class GameItem(val meta: TitleEntry) : BaseItem() { /**
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
*/
class AppItem(val meta: AppEntry) : BaseItem() {
/**
* The icon of the application
*/
val icon: Bitmap? val icon: Bitmap?
get() = meta.icon get() = meta.icon
/**
* The title of the application
*/
val title: String val title: String
get() = meta.name + " (" + type + ")" get() = meta.name + " (" + type + ")"
/**
* The string used as the sub-title, we currently use the author
*/
val subTitle: String? val subTitle: String?
get() = meta.author get() = meta.author
/**
* The URI of the application's image file
*/
val uri: Uri val uri: Uri
get() = meta.uri get() = meta.uri
/**
* The format of the application ROM as a string
*/
private val type: String private val type: String
get() = meta.romType.name get() = meta.format.name
override fun key(): String? { override fun key(): String? {
return if (meta.valid) meta.name + " " + meta.author else meta.name return if (meta.author != null) meta.name + " " + meta.author else meta.name
} }
} }
internal class GameAdapter(val context: Context?) : HeaderAdapter<GameItem, BaseHeader>(), View.OnClickListener { /**
* This adapter is used to display all found applications using their metadata
*/
internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHeader>(), View.OnClickListener {
/**
* This adds a string header to the view
*/
fun addHeader(string: String) { fun addHeader(string: String) {
super.addHeader(BaseHeader(string)) super.addHeader(BaseHeader(string))
} }
/**
* The onClick handler, it's for displaying the icon preview
*
* @param view The specific view that was clicked
*/
override fun onClick(view: View) { override fun onClick(view: View) {
val position = view.tag as Int val position = view.tag as Int
if (getItem(position) is GameItem) { if (getItem(position) is AppItem) {
val item = getItem(position) as GameItem val item = getItem(position) as AppItem
if (view.id == R.id.icon) { if (view.id == R.id.icon) {
val builder = Dialog(context!!) val builder = Dialog(context!!)
builder.requestWindowFeature(Window.FEATURE_NO_TITLE) builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
@ -63,44 +93,61 @@ internal class GameAdapter(val context: Context?) : HeaderAdapter<GameItem, Base
} }
} }
/**
* This returns the view for an element at a specific position
*
* @param position The position of the requested item
* @param convertView An existing view (If any)
* @param parent The parent view group used for layout inflation
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView var view = convertView
val viewHolder: ViewHolder val viewHolder: ViewHolder
val item = elementArray[visibleArray[position]] val item = elementArray[visibleArray[position]]
if (view == null) { if (view == null) {
viewHolder = ViewHolder() viewHolder = ViewHolder()
if (item is GameItem) { if (item is AppItem) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
view = inflater.inflate(R.layout.game_item, parent, false) view = inflater.inflate(R.layout.game_item, parent, false)
viewHolder.icon = view.findViewById(R.id.icon) viewHolder.icon = view.findViewById(R.id.icon)
viewHolder.txtTitle = view.findViewById(R.id.text_title) viewHolder.title = view.findViewById(R.id.text_title)
viewHolder.txtSub = view.findViewById(R.id.text_subtitle) viewHolder.subtitle = view.findViewById(R.id.text_subtitle)
view.tag = viewHolder view.tag = viewHolder
} else if (item is BaseHeader) { } else if (item is BaseHeader) {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
view = inflater.inflate(R.layout.section_item, parent, false) view = inflater.inflate(R.layout.section_item, parent, false)
viewHolder.txtTitle = view.findViewById(R.id.text_title)
viewHolder.title = view.findViewById(R.id.text_title)
view.tag = viewHolder view.tag = viewHolder
} }
} else { } else {
viewHolder = view.tag as ViewHolder viewHolder = view.tag as ViewHolder
} }
if (item is GameItem) {
val data = getItem(position) as GameItem if (item is AppItem) {
viewHolder.txtTitle!!.text = data.title val data = getItem(position) as AppItem
viewHolder.txtSub!!.text = data.subTitle
viewHolder.icon!!.setImageBitmap(data.icon) viewHolder.title!!.text = data.title
viewHolder.subtitle!!.text = data.subTitle ?: context?.getString(R.string.metadata_missing)!!
viewHolder.icon!!.setImageBitmap(data.icon ?: context!!.resources.getDrawable(R.drawable.ic_missing, context.theme).toBitmap(256, 256))
viewHolder.icon!!.setOnClickListener(this) viewHolder.icon!!.setOnClickListener(this)
viewHolder.icon!!.tag = position viewHolder.icon!!.tag = position
} else { } else {
viewHolder.txtTitle!!.text = (getItem(position) as BaseHeader).title viewHolder.title!!.text = (getItem(position) as BaseHeader).title
} }
return view!! return view!!
} }
private class ViewHolder { /**
var icon: ImageView? = null * The ViewHolder object is used to hold the views associated with an object
var txtTitle: TextView? = null *
var txtSub: TextView? = null * @param icon The ImageView associated with the icon
} * @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle
*/
private class ViewHolder(var icon: ImageView? = null, var title: TextView? = null, var subtitle: TextView? = null)
} }

View File

@ -24,19 +24,22 @@ enum class ElementType(val type: Int) {
} }
/** /**
* @brief This is the interface class that all element classes inherit from * This is an abstract class that all adapter element classes inherit from
*/ */
abstract class BaseElement constructor(val elementType: ElementType) : Serializable abstract class BaseElement constructor(val elementType: ElementType) : Serializable
/** /**
* @brief This is the interface class that all header classes inherit from * This is an abstract class that all adapter header classes inherit from
*/ */
class BaseHeader constructor(val title: String) : BaseElement(ElementType.Header) class BaseHeader constructor(val title: String) : BaseElement(ElementType.Header)
/** /**
* @brief This is the interface class that all item classes inherit from * This is an abstract class that all adapter item classes inherit from
*/ */
abstract class BaseItem : BaseElement(ElementType.Item) { abstract class BaseItem : BaseElement(ElementType.Item) {
/**
* This function returns a string used for searching
*/
abstract fun key(): String? abstract fun key(): String?
} }

View File

@ -11,8 +11,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.core.graphics.drawable.toBitmap
import emu.skyline.R
import emu.skyline.utility.RandomAccessDocument import emu.skyline.utility.RandomAccessDocument
import java.io.IOException import java.io.IOException
import java.io.ObjectInputStream import java.io.ObjectInputStream
@ -20,52 +18,119 @@ import java.io.ObjectOutputStream
import java.io.Serializable import java.io.Serializable
import java.util.* import java.util.*
enum class TitleFormat { /**
* An enumeration of all supported ROM formats
*/
enum class RomFormat {
NRO, XCI, NSP NRO, XCI, NSP
} }
fun getTitleFormat(uri: Uri, contentResolver: ContentResolver): TitleFormat { /**
* This resolves the format of a ROM from it's URI so we can determine formats for ROMs launched from arbitrary locations
*
* @param uri The URL of the ROM
* @param contentResolver The instance of ContentResolver associated with the current context
*/
fun getRomFormat(uri: Uri, contentResolver: ContentResolver): RomFormat {
var uriStr = "" var uriStr = ""
contentResolver.query(uri, null, null, null, null)?.use { cursor -> contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst() cursor.moveToFirst()
uriStr = cursor.getString(nameIndex) uriStr = cursor.getString(nameIndex)
} }
return TitleFormat.valueOf(uriStr.substring(uriStr.lastIndexOf(".") + 1).toUpperCase(Locale.ROOT)) return RomFormat.valueOf(uriStr.substring(uriStr.lastIndexOf(".") + 1).toUpperCase(Locale.ROOT))
} }
class TitleEntry(var name: String, var author: String, var romType: TitleFormat, var valid: Boolean, var uri: Uri, 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, context.theme).toBitmap(256, 256)) { * This class is used to hold an application's metadata in a serializable way
context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> */
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) class AppEntry : Serializable {
cursor.moveToFirst() /**
name = cursor.getString(nameIndex) * 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)
}!!
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) @Throws(IOException::class)
private fun writeObject(output: ObjectOutputStream) { private fun writeObject(output: ObjectOutputStream) {
output.writeUTF(name) output.writeUTF(name)
output.writeUTF(author) output.writeObject(format)
output.writeObject(romType)
output.writeUTF(uri.toString()) output.writeUTF(uri.toString())
output.writeBoolean(valid) output.writeBoolean(author != null)
icon.compress(Bitmap.CompressFormat.WEBP, 100, output) if (author != null)
output.writeUTF(author)
output.writeBoolean(icon != null)
if (icon != null)
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) @Throws(IOException::class, ClassNotFoundException::class)
private fun readObject(input: ObjectInputStream) { private fun readObject(input: ObjectInputStream) {
name = input.readUTF() name = input.readUTF()
author = input.readUTF() format = input.readObject() as RomFormat
romType = input.readObject() as TitleFormat
uri = Uri.parse(input.readUTF()) uri = Uri.parse(input.readUTF())
valid = input.readBoolean() if (input.readBoolean())
icon = BitmapFactory.decodeStream(input) author = input.readUTF()
if (input.readBoolean())
icon = BitmapFactory.decodeStream(input)
} }
} }
internal abstract class BaseLoader(val context: Context, val romType: TitleFormat) { /**
abstract fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry * This class is used as the base class for all loaders
*/
internal abstract class BaseLoader(val context: Context, val format: RomFormat) {
/**
* This returns an AppEntry object for the supplied document
*/
abstract fun getAppEntry(file: RandomAccessDocument, uri: Uri): AppEntry
/**
* This returns if the supplied document is a valid ROM or not
*/
abstract fun verifyFile(file: RandomAccessDocument): Boolean abstract fun verifyFile(file: RandomAccessDocument): Boolean
} }

View File

@ -8,45 +8,56 @@ package emu.skyline.loader
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import emu.skyline.R
import emu.skyline.utility.RandomAccessDocument import emu.skyline.utility.RandomAccessDocument
import java.io.IOException import java.io.IOException
internal class NroLoader(context: Context) : BaseLoader(context, TitleFormat.NRO) { /**
override fun getTitleEntry(file: RandomAccessDocument, uri: Uri): TitleEntry { * 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) {
override fun getAppEntry(file: RandomAccessDocument, uri: Uri): AppEntry {
return try { return try {
file.seek(0x18) // Skip to NroHeader.size file.seek(0x18) // Skip to NroHeader.size
val asetOffset = Integer.reverseBytes(file.readInt()) val asetOffset = Integer.reverseBytes(file.readInt())
file.seek(asetOffset.toLong()) // Skip to the offset specified by NroHeader.size file.seek(asetOffset.toLong()) // Skip to the offset specified by NroHeader.size
val buffer = ByteArray(4) val buffer = ByteArray(4)
file.read(buffer) file.read(buffer)
if (String(buffer) != "ASET") throw IOException() if (String(buffer) != "ASET") throw IOException()
file.skipBytes(0x4) file.skipBytes(0x4)
val iconOffset = java.lang.Long.reverseBytes(file.readLong()) val iconOffset = java.lang.Long.reverseBytes(file.readLong())
val iconSize = Integer.reverseBytes(file.readInt()) val iconSize = Integer.reverseBytes(file.readInt())
if (iconOffset == 0L || iconSize == 0) throw IOException() if (iconOffset == 0L || iconSize == 0) throw IOException()
file.seek(asetOffset + iconOffset) file.seek(asetOffset + iconOffset)
val iconData = ByteArray(iconSize) val iconData = ByteArray(iconSize)
file.read(iconData) file.read(iconData)
val icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize) val icon = BitmapFactory.decodeByteArray(iconData, 0, iconSize)
file.seek(asetOffset + 0x18.toLong()) file.seek(asetOffset + 0x18.toLong())
val nacpOffset = java.lang.Long.reverseBytes(file.readLong()) val nacpOffset = java.lang.Long.reverseBytes(file.readLong())
val nacpSize = java.lang.Long.reverseBytes(file.readLong()) val nacpSize = java.lang.Long.reverseBytes(file.readLong())
if (nacpOffset == 0L || nacpSize == 0L) throw IOException() if (nacpOffset == 0L || nacpSize == 0L) throw IOException()
file.seek(asetOffset + nacpOffset) file.seek(asetOffset + nacpOffset)
val name = ByteArray(0x200) val name = ByteArray(0x200)
file.read(name) file.read(name)
val author = ByteArray(0x100) val author = ByteArray(0x100)
file.read(author) file.read(author)
TitleEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), romType, true, uri, icon)
AppEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), format, uri, icon)
} catch (e: IOException) { } catch (e: IOException) {
TitleEntry(context, context.getString(R.string.aset_missing), romType, false, uri) AppEntry(context, format, uri)
} }
} }
override fun verifyFile(file: RandomAccessDocument): Boolean { override fun verifyFile(file: RandomAccessDocument): Boolean {
try { try {
file.seek(0x10) // Skip to NroHeader.magic file.seek(0x10) // Skip to NroHeader.magic
val buffer = ByteArray(4) val buffer = ByteArray(4)
file.read(buffer) file.read(buffer)
if (String(buffer) != "NRO0") return false if (String(buffer) != "NRO0") return false

View File

@ -15,15 +15,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import emu.skyline.GameActivity import emu.skyline.EmulationActivity
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GameItem import emu.skyline.adapter.AppItem
import kotlinx.android.synthetic.main.game_dialog.* import kotlinx.android.synthetic.main.game_dialog.*
class GameDialog() : DialogFragment() { class GameDialog() : DialogFragment() {
var item: GameItem? = null var item: AppItem? = null
constructor(item: GameItem) : this() { constructor(item: AppItem) : this() {
this.item = item this.item = item
} }
@ -33,7 +33,7 @@ class GameDialog() : DialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
if (item is GameItem) { if (item is AppItem) {
game_icon.setImageBitmap(item?.icon) game_icon.setImageBitmap(item?.icon)
game_title.text = item?.title game_title.text = item?.title
game_subtitle.text = item?.subTitle game_subtitle.text = item?.subTitle
@ -42,16 +42,16 @@ class GameDialog() : DialogFragment() {
game_pin.setOnClickListener { game_pin.setOnClickListener {
val info = ShortcutInfo.Builder(context, item?.title) val info = ShortcutInfo.Builder(context, item?.title)
info.setShortLabel(item?.meta?.name!!) info.setShortLabel(item?.meta?.name!!)
info.setActivity(ComponentName(context!!, GameActivity::class.java)) info.setActivity(ComponentName(context!!, EmulationActivity::class.java))
info.setIcon(Icon.createWithBitmap(item?.icon)) info.setIcon(Icon.createWithBitmap(item?.icon))
val intent = Intent(context, GameActivity::class.java) val intent = Intent(context, EmulationActivity::class.java)
intent.data = item?.uri intent.data = item?.uri
intent.action = Intent.ACTION_VIEW intent.action = Intent.ACTION_VIEW
info.setIntent(intent) info.setIntent(intent)
shortcutManager.requestPinShortcut(info.build(), null) shortcutManager.requestPinShortcut(info.build(), null)
} }
game_play.setOnClickListener { game_play.setOnClickListener {
val intent = Intent(activity, GameActivity::class.java) val intent = Intent(activity, EmulationActivity::class.java)
intent.data = item?.uri intent.data = item?.uri
startActivity(intent) startActivity(intent)
} }

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".GameActivity"> tools:context=".EmulationActivity">
<SurfaceView <SurfaceView
android:id="@+id/game_view" android:id="@+id/game_view"

View File

@ -8,10 +8,9 @@
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<!-- Main --> <!-- Main -->
<string name="refreshed">The list of ROMs has been refreshed.</string> <string name="refreshed">The list of ROMs has been refreshed.</string>
<string name="aset_missing">ASET Header Missing</string> <string name="metadata_missing">Metadata Missing</string>
<string name="icon">Icon</string> <string name="icon">Icon</string>
<string name="no_rom">Cannot find any ROMs</string> <string name="no_rom">Cannot find any ROMs</string>
<string name="nro">NROs</string>
<string name="pin">Pin</string> <string name="pin">Pin</string>
<string name="play">Play</string> <string name="play">Play</string>
<!-- Toolbar Logger --> <!-- Toolbar Logger -->