Refactor Activities and Improve MainActivity

This commit mainly refactors all activities to bring them in-line with the guidelines and makes certain improvements such as using `Snackbar`s rather than `Toast`s where possible, Using `CoordinatorLayout` to allow the app bar to hide itself when possible, the app bar has also been consolidated into it's own layout file to increase layout redraw performance as existing views can be used.
This commit is contained in:
◱ PixelyIon 2020-04-13 01:59:19 +05:30 committed by ◱ PixelyIon
parent 0a5460f4fc
commit 4500f54e85
11 changed files with 216 additions and 140 deletions

View File

@ -21,7 +21,7 @@ void signalHandler(int signal) {
FaultCount++; FaultCount++;
} }
extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeRom(JNIEnv *env, jobject instance, jstring romUriJstring, jint romType, jint romFd, jint preferenceFd, jint logFd) { extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(JNIEnv *env, jobject instance, jstring romUriJstring, jint romType, jint romFd, jint preferenceFd, jint logFd) {
Halt = false; Halt = false;
FaultCount = 0; FaultCount = 0;

View File

@ -18,9 +18,6 @@ import emu.skyline.loader.getRomFormat
import kotlinx.android.synthetic.main.app_activity.* import kotlinx.android.synthetic.main.app_activity.*
import java.io.File import java.io.File
/**
* This activity is used for emulation using libskyline
*/
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
init { init {
System.loadLibrary("skyline") // libskyline.so System.loadLibrary("skyline") // libskyline.so
@ -65,7 +62,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
* @param preferenceFd The file descriptor of the Preference XML * @param preferenceFd The file descriptor of the Preference XML
* @param logFd The file descriptor of the Log file * @param logFd The file descriptor of the Log file
*/ */
private external fun executeRom(romUri: String, romType: Int, romFd: Int, preferenceFd: Int, logFd: Int) private external fun executeApplication(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 * This sets the halt flag in libskyline to the provided value, if set to true it causes libskyline to halt emulation
@ -86,7 +83,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
* *
* @param rom The URI of the ROM to execute * @param rom The URI of the ROM to execute
*/ */
private fun executeRom(rom : Uri) { private fun executeApplication(rom: Uri) {
val romType = getRomFormat(rom, contentResolver).ordinal val romType = getRomFormat(rom, contentResolver).ordinal
romFd = contentResolver.openFileDescriptor(rom, "r")!! romFd = contentResolver.openFileDescriptor(rom, "r")!!
@ -94,7 +91,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
while ((surface == null)) while ((surface == null))
Thread.yield() Thread.yield()
executeRom(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd) executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, logFd.fd)
if (shouldFinish) if (shouldFinish)
runOnUiThread { finish() } runOnUiThread { finish() }
@ -104,12 +101,12 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
} }
/** /**
* The onCreate handler for the activity, it sets up the FDs and calls [executeRom] * This sets up [preferenceFd] and [logFd] then calls [executeApplication] for executing the application
*/ */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.game_activity) setContentView(R.layout.app_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)
@ -120,11 +117,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
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!!) executeApplication(intent.data!!)
} }
/** /**
* The onNewIntent handler is used to stop the currently executing ROM and replace it with the one specified in the new intent * This 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
@ -136,13 +133,13 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
romFd.close() romFd.close()
executeRom(intent?.data!!) executeApplication(intent?.data!!)
super.onNewIntent(intent) super.onNewIntent(intent)
} }
/** /**
* The onDestroy handler is used to halt emulation * This is used to halt emulation entirely
*/ */
override fun onDestroy() { override fun onDestroy() {
shouldFinish = false shouldFinish = false
@ -158,7 +155,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
} }
/** /**
* The surfaceCreated handler passes in the surface to libskyline * This sets [surface] to [holder].surface and passes it into libskyline
*/ */
override fun surfaceCreated(holder: SurfaceHolder?) { override fun surfaceCreated(holder: SurfaceHolder?) {
Log.d("surfaceCreated", "Holder: ${holder.toString()}") Log.d("surfaceCreated", "Holder: ${holder.toString()}")
@ -167,14 +164,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
} }
/** /**
* The surfaceChanged handler is purely used for debugging purposes * This is purely used for debugging surface changes
*/ */
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 * This sets [surface] to null and passes it into libskyline
*/ */
override fun surfaceDestroyed(holder: SurfaceHolder?) { override fun surfaceDestroyed(holder: SurfaceHolder?) {
Log.d("surfaceDestroyed", "Holder: ${holder.toString()}") Log.d("surfaceDestroyed", "Holder: ${holder.toString()}")

View File

@ -10,7 +10,6 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.ListView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
@ -18,23 +17,37 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.LogAdapter import emu.skyline.adapter.LogAdapter
import kotlinx.android.synthetic.main.log_activity.* import kotlinx.android.synthetic.main.log_activity.*
import kotlinx.android.synthetic.main.titlebar.*
import org.json.JSONObject import org.json.JSONObject
import java.io.* import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URL import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.stream.Collectors
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
class LogActivity : AppCompatActivity() { class LogActivity : AppCompatActivity() {
/**
* The log file is used to read log entries from or to clear all entries
*/
private lateinit var logFile: File private lateinit var logFile: File
/**
* The adapter used for adding elements from the log to [log_list]
*/
private lateinit var adapter: LogAdapter private lateinit var adapter: LogAdapter
/**
* This initializes [toolbar] and fills [log_list] with data from the logs
*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.log_activity) setContentView(R.layout.log_activity)
setSupportActionBar(findViewById(R.id.toolbar))
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
@ -51,24 +64,30 @@ class LogActivity : AppCompatActivity() {
try { try {
logFile = File("${applicationInfo.dataDir}/skyline.log") logFile = File("${applicationInfo.dataDir}/skyline.log")
logFile.forEachLine { logFile.forEachLine {
adapter.add(it) adapter.add(it)
} }
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
Log.w("Logger", "IO Error during access of log file: " + e.message) Log.w("Logger", "IO Error during access of log file: " + e.message)
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()
finish() finish()
} catch (e: IOException) { } catch (e: IOException) {
Log.w("Logger", "IO Error during access of log file: " + e.message) Log.w("Logger", "IO Error during access of log file: " + e.message)
Toast.makeText(applicationContext, getString(R.string.io_error) + ": " + e.message, Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.error) + ": ${e.localizedMessage}", Toast.LENGTH_LONG).show()
} }
} }
/**
* This inflates the layout for the menu [R.menu.toolbar_log] and sets up searching the logs
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.toolbar_log, menu) menuInflater.inflate(R.menu.toolbar_log, menu)
val mSearch = menu.findItem(R.id.action_search_log)
val searchView = mSearch.actionView as SearchView val searchView = menu.findItem(R.id.action_search_log).actionView as SearchView
searchView.isSubmitButtonEnabled = false searchView.isSubmitButtonEnabled = false
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
searchView.isIconified = false searchView.isIconified = false
@ -80,9 +99,13 @@ class LogActivity : AppCompatActivity() {
return true return true
} }
}) })
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
/**
* This handles menu selection for [R.id.action_clear] and [R.id.action_share_log]
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_clear -> { R.id.action_clear -> {
@ -90,57 +113,67 @@ class LogActivity : AppCompatActivity() {
logFile.writeText("") logFile.writeText("")
} catch (e: IOException) { } catch (e: IOException) {
Log.w("Logger", "IO Error while clearing the log file: " + e.message) Log.w("Logger", "IO Error while clearing the log file: " + e.message)
Toast.makeText(applicationContext, getString(R.string.io_error) + ": " + e.message, Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.error) + ": ${e.localizedMessage}", Toast.LENGTH_LONG).show()
} }
Toast.makeText(applicationContext, getString(R.string.cleared), Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.cleared), Toast.LENGTH_LONG).show()
finish() finish()
true true
} }
R.id.action_share_log -> { R.id.action_share_log -> {
uploadAndShareLog() uploadAndShareLog()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
/**
* This uploads the logs and launches the [Intent.ACTION_SEND] intent
*/
private fun uploadAndShareLog() { private fun uploadAndShareLog() {
Snackbar.make(findViewById(android.R.id.content), getString(R.string.upload_logs), Snackbar.LENGTH_SHORT).show()
val shareThread = Thread(Runnable { val shareThread = Thread(Runnable {
var urlConnection: HttpsURLConnection? = null var urlConnection: HttpsURLConnection? = null
try { try {
val url = URL("https://hastebin.com/documents") val url = URL("https://hastebin.com/documents")
urlConnection = url.openConnection() as HttpsURLConnection urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.requestMethod = "POST" urlConnection.requestMethod = "POST"
urlConnection.setRequestProperty("Host", "hastebin.com") urlConnection.setRequestProperty("Host", "hastebin.com")
urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8") urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8")
urlConnection.setRequestProperty("Referer", "https://hastebin.com/") urlConnection.setRequestProperty("Referer", "https://hastebin.com/")
val bufferedWriter = urlConnection.outputStream.bufferedWriter() val bufferedWriter = urlConnection.outputStream.bufferedWriter()
bufferedWriter.write(logFile.readText()) bufferedWriter.write(logFile.readText())
bufferedWriter.flush() bufferedWriter.flush()
bufferedWriter.close() bufferedWriter.close()
if (urlConnection.responseCode != 200) { if (urlConnection.responseCode != 200) {
Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.responseCode) Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.responseCode)
throw Exception() throw Exception()
} }
val bufferedReader = urlConnection.inputStream.bufferedReader() val bufferedReader = urlConnection.inputStream.bufferedReader()
val key = JSONObject(bufferedReader.readText()).getString("key") val key = JSONObject(bufferedReader.readText()).getString("key")
bufferedReader.close() bufferedReader.close()
val result = "https://hastebin.com/$key" val result = "https://hastebin.com/$key"
val sharingIntent = Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, result) val sharingIntent = Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, result)
startActivity(Intent.createChooser(sharingIntent, "Share log url with:")) startActivity(Intent.createChooser(sharingIntent, "Share log url with:"))
} catch (e: Exception) { } catch (e: Exception) {
runOnUiThread { Toast.makeText(applicationContext, getString(R.string.share_error), Toast.LENGTH_LONG).show() } runOnUiThread { Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_LONG).show() }
e.printStackTrace() e.printStackTrace()
} finally { } finally {
urlConnection!!.disconnect() urlConnection!!.disconnect()
} }
}) })
shareThread.start() shareThread.start()
try {
shareThread.join(1000)
} catch (e: Exception) {
Toast.makeText(applicationContext, getString(R.string.share_error), Toast.LENGTH_LONG).show()
e.printStackTrace()
}
} }
} }

