skyline/app/src/main/java/emu/skyline/MainActivity.kt

294 lines
12 KiB
Kotlin

/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline
import android.content.Intent
import android.graphics.Color
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.size
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.AppViewItem
import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.LayoutType
import emu.skyline.data.AppItem
import emu.skyline.data.DataItem
import emu.skyline.data.HeaderItem
import emu.skyline.loader.LoaderResult
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.main_activity.*
import kotlin.math.ceil
class MainActivity : AppCompatActivity() {
private val settings by lazy { Settings(this) }
private val adapter = GenericAdapter()
private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
private val viewModel by viewModels<MainViewModel>()
private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog)
override fun onCreate(savedInstanceState : Bundle?) {
AppCompatDelegate.setDefaultNightMode(
when ((settings.appTheme.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
}
)
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
setupAppList()
swipe_refresh_layout.apply {
setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) })
setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) })
post { setDistanceToTriggerSync(swipe_refresh_layout.height / 3) }
setOnRefreshListener { loadRoms(false) }
}
viewModel.state.observe(owner = this, onChanged = ::handleState)
loadRoms(!settings.refreshRequired)
search_bar.apply {
setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
setRefreshIconListener { loadRoms(false) }
addTextChangedListener(afterTextChanged = { editable ->
editable?.let { text -> adapter.filter.filter(text.toString()) }
})
if (!viewModel.searchBarAnimated) {
viewModel.searchBarAnimated = true
post { startTitleAnimation() }
}
}
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
search_bar.refreshIconVisible = !isInTouchMode
}
}
private inner class GridSpacingItemDecoration : RecyclerView.ItemDecoration() {
private val padding = resources.getDimensionPixelSize(R.dimen.grid_padding)
override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val gridLayoutManager = parent.layoutManager as GridLayoutManager
val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams
when (layoutParams.spanIndex) {
0 -> outRect.left = padding
gridLayoutManager.spanCount - 1 -> outRect.right = padding
else -> {
outRect.left = padding / 2
outRect.right = padding / 2
}
}
if (layoutParams.spanSize == gridLayoutManager.spanCount) outRect.right = padding
}
}
private fun setAppListDecoration() {
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
when (layoutType) {
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
LayoutType.Grid, LayoutType.GridCompact -> app_list.addItemDecoration(GridSpacingItemDecoration())
}
}
/**
* This layout manager handles situations where [onFocusSearchFailed] gets called, when possible we always want to focus on the item with the same span index
*/
private inner class CustomLayoutManager(gridSpan : Int) : GridLayoutManager(this, gridSpan) {
init {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
}
}
override fun onFocusSearchFailed(focused : View, focusDirection : Int, recycler : RecyclerView.Recycler, state : RecyclerView.State) : View? {
val nextFocus = super.onFocusSearchFailed(focused, focusDirection, recycler, state)
when (focusDirection) {
View.FOCUS_DOWN -> {
findContainingItemView(focused)?.let { focusedChild ->
val current = app_list.indexOfChild(focusedChild)
val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex
for (i in current + 1 until app_list.size) {
val candidate = getChildAt(i)!!
// Return candidate when span index matches
if (currentSpanIndex == (candidate.layoutParams as LayoutParams).spanIndex) return candidate
}
if (nextFocus == null) {
app_bar_layout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible
app_list.smoothScrollToPosition(adapter.itemCount)
}
}
}
View.FOCUS_UP -> {
if (nextFocus?.isFocusable != true) {
search_bar.requestFocus()
app_bar_layout.setExpanded(true)
return null
}
}
}
return nextFocus
}
}
private fun setupAppList() {
app_list.adapter = adapter
val itemWidth = 225
val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
app_list.layoutManager = CustomLayoutManager(gridSpan)
setAppListDecoration()
if (settings.searchLocation.isEmpty()) {
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
startActivityForResult(intent, 1)
}
}
private fun handleState(state : MainState) = when (state) {
MainState.Loading -> {
search_bar.animateRefreshIcon()
swipe_refresh_layout.isRefreshing = true
}
is MainState.Loaded -> {
swipe_refresh_layout.isRefreshing = false
populateAdapter(state.items)
}
is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
}
private fun selectStartGame(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
if (settings.selectAction)
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
else if (appItem.loaderResult == LoaderResult.Success)
startActivity(Intent(this, EmulationActivity::class.java).apply { data = appItem.uri })
}
private fun selectShowGameDialog(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
}
private fun loadRoms(loadFromFile : Boolean) {
viewModel.loadRoms(this, loadFromFile, Uri.parse(settings.searchLocation))
settings.refreshRequired = false
}
private fun populateAdapter(items : List<DataItem>) {
adapter.setItems(items.map {
when (it) {
is HeaderItem -> HeaderViewItem(it.title)
is AppItem -> it.toViewItem()
}
})
if (items.isEmpty()) adapter.setItems(listOf(HeaderViewItem(getString(R.string.no_rom))))
}
/**
* 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?) {
super.onActivityResult(requestCode, resultCode, intent)
if (resultCode == RESULT_OK) {
when (requestCode) {
1 -> {
val uri = intent!!.data!!
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
settings.searchLocation = uri.toString()
loadRoms(!settings.refreshRequired)
}
2 -> {
try {
val intentGame = Intent(this, EmulationActivity::class.java)
intentGame.data = intent!!.data!!
if (resultCode != 0)
startActivityForResult(intentGame, resultCode)
else
startActivity(intentGame)
} catch (e : Exception) {
Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_SHORT).show()
}
}
3 -> if (settings.refreshRequired) loadRoms(false)
}
}
}
override fun onResume() {
super.onResume()
var layoutTypeChanged = false
for (appViewItem in adapter.currentItems.filterIsInstance(AppViewItem::class.java)) {
if (layoutType != appViewItem.layoutType) {
appViewItem.layoutType = layoutType
layoutTypeChanged = true
} else {
break
}
}
if (layoutTypeChanged) {
setAppListDecoration()
adapter.notifyItemRangeChanged(0, adapter.currentItems.size)
}
}
override fun onBackPressed() {
search_bar.apply {
if (hasFocus() && text.isNotEmpty()) {
text = ""
clearFocus()
} else {
super.onBackPressed()
}
}
}
}