PixelDroid-App-Android/app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt

325 lines
13 KiB
Kotlin
Raw Normal View History

2021-04-22 11:47:18 +02:00
package org.pixeldroid.app.settings
import android.annotation.SuppressLint
import androidx.appcompat.app.AlertDialog
2022-11-25 16:50:46 +01:00
import android.app.Dialog
import android.content.Intent
import android.content.SharedPreferences
2022-11-25 16:50:46 +01:00
import android.content.res.XmlResourceParser
2021-09-25 13:52:18 +02:00
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.addCallback
2022-11-25 16:50:46 +01:00
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
2022-07-08 23:20:44 +02:00
import androidx.fragment.app.DialogFragment
2022-11-25 16:50:46 +01:00
import androidx.preference.ListPreference
2022-03-12 22:07:24 +01:00
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton
import com.google.android.material.checkbox.MaterialCheckBox
2022-11-28 12:30:30 +01:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.json.JSONArray
import org.json.JSONObject
2021-04-22 11:47:18 +02:00
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
2023-10-29 17:48:27 +01:00
import org.pixeldroid.app.databinding.SettingsBinding
import org.pixeldroid.app.utils.loadDefaultMenuTabs
import org.pixeldroid.app.utils.loadJsonMenuTabs
2021-04-22 11:47:18 +02:00
import org.pixeldroid.app.utils.setThemeFromPreferences
import org.pixeldroid.common.ThemedActivity
2022-11-25 16:50:46 +01:00
2023-10-29 17:48:27 +01:00
class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
2020-12-11 16:53:12 +01:00
private var restartMainOnExit = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2023-10-29 17:48:27 +01:00
val binding = SettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
2023-12-13 11:18:05 +01:00
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Handle the back button event
// If a setting (for example language or theme) was changed, the main activity should be
// started without history so that the change is applied to the whole back stack
if (restartMainOnExit) {
val intent = Intent(this@SettingsActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
2023-12-13 11:18:05 +01:00
super@SettingsActivity.startActivity(intent)
} else {
2023-12-13 11:18:05 +01:00
finish()
}
}
2020-12-11 16:53:12 +01:00
restartMainOnExit = intent.getBooleanExtra("restartMain", false)
}
override fun onResume() {
super.onResume()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(
this
)
}
override fun onPause() {
super.onPause()
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(
this
)
}
2023-11-03 17:49:08 +01:00
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences?.let {
when (key) {
"theme" -> {
setThemeFromPreferences(it, resources)
recreateWithRestartStatus()
}
"themeColor" -> {
recreateWithRestartStatus()
}
2022-07-10 13:42:19 +02:00
}
}
2020-12-11 16:53:12 +01:00
}
2020-12-11 16:53:12 +01:00
/**
* Mark main activity to be changed and recreate the current one
*/
private fun recreateWithRestartStatus() {
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val savedInstanceState = Bundle().apply {
putBoolean("restartMain", true)
}
intent.putExtras(savedInstanceState)
finish()
2021-09-25 13:52:18 +02:00
super.startActivity(intent)
}
2020-12-11 16:53:12 +01:00
class SettingsFragment : PreferenceFragmentCompat() {
2022-07-08 23:20:44 +02:00
override fun onDisplayPreferenceDialog(preference: Preference) {
var dialogFragment: DialogFragment? = null
if (preference is ColorPreference) {
dialogFragment = ColorPreferenceDialog((preference as ColorPreference?)!!)
2022-11-25 16:50:46 +01:00
} else if(preference.key == "language"){
dialogFragment = LanguageSettingFragment()
} else if (preference.key == "arrange_tabs") {
dialogFragment = ArrangeTabsFragment()
2022-07-08 23:20:44 +02:00
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, "settings_fragment")
} else {
super.onDisplayPreferenceDialog(preference)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
2021-09-25 13:52:18 +02:00
2022-11-25 16:50:46 +01:00
findPreference<ListPreference>("language")?.let {
it.setSummaryProvider {
val locale = AppCompatDelegate.getApplicationLocales().get(0)
locale?.getDisplayName(locale) ?: getString(R.string.default_system)
}
}
2021-09-25 13:52:18 +02:00
//Hide Notification setting for Android versions where it doesn't work
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
2022-11-25 16:50:46 +01:00
findPreference<Preference>("notification")
2022-03-12 22:07:24 +01:00
?.let { preferenceScreen.removePreference(it) }
2021-09-25 13:52:18 +02:00
}
}
}
2022-11-25 16:50:46 +01:00
}
class LanguageSettingFragment : DialogFragment() {
2022-11-25 16:50:46 +01:00
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val list: MutableList<String> = mutableListOf()
// IDE doesn't find it, but compiling works apparently?
resources.getXml(R.xml._generated_res_locale_config).use {
2022-11-25 16:50:46 +01:00
var eventType = it.eventType
while (eventType != XmlResourceParser.END_DOCUMENT) {
when (eventType) {
XmlResourceParser.START_TAG -> {
if (it.name == "locale") {
list.add(it.getAttributeValue(0))
}
}
}
eventType = it.next()
}
}
val locales = AppCompatDelegate.getApplicationLocales()
val checkedItem: Int =
if(locales.isEmpty) 0
else {
2022-11-25 22:01:36 +01:00
// For some reason we get a bit inconsistent language tags. This normalises it for
// the currently used languages, but it might break in the future if we add some
val index = list.indexOf(locales.get(0)?.toLanguageTag()?.lowercase()?.replace('_', '-'))
2022-11-25 16:50:46 +01:00
// If found, we want to compensate for the first in the list being the default
if(index == -1) -1
else index + 1
}
2022-11-28 12:30:30 +01:00
return MaterialAlertDialogBuilder(requireContext()).apply {
2022-11-25 16:50:46 +01:00
setIcon(R.drawable.translate_black_24dp)
setTitle(R.string.language)
setSingleChoiceItems((mutableListOf(getString(R.string.default_system)) + list.map {
val appLocale = LocaleListCompat.forLanguageTags(it)
appLocale.get(0)!!.getDisplayName(appLocale.get(0)!!)
}).toTypedArray(), checkedItem) { dialog, which ->
val languageTag = if(which in 1..list.size) list[which - 1] else null
dialog.dismiss()
2022-11-25 22:01:36 +01:00
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag))
2022-11-25 16:50:46 +01:00
}
setNegativeButton(android.R.string.ok) { _, _ -> }
}.create()
}
}
class ArrangeTabsFragment: DialogFragment() {
@SuppressLint("ClickableViewAccessibility")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater: LayoutInflater = requireActivity().layoutInflater
val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val tabsCheckedString = sharedPreferences.getString("tabsChecked", null)
val map = if (tabsCheckedString == null) {
// Load default menu
val list = loadDefaultMenuTabs(requireContext(), dialogView)
list.zip(List(list.size){true}.toTypedArray()).toMutableList()
} else {
// Get current menu visibility and order from settings
loadJsonMenuTabs(tabsCheckedString).toMutableList()
}
val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs)
val listAdapter = ListViewAdapter(map)
listFeed.adapter = listAdapter
listFeed.layoutManager = LinearLayoutManager(requireActivity())
val callback = object: ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
listAdapter.onItemMove(source.bindingAdapterPosition, target.bindingAdapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Do nothing, all items should remain in the list
}
}
val itemTouchHelper = ItemTouchHelper(callback)
itemTouchHelper.attachToRecyclerView(listFeed)
val dialog = MaterialAlertDialogBuilder(requireContext()).apply {
setIcon(R.drawable.outline_bottom_navigation)
setTitle(R.string.arrange_tabs_summary)
setView(dialogView)
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ ->
// Save values into preferences
val tabsChecked = listAdapter.tabsChecked.toList()
val tabsJson = JSONArray()
val checkedJson = JSONArray()
tabsChecked.forEach { (k, v) ->
tabsJson.put(k)
checkedJson.put(v.toString())
}
val tabsCheckedJson = JSONObject().apply {
put("tabs", tabsJson)
put("checked", checkedJson)
}.toString()
sharedPreferences.edit().putString("tabsChecked", tabsCheckedJson).apply()
}
}.create()
return dialog
}
inner class ListViewAdapter(val tabsChecked: MutableList<Pair<String, Boolean>>):
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = FrameLayout.inflate(context, R.layout.layout_tab, null)
// Make sure the layout occupies full width
view.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
return object: RecyclerView.ViewHolder(view) {}
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView: MaterialButton = holder.itemView.findViewById(R.id.textView)
val checkBox: MaterialCheckBox = holder.itemView.findViewById(R.id.checkBox)
val dragHandle: ImageView = holder.itemView.findViewById(R.id.dragHandle)
// Set content of each entry
textView.text = tabsChecked[position].first
checkBox.isChecked = tabsChecked[position].second
// Also interact with checkbox when button is clicked
textView.setOnClickListener {
val isCheckedNew = !tabsChecked[position].second
tabsChecked[position] = Pair(tabsChecked[position].first, isCheckedNew)
checkBox.isChecked = isCheckedNew
// Disable OK button when no tab is selected or when strictly more than 5 tabs are selected
val maxItemCount = BottomNavigationView(requireContext()).maxItemCount // = 5
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
with (tabsChecked.count { (_, v) -> v }) { this in 1..maxItemCount}
}
// Also highlight button when checkbox is clicked
checkBox.setOnTouchListener { _, motionEvent ->
textView.dispatchTouchEvent(motionEvent)
}
// Do not highlight the button when the drag handle is touched
dragHandle.setOnTouchListener { _, _ -> true }
}
override fun getItemCount(): Int {
return tabsChecked.size
}
fun onItemMove(from: Int, to: Int) {
val previous = tabsChecked.removeAt(from)
tabsChecked.add(to, previous)
notifyItemMoved(from, to)
notifyItemChanged(to) // necessary to avoid checkBox issues
}
}
}