View File

@ -11,8 +11,6 @@ import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
@ -32,48 +30,63 @@ import emu.skyline.loader.NroLoader
import emu.skyline.utility.AppDialog import emu.skyline.utility.AppDialog
import emu.skyline.utility.RandomAccessDocument import emu.skyline.utility.RandomAccessDocument
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.titlebar.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.ceil import kotlin.math.ceil
class MainActivity : AppCompatActivity(), View.OnClickListener { class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClickListener {
/**
* This is used to get/set shared preferences
*/
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private var adapter = AppAdapter(this)
private fun notifyUser(text: String) { /**
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show() * The adapter used for adding elements to [app_list]
} */
private lateinit var adapter: AppAdapter
private fun findFile(ext: String, loader: BaseLoader, directory: DocumentFile, found: Boolean = false): Boolean { /**
* 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 {
var foundCurrent = found var foundCurrent = found
directory.listFiles() directory.listFiles().forEach { file ->
.forEach { file -> if (file.isDirectory) {
if (file.isDirectory) { foundCurrent = addEntries(extension, loader, file, foundCurrent)
foundCurrent = findFile(ext, loader, file, foundCurrent) } else {
} else { if (extension.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.getAppEntry(document, file.uri) val entry = loader.getAppEntry(document, file.uri)
runOnUiThread {
if (!foundCurrent) { runOnUiThread {
adapter.addHeader(loader.format.name) if (!foundCurrent) {
foundCurrent = true adapter.addHeader(loader.format.name)
} foundCurrent = true
adapter.addItem(AppItem(entry))
}
} }
document.close()
adapter.addItem(AppItem(entry))
} }
} }
document.close()
} }
}
}
return foundCurrent return foundCurrent
} }
private fun refreshFiles(tryLoad: Boolean) { /**
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
*
* @param tryLoad If this is false then trying to load cached adapter data is skipped entirely
*/
private fun refreshAdapter(tryLoad: Boolean) {
if (tryLoad) { if (tryLoad) {
try { try {
adapter.load(File("${applicationInfo.dataDir}/roms.bin")) adapter.load(File("${applicationInfo.dataDir}/roms.bin"))
@ -89,7 +102,8 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
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 = addEntries("nro", NroLoader(this), DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!)
runOnUiThread { runOnUiThread {
if (!foundNros) if (!foundNros)
@ -113,7 +127,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
} catch (e: Exception) { } catch (e: Exception) {
runOnUiThread { runOnUiThread {
notifyUser(e.message!!) Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_SHORT).show()
} }
} }
@ -121,18 +135,26 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
} }
/**
* This initializes [toolbar], [open_fab], [log_fab] and [app_list]
*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity) setContentView(R.layout.main_activity)
setSupportActionBar(toolbar)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false) PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) { AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO 0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES 1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
}) })
setSupportActionBar(toolbar)
open_fab.setOnClickListener(this) open_fab.setOnClickListener(this)
log_fab.setOnClickListener(this) log_fab.setOnClickListener(this)
@ -157,22 +179,25 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
app_list.layoutManager = layoutManager app_list.layoutManager = layoutManager
} }
true
} }
if (sharedPreferences.getString("search_location", "") == "") { if (sharedPreferences.getString("search_location", "") == "") {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
startActivityForResult(intent, 1) startActivityForResult(intent, 1)
} else } else
refreshFiles(!sharedPreferences.getBoolean("refresh_required", false)) refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false))
} }
/**
* This inflates the layout for the menu [R.menu.toolbar_main] and sets up searching the logs
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.toolbar_main, menu) menuInflater.inflate(R.menu.toolbar_main, menu)
val mSearch = menu.findItem(R.id.action_search_main)
val searchView = mSearch.actionView as SearchView val searchView = menu.findItem(R.id.action_search_main).actionView as SearchView
searchView.isSubmitButtonEnabled = false
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
searchView.clearFocus() searchView.clearFocus()
@ -184,19 +209,26 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
return true return true
} }
}) })
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
/**
* This handles on-click interaction with [R.id.log_fab], [R.id.open_fab], [R.id.app_item_linear] and [R.id.app_item_grid]
*/
override fun onClick(view: View) { override fun onClick(view: View) {
when (view.id) { when (view.id) {
R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java)) R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java))
R.id.open_fab -> { R.id.open_fab -> {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*" intent.type = "*/*"
startActivityForResult(intent, 2) startActivityForResult(intent, 2)
} }
R.id.app_item_linear -> {
R.id.app_item_linear, R.id.app_item_grid -> {
val tag = view.tag val tag = view.tag
if (tag is AppItem) { if (tag is AppItem) {
@ -209,9 +241,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
} }
/**
* This handles long-click interaction with [R.id.app_item_linear] and [R.id.app_item_grid]
*/
override fun onLongClick(view: View?): Boolean { override fun onLongClick(view: View?): Boolean {
when (view?.id) { when (view?.id) {
R.id.app_item_linear -> { R.id.app_item_linear, R.id.app_item_grid -> {
val tag = view.tag val tag = view.tag
if (tag is AppItem) { if (tag is AppItem) {
@ -225,47 +260,58 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
return false return false
} }
/**
* This handles menu interaction for [R.id.action_settings] and [R.id.action_refresh]
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.action_settings -> { R.id.action_settings -> {
startActivityForResult(Intent(this, SettingsActivity::class.java), 3) startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
true true
} }
R.id.action_refresh -> { R.id.action_refresh -> {
refreshFiles(false) refreshAdapter(false)
notifyUser(getString(R.string.refreshed))
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
/**
* This handles receiving activity result from [Intent.ACTION_OPEN_DOCUMENT_TREE], [Intent.ACTION_OPEN_DOCUMENT] and [SettingsActivity]
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
when (requestCode) { when (requestCode) {
1 -> { 1 -> {
val uri = intent!!.data!! val uri = intent!!.data!!
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
sharedPreferences.edit().putString("search_location", uri.toString()).apply() sharedPreferences.edit().putString("search_location", uri.toString()).apply()
refreshFiles(!sharedPreferences.getBoolean("refresh_required", false))
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false))
} }
2 -> { 2 -> {
try { try {
val uri = (intent!!.data!!)
val intentGame = Intent(this, EmulationActivity::class.java) val intentGame = Intent(this, EmulationActivity::class.java)
intentGame.data = uri intentGame.data = intent!!.data!!
if (resultCode != 0) if (resultCode != 0)
startActivityForResult(intentGame, resultCode) startActivityForResult(intentGame, resultCode)
else else
startActivity(intentGame) startActivity(intentGame)
} catch (e: Exception) { } catch (e: Exception) {
notifyUser(e.message!!) Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_SHORT).show()
} }
} }
3 -> { 3 -> {
if (sharedPreferences.getBoolean("refresh_required", false)) if (sharedPreferences.getBoolean("refresh_required", false))
refreshFiles(false) refreshAdapter(false)
} }
} }
} }

View File

@ -9,33 +9,54 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import kotlinx.android.synthetic.main.log_activity.* import kotlinx.android.synthetic.main.titlebar.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
/**
* This is the instance of [PreferenceFragment] that is shown inside [R.id.settings]
*/
private val preferenceFragment: PreferenceFragment = PreferenceFragment() private val preferenceFragment: PreferenceFragment = PreferenceFragment()
/**
* This initializes [toolbar] and [R.id.settings]
*/
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity) setContentView(R.layout.settings_activity)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings, preferenceFragment) .replace(R.id.settings, preferenceFragment)
.commit() .commit()
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }
/**
* This is used to refresh the preferences after [emu.skyline.preference.FolderActivity] has returned
*/
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
preferenceFragment.refreshPreferences() preferenceFragment.refreshPreferences()
} }
/**
* This fragment is used to display all of the preferences and handle refreshing the preferences
*/
class PreferenceFragment : PreferenceFragmentCompat() { class PreferenceFragment : PreferenceFragmentCompat() {
/**
* This clears the preference screen and reloads all preferences
*/
fun refreshPreferences() { fun refreshPreferences() {
preferenceScreen = null preferenceScreen = null
addPreferencesFromResource(R.xml.preferences) addPreferencesFromResource(R.xml.preferences)
} }
/**
* This constructs the preferences from [R.xml.preferences]
*/
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey) setPreferencesFromResource(R.xml.preferences, rootKey)
} }

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="32dp"
android:height="24dp" android:height="32dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="?attr/colorOnSecondary" android:fillColor="?attr/colorOnSecondary"
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z" /> android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z" />
</vector> </vector>

View File

@ -1,22 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
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=".LogActivity"> tools:context=".LogActivity">
<androidx.appcompat.widget.Toolbar <include layout="@layout/titlebar" />
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/AppTheme.ActionBar"
app:popupTheme="@style/AppTheme.PopupMenu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_list" android:id="@+id/log_list"
@ -24,6 +14,5 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:transcriptMode="normal" android:transcriptMode="normal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_constraintTop_toBottomOf="@+id/toolbar" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,61 +1,45 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
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=".MainActivity"> tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar <include layout="@layout/titlebar" />
android:id="@+id/toolbar"
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list" android:id="@+id/app_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
android:minHeight="?attr/actionBarSize"
android:theme="@style/AppTheme.ActionBar"
app:popupTheme="@style/AppTheme.PopupMenu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ListView
android:id="@+id/game_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="8dp" android:layout_gravity="bottom|end"
android:orientation="vertical" android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent" android:orientation="vertical">
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/open_fab" android:id="@+id/open_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:clickable="true" android:padding="8dp"
android:focusable="true" app:maxImageSize="26dp"
app:srcCompat="@drawable/ic_open" /> app:srcCompat="@drawable/ic_open" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/log_fab" android:id="@+id/log_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:padding="8dp"
android:layout_marginBottom="8dp" app:maxImageSize="26dp"
android:clickable="true"
android:focusable="true"
app:srcCompat="@drawable/ic_log" /> app:srcCompat="@drawable/ic_log" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -4,17 +4,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.Toolbar <include layout="@layout/titlebar"/>
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/AppTheme.ActionBar"
app:popupTheme="@style/AppTheme.PopupMenu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout <FrameLayout
android:id="@+id/settings" android:id="@+id/settings"

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|snap"
android:theme="@style/AppTheme.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>

View File

@ -2,24 +2,25 @@
<string name="app_name">Skyline</string> <string name="app_name">Skyline</string>
<!-- Common --> <!-- Common -->
<string name="search">Search</string> <string name="search">Search</string>
<string name="error">An error has occurred</string>
<!-- Toolbar Main --> <!-- Toolbar Main -->
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="log">Logger</string> <string name="log">Logger</string>
<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="metadata_missing">Metadata 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="pin">Pin</string> <string name="pin">Pin</string>
<string name="play">Play</string> <string name="play">Play</string>
<string name="searching_roms">Searching for ROMs</string>
<!-- Toolbar Logger --> <!-- Toolbar Logger -->
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="share">Share</string> <string name="share">Share</string>
<!-- Logger --> <!-- Logger -->
<string name="file_missing">The log file was not found</string> <string name="file_missing">The log file was not found</string>
<string name="io_error">An I/O error has occurred</string> <string name="io_error">An I/O error has occurred</string>
<string name="share_error">An error has occurred while sharing</string> <string name="upload_logs">The logs are being uploaded</string>
<string name="cleared">The logs have been cleared</string> <string name="cleared">The logs have been cleared</string>
<!-- Settings --> <!-- Settings -->
<string name="emulator">Emulator</string> <string name="emulator">Emulator</string>