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

294 lines
12 KiB
Kotlin
Raw Normal View History

/*
* 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
2020-10-03 11:58:34 +02:00
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
2020-10-03 11:58:34 +02:00
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
2020-10-03 11:58:34 +02:00
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
2020-10-05 12:04:57 +02:00
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) }
2020-10-03 11:58:34 +02:00
private val adapter = GenericAdapter()
private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
2020-10-03 11:58:34 +02:00
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
}
}
2020-10-03 11:58:34 +02:00
private fun setAppListDecoration() {
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
2020-10-03 11:58:34 +02:00
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
2020-10-03 11:58:34 +02:00
}
}
private fun setupAppList() {
2020-10-03 11:58:34 +02:00
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)
2020-10-03 11:58:34 +02:00
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()
2020-10-03 11:58:34 +02:00
var layoutTypeChanged = false
for (appViewItem in adapter.currentItems.filterIsInstance(AppViewItem::class.java)) {
2020-10-03 11:58:34 +02:00
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()
}
}
}
}