mirror of
https://github.com/skyline-emu/skyline.git
synced 2025-01-09 20:49:24 +01:00
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:
parent
f4968793b8
commit
55a9f8e937
@ -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)
|
||||
* 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):
|
||||
#### If a child control-flow statement has brackets, the parent statement must as well
|
||||
|
@ -44,7 +44,7 @@
|
||||
android:value="emu.skyline.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="emu.skyline.GameActivity"
|
||||
android:name="emu.skyline.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleInstance"
|
||||
android:screenOrientation="landscape">
|
||||
|
@ -21,7 +21,7 @@ void signalHandler(int signal) {
|
||||
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;
|
||||
FaultCount = 0;
|
||||
|
||||
@ -43,9 +43,9 @@ extern "C" JNIEXPORT void Java_emu_skyline_GameActivity_executeRom(JNIEnv *env,
|
||||
|
||||
try {
|
||||
skyline::kernel::OS os(jvmManager, logger, settings);
|
||||
const char *romString = env->GetStringUTFChars(romJstring, nullptr);
|
||||
logger->Info("Launching ROM {}", romString);
|
||||
env->ReleaseStringUTFChars(romJstring, romString);
|
||||
const char *romUri = env->GetStringUTFChars(romUriJstring, nullptr);
|
||||
logger->Info("Launching ROM {}", romUri);
|
||||
env->ReleaseStringUTFChars(romUriJstring, romUri);
|
||||
os.Execute(romFd, static_cast<skyline::TitleFormat>(romType));
|
||||
} catch (std::exception &e) {
|
||||
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()));
|
||||
}
|
||||
|
||||
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);
|
||||
Halt = halt;
|
||||
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);
|
||||
if (!env->IsSameObject(Surface, nullptr))
|
||||
env->DeleteGlobalRef(Surface);
|
||||
|
@ -10,105 +10,175 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import android.view.InputQueue
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import emu.skyline.loader.getTitleFormat
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import kotlinx.android.synthetic.main.game_activity.*
|
||||
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 {
|
||||
System.loadLibrary("skyline") // libskyline.so
|
||||
}
|
||||
|
||||
/**
|
||||
* The file descriptor of the ROM
|
||||
*/
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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?)
|
||||
|
||||
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")!!
|
||||
gameThread = Thread {
|
||||
|
||||
emulationThread = Thread {
|
||||
while ((surface == null))
|
||||
Thread.yield()
|
||||
|
||||
executeRom(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd)
|
||||
|
||||
if (shouldFinish)
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.game_activity)
|
||||
|
||||
val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml")
|
||||
preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE)
|
||||
|
||||
val log = File("${applicationInfo.dataDir}/skyline.log")
|
||||
logFd = ParcelFileDescriptor.open(log, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE)
|
||||
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
game_view.holder.addCallback(this)
|
||||
|
||||
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?) {
|
||||
shouldFinish = false
|
||||
|
||||
setHalt(true)
|
||||
gameThread.join()
|
||||
emulationThread.join()
|
||||
|
||||
shouldFinish = true
|
||||
|
||||
romFd.close()
|
||||
|
||||
executeRom(intent?.data!!)
|
||||
|
||||
super.onNewIntent(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* The onDestroy handler is used to halt emulation
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
shouldFinish = false
|
||||
|
||||
setHalt(true)
|
||||
gameThread.join()
|
||||
emulationThread.join()
|
||||
|
||||
romFd.close()
|
||||
preferenceFd.close()
|
||||
logFd.close()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* The surfaceCreated handler passes in the surface to libskyline
|
||||
*/
|
||||
override fun surfaceCreated(holder: SurfaceHolder?) {
|
||||
Log.d("surfaceCreated", "Holder: ${holder.toString()}")
|
||||
surface = holder!!.surface
|
||||
setSurface(surface)
|
||||
}
|
||||
|
||||
/**
|
||||
* The surfaceChanged handler is purely used for debugging purposes
|
||||
*/
|
||||
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
|
||||
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?) {
|
||||
Log.d("surfaceDestroyed", "Holder: ${holder.toString()}")
|
||||
surface = null
|
||||
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)
|
||||
}
|
||||
}
|
@ -19,8 +19,8 @@ import androidx.appcompat.widget.SearchView
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.adapter.GameAdapter
|
||||
import emu.skyline.adapter.GameItem
|
||||
import emu.skyline.adapter.AppAdapter
|
||||
import emu.skyline.adapter.AppItem
|
||||
import emu.skyline.loader.BaseLoader
|
||||
import emu.skyline.loader.NroLoader
|
||||
import emu.skyline.utility.GameDialog
|
||||
@ -32,7 +32,7 @@ import kotlin.concurrent.thread
|
||||
|
||||
class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private var adapter = GameAdapter(this)
|
||||
private var adapter = AppAdapter(this)
|
||||
|
||||
private fun notifyUser(text: String) {
|
||||
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)) {
|
||||
val document = RandomAccessDocument(this, file)
|
||||
if (loader.verifyFile(document)) {
|
||||
val entry = loader.getTitleEntry(document, file.uri)
|
||||
val entry = loader.getAppEntry(document, file.uri)
|
||||
runOnUiThread {
|
||||
if (!foundCurrent) {
|
||||
adapter.addHeader(getString(R.string.nro))
|
||||
adapter.addHeader(loader.format.name)
|
||||
foundCurrent = true
|
||||
}
|
||||
adapter.addItem(GameItem(entry))
|
||||
adapter.addItem(AppItem(entry))
|
||||
}
|
||||
}
|
||||
document.close()
|
||||
@ -75,25 +75,31 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
Log.w("refreshFiles", "Ran into exception while loading: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
thread(start = true) {
|
||||
val snackbar = Snackbar.make(findViewById(android.R.id.content), getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE)
|
||||
runOnUiThread { snackbar.show() }
|
||||
|
||||
try {
|
||||
runOnUiThread { adapter.clear() }
|
||||
val foundNros = findFile("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
|
||||
|
||||
runOnUiThread {
|
||||
if (!foundNros)
|
||||
adapter.addHeader(getString(R.string.no_rom))
|
||||
|
||||
try {
|
||||
adapter.save(File("${applicationInfo.dataDir}/roms.bin"))
|
||||
} catch (e: IOException) {
|
||||
Log.w("refreshFiles", "Ran into exception while saving: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
sharedPreferences.edit().putBoolean("refresh_required", false).apply()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
runOnUiThread {
|
||||
sharedPreferences.edit().remove("search_location").apply()
|
||||
|
||||
val intent = intent
|
||||
finish()
|
||||
startActivity(intent)
|
||||
@ -103,6 +109,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
notifyUser(e.message!!)
|
||||
}
|
||||
}
|
||||
|
||||
runOnUiThread { snackbar.dismiss() }
|
||||
}
|
||||
}
|
||||
@ -124,15 +131,15 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
game_list.adapter = adapter
|
||||
game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long ->
|
||||
val item = parent.getItemAtPosition(position)
|
||||
if (item is GameItem) {
|
||||
val intent = Intent(this, GameActivity::class.java)
|
||||
if (item is AppItem) {
|
||||
val intent = Intent(this, EmulationActivity::class.java)
|
||||
intent.data = item.uri
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
game_list.onItemLongClickListener = AdapterView.OnItemLongClickListener { parent, _, position, _ ->
|
||||
val item = parent.getItemAtPosition(position)
|
||||
if (item is GameItem) {
|
||||
if (item is AppItem) {
|
||||
val dialog = GameDialog(item)
|
||||
dialog.show(supportFragmentManager, "game")
|
||||
}
|
||||
@ -205,7 +212,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
|
||||
2 -> {
|
||||
try {
|
||||
val uri = (intent!!.data!!)
|
||||
val intentGame = Intent(this, GameActivity::class.java)
|
||||
val intentGame = Intent(this, EmulationActivity::class.java)
|
||||
intentGame.data = uri
|
||||
if (resultCode != 0)
|
||||
startActivityForResult(intentGame, resultCode)
|
||||
|
@ -18,39 +18,69 @@ import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
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?
|
||||
get() = meta.icon
|
||||
|
||||
/**
|
||||
* The title of the application
|
||||
*/
|
||||
val title: String
|
||||
get() = meta.name + " (" + type + ")"
|
||||
|
||||
/**
|
||||
* The string used as the sub-title, we currently use the author
|
||||
*/
|
||||
val subTitle: String?
|
||||
get() = meta.author
|
||||
|
||||
/**
|
||||
* The URI of the application's image file
|
||||
*/
|
||||
val uri: Uri
|
||||
get() = meta.uri
|
||||
|
||||
/**
|
||||
* The format of the application ROM as a string
|
||||
*/
|
||||
private val type: String
|
||||
get() = meta.romType.name
|
||||
get() = meta.format.name
|
||||
|
||||
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) {
|
||||
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) {
|
||||
val position = view.tag as Int
|
||||
if (getItem(position) is GameItem) {
|
||||
val item = getItem(position) as GameItem
|
||||
if (getItem(position) is AppItem) {
|
||||
val item = getItem(position) as AppItem
|
||||
if (view.id == R.id.icon) {
|
||||
val builder = Dialog(context!!)
|
||||
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 {
|
||||
var view = convertView
|
||||
val viewHolder: ViewHolder
|
||||
val item = elementArray[visibleArray[position]]
|
||||
|
||||
if (view == null) {
|
||||
viewHolder = ViewHolder()
|
||||
if (item is GameItem) {
|
||||
if (item is AppItem) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
view = inflater.inflate(R.layout.game_item, parent, false)
|
||||
|
||||
viewHolder.icon = view.findViewById(R.id.icon)
|
||||
viewHolder.txtTitle = view.findViewById(R.id.text_title)
|
||||
viewHolder.txtSub = view.findViewById(R.id.text_subtitle)
|
||||
viewHolder.title = view.findViewById(R.id.text_title)
|
||||
viewHolder.subtitle = view.findViewById(R.id.text_subtitle)
|
||||
view.tag = viewHolder
|
||||
} else if (item is BaseHeader) {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
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
|
||||
}
|
||||
} else {
|
||||
viewHolder = view.tag as ViewHolder
|
||||
}
|
||||
if (item is GameItem) {
|
||||
val data = getItem(position) as GameItem
|
||||
viewHolder.txtTitle!!.text = data.title
|
||||
viewHolder.txtSub!!.text = data.subTitle
|
||||
viewHolder.icon!!.setImageBitmap(data.icon)
|
||||
|
||||
if (item is AppItem) {
|
||||
val data = getItem(position) as AppItem
|
||||
|
||||
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!!.tag = position
|
||||
} else {
|
||||
viewHolder.txtTitle!!.text = (getItem(position) as BaseHeader).title
|
||||
viewHolder.title!!.text = (getItem(position) as BaseHeader).title
|
||||
}
|
||||
|
||||
return view!!
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
var icon: ImageView? = null
|
||||
var txtTitle: TextView? = null
|
||||
var txtSub: TextView? = null
|
||||
}
|
||||
/**
|
||||
* The ViewHolder object is used to hold the views associated with an object
|
||||
*
|
||||
* @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)
|
||||
}
|
@ -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
|
||||
|
||||
/**
|
||||
* @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)
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
/**
|
||||
* This function returns a string used for searching
|
||||
*/
|
||||
abstract fun key(): String?
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,6 @@ 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
|
||||
@ -20,52 +18,119 @@ import java.io.ObjectOutputStream
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
enum class TitleFormat {
|
||||
/**
|
||||
* An enumeration of all supported ROM formats
|
||||
*/
|
||||
enum class RomFormat {
|
||||
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 = ""
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
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)) {
|
||||
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
name = cursor.getString(nameIndex)
|
||||
}
|
||||
/**
|
||||
* 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)
|
||||
}!!
|
||||
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.writeUTF(author)
|
||||
output.writeObject(romType)
|
||||
output.writeObject(format)
|
||||
output.writeUTF(uri.toString())
|
||||
output.writeBoolean(valid)
|
||||
icon.compress(Bitmap.CompressFormat.WEBP, 100, output)
|
||||
output.writeBoolean(author != null)
|
||||
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)
|
||||
private fun readObject(input: ObjectInputStream) {
|
||||
name = input.readUTF()
|
||||
author = input.readUTF()
|
||||
romType = input.readObject() as TitleFormat
|
||||
format = input.readObject() as RomFormat
|
||||
uri = Uri.parse(input.readUTF())
|
||||
valid = input.readBoolean()
|
||||
icon = BitmapFactory.decodeStream(input)
|
||||
if (input.readBoolean())
|
||||
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
|
||||
}
|
||||
|
@ -8,45 +8,56 @@ 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 {
|
||||
/**
|
||||
* 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 {
|
||||
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)
|
||||
|
||||
AppEntry(String(name).substringBefore((0.toChar())), String(author).substringBefore((0.toChar())), format, uri, icon)
|
||||
} catch (e: IOException) {
|
||||
TitleEntry(context, context.getString(R.string.aset_missing), romType, false, uri)
|
||||
AppEntry(context, format, 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
|
||||
|
@ -15,15 +15,15 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import emu.skyline.GameActivity
|
||||
import emu.skyline.EmulationActivity
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GameItem
|
||||
import emu.skyline.adapter.AppItem
|
||||
import kotlinx.android.synthetic.main.game_dialog.*
|
||||
|
||||
class GameDialog() : DialogFragment() {
|
||||
var item: GameItem? = null
|
||||
var item: AppItem? = null
|
||||
|
||||
constructor(item: GameItem) : this() {
|
||||
constructor(item: AppItem) : this() {
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ class GameDialog() : DialogFragment() {
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
if (item is GameItem) {
|
||||
if (item is AppItem) {
|
||||
game_icon.setImageBitmap(item?.icon)
|
||||
game_title.text = item?.title
|
||||
game_subtitle.text = item?.subTitle
|
||||
@ -42,16 +42,16 @@ class GameDialog() : DialogFragment() {
|
||||
game_pin.setOnClickListener {
|
||||
val info = ShortcutInfo.Builder(context, item?.title)
|
||||
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))
|
||||
val intent = Intent(context, GameActivity::class.java)
|
||||
val intent = Intent(context, EmulationActivity::class.java)
|
||||
intent.data = item?.uri
|
||||
intent.action = Intent.ACTION_VIEW
|
||||
info.setIntent(intent)
|
||||
shortcutManager.requestPinShortcut(info.build(), null)
|
||||
}
|
||||
game_play.setOnClickListener {
|
||||
val intent = Intent(activity, GameActivity::class.java)
|
||||
val intent = Intent(activity, EmulationActivity::class.java)
|
||||
intent.data = item?.uri
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".GameActivity">
|
||||
tools:context=".EmulationActivity">
|
||||
|
||||
<SurfaceView
|
||||
android:id="@+id/game_view"
|
||||
|
@ -8,10 +8,9 @@
|
||||
<string name="refresh">Refresh</string>
|
||||
<!-- Main -->
|
||||
<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="no_rom">Cannot find any ROMs</string>
|
||||
<string name="nro">NROs</string>
|
||||
<string name="pin">Pin</string>
|
||||
<string name="play">Play</string>
|
||||
<!-- Toolbar Logger -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user