package org.pixeldroid.app.settings import android.annotation.SuppressLint import androidx.appcompat.app.AlertDialog import android.app.Dialog import android.content.Intent import android.content.SharedPreferences import android.content.res.XmlResourceParser 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 import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat import androidx.fragment.app.DialogFragment import androidx.preference.ListPreference 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 import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.json.JSONArray import org.json.JSONObject import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.databinding.SettingsBinding import org.pixeldroid.app.utils.loadDefaultMenuTabs import org.pixeldroid.app.utils.loadJsonMenuTabs import org.pixeldroid.app.utils.setThemeFromPreferences import org.pixeldroid.common.ThemedActivity class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener { private var restartMainOnExit = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = SettingsBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.topBar) supportFragmentManager .beginTransaction() .replace(R.id.settings, SettingsFragment()) .commit() supportActionBar?.setDisplayHomeAsUpEnabled(true) 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 super@SettingsActivity.startActivity(intent) } else { finish() } } 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 ) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { sharedPreferences?.let { when (key) { "theme" -> { setThemeFromPreferences(it, resources) recreateWithRestartStatus() } "themeColor" -> { recreateWithRestartStatus() } } } } /** * 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() super.startActivity(intent) } class SettingsFragment : PreferenceFragmentCompat() { override fun onDisplayPreferenceDialog(preference: Preference) { var dialogFragment: DialogFragment? = null if (preference is ColorPreference) { dialogFragment = ColorPreferenceDialog((preference as ColorPreference?)!!) } else if(preference.key == "language"){ dialogFragment = LanguageSettingFragment() } else if (preference.key == "arrange_tabs") { dialogFragment = ArrangeTabsFragment() } 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) findPreference("language")?.let { it.setSummaryProvider { val locale = AppCompatDelegate.getApplicationLocales().get(0) locale?.getDisplayName(locale) ?: getString(R.string.default_system) } } //Hide Notification setting for Android versions where it doesn't work if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { findPreference("notification") ?.let { preferenceScreen.removePreference(it) } } } } } class LanguageSettingFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val list: MutableList = mutableListOf() // IDE doesn't find it, but compiling works apparently? resources.getXml(R.xml._generated_res_locale_config).use { 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 { // 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('_', '-')) // If found, we want to compensate for the first in the list being the default if(index == -1) -1 else index + 1 } return MaterialAlertDialogBuilder(requireContext()).apply { 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() AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag)) } 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>): RecyclerView.Adapter() { 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 = getString(tabsChecked[position].first.toInt()) 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 } } }