Add: Prefs activity and base fragment

This commit is contained in:
machiav3lli 2022-01-14 01:38:29 +01:00
parent 51a84d342b
commit 08badc9327
2 changed files with 553 additions and 0 deletions

View File

@ -0,0 +1,197 @@
package com.looker.droidify.ui.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.*
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.appbar.MaterialToolbar
import com.looker.droidify.BuildConfig
import com.looker.droidify.ContextWrapperX
import com.looker.droidify.MainApplication
import com.looker.droidify.R
import com.looker.droidify.content.Preferences
import com.looker.droidify.database.CursorOwner
import com.looker.droidify.databinding.ActivityPrefsXBinding
import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService
import com.looker.droidify.ui.fragments.MainNavFragmentX
import com.looker.droidify.ui.fragments.Source
import com.looker.droidify.utility.extension.text.nullIfEmpty
import kotlinx.coroutines.launch
// TODO clean up the bloat
class PrefsActivityX : AppCompatActivity() {
companion object {
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
const val EXTRA_CACHE_FILE_NAME =
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
}
sealed class SpecialIntent {
object Updates : SpecialIntent()
class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent()
}
lateinit var binding: ActivityPrefsXBinding
lateinit var toolbar: MaterialToolbar
lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController
private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ ->
navController.currentDestination?.let {
val source = Source.values()[when (it.id) {
R.id.updateTab -> 1
R.id.otherTab -> 2
else -> 0 // R.id.userTab
}]
updateUpdateNotificationBlocker(source)
}
})
val db
get() = (application as MainApplication).db
lateinit var cursorOwner: CursorOwner
private set
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration))
super.onCreate(savedInstanceState)
binding = ActivityPrefsXBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
toolbar = binding.toolbar
if (savedInstanceState == null && (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
handleIntent(intent)
}
setContentView(binding.root)
if (savedInstanceState == null) {
cursorOwner = CursorOwner()
supportFragmentManager.beginTransaction()
.add(cursorOwner, CursorOwner::class.java.name)
.commit()
} else {
cursorOwner = supportFragmentManager
.findFragmentByTag(CursorOwner::class.java.name) as CursorOwner
}
setSupportActionBar(toolbar)
toolbar.isFocusableInTouchMode = true
binding.collapsingToolbar.title = getString(R.string.settings)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_content) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavigation.setupWithNavController(navController)
appBarConfiguration = AppBarConfiguration(
setOf(R.id.exploreTab, R.id.latestTab, R.id.installedTab)
)
setupActionBarWithNavController(navController, appBarConfiguration)
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
hideKeyboard()
}
}
override fun onStart() {
super.onStart()
syncConnection.bind(this)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration)
}
private fun hideKeyboard() {
(getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent(intent)
}
private val Intent.packageName: String?
get() {
val uri = data
return when {
uri?.scheme == "package" || uri?.scheme == "fdroid.app" -> {
uri.schemeSpecificPart?.nullIfEmpty()
}
uri?.scheme == "market" && uri.host == "details" -> {
uri.getQueryParameter("id")?.nullIfEmpty()
}
uri != null && uri.scheme in setOf("http", "https") -> {
val host = uri.host.orEmpty()
if (host == "f-droid.org" || host.endsWith(".f-droid.org")) {
uri.lastPathSegment?.nullIfEmpty()
} else {
null
}
}
else -> {
null
}
}
}
private fun handleSpecialIntent(specialIntent: SpecialIntent) {
when (specialIntent) {
is SpecialIntent.Updates -> navController.navigate(R.id.installedTab)
is SpecialIntent.Install -> {
val packageName = specialIntent.packageName
if (!packageName.isNullOrEmpty()) {
lifecycleScope.launch {
specialIntent.cacheFileName?.let {
AppInstaller.getInstance(this@PrefsActivityX)
?.defaultInstaller?.install(packageName, it)
}
}
}
Unit
}
}::class
}
private fun handleIntent(intent: Intent?) {
when (intent?.action) {
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
ACTION_INSTALL -> handleSpecialIntent(
SpecialIntent.Install(
intent.packageName,
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
)
)
}
}
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(ContextWrapperX.wrap(newBase))
}
private fun updateUpdateNotificationBlocker(activeSource: Source) {
val blockerFragment = if (activeSource == Source.UPDATES) {
supportFragmentManager.fragments.asSequence().mapNotNull { it as? MainNavFragmentX }
.find { it.source == activeSource }
} else {
null
}
syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment)
}
}

View File

@ -0,0 +1,356 @@
package com.looker.droidify.ui.fragments
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.InputFilter
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.net.toUri
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.google.android.material.circularreveal.CircularRevealFrameLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textview.MaterialTextView
import com.looker.droidify.R
import com.looker.droidify.content.Preferences
import com.looker.droidify.databinding.FragmentPrefsBinding
import com.looker.droidify.databinding.PreferenceItemBinding
import com.looker.droidify.utility.extension.resources.*
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
abstract class PrefsNavFragmentX : Fragment() {
private lateinit var binding: FragmentPrefsBinding
private var preferenceBinding: PreferenceItemBinding? = null
private val preferences = mutableMapOf<Preferences.Key<*>, Preference<*>>()
override fun onResume() {
super.onResume()
preferences.forEach { (_, preference) -> preference.update() }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
super.onCreate(savedInstanceState)
binding = FragmentPrefsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
preferenceBinding = PreferenceItemBinding.inflate(layoutInflater)
val content = binding.fragmentContent
val scroll = NestedScrollView(content.context)
scroll.id = R.id.preferences_list
scroll.isFillViewport = true
content.addView(
scroll,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val scrollLayout = CircularRevealFrameLayout(content.context)
scroll.addView(
scrollLayout,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setupPrefs(scrollLayout)
lifecycleScope.launch {
Preferences.subject
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { updatePreference(it) }
}
updatePreference(null)
}
abstract fun setupPrefs(scrollLayout: CircularRevealFrameLayout)
private fun openURI(url: Uri) {
startActivity(Intent(Intent.ACTION_VIEW, url))
}
override fun onDestroyView() {
super.onDestroyView()
preferences.clear()
preferenceBinding = null
}
private fun updatePreference(key: Preferences.Key<*>?) {
if (key != null) {
preferences[key]?.update()
}
if (key == null || key == Preferences.Key.ProxyType) {
val enabled = when (Preferences[Preferences.Key.ProxyType]) {
is Preferences.ProxyType.Direct -> false
is Preferences.ProxyType.Http, is Preferences.ProxyType.Socks -> true
}
preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled)
preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled)
}
if (key == Preferences.Key.RootPermission) {
preferences[Preferences.Key.RootPermission]?.setEnabled(
Shell.getCachedShell()?.isRoot
?: Shell.getShell().isRoot
)
}
if (key == Preferences.Key.Theme) {
requireActivity().recreate()
}
}
protected fun LinearLayoutCompat.addText(title: String, summary: String, url: String) {
val text = MaterialTextView(context)
val subText = MaterialTextView(context)
text.text = title
subText.text = summary
text.setTextSizeScaled(16)
subText.setTextSizeScaled(14)
resources.sizeScaled(16).let {
text.setPadding(it, it, 5, 5)
subText.setPadding(it, 5, 5, 25)
}
addView(
text,
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
)
addView(
subText,
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
)
text.setOnClickListener { openURI(url.toUri()) }
subText.setOnClickListener { openURI(url.toUri()) }
}
protected inline fun LinearLayoutCompat.addCategory(
title: String,
callback: LinearLayoutCompat.() -> Unit,
) {
val text = MaterialTextView(context)
text.typeface = TypefaceExtra.medium
text.setTextSizeScaled(14)
text.setTextColor(text.context.getColorFromAttr(R.attr.colorPrimary))
text.text = title
resources.sizeScaled(16).let { text.setPadding(it, it, it, 0) }
addView(
text,
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
)
callback()
}
protected fun <T> LinearLayoutCompat.addPreference(
key: Preferences.Key<T>, title: String,
summaryProvider: () -> String, dialogProvider: ((Context) -> AlertDialog)?,
): Preference<T> {
val preference =
Preference(key, this@PrefsNavFragmentX, this, title, summaryProvider, dialogProvider)
preferences[key] = preference
return preference
}
protected fun LinearLayoutCompat.addSwitch(
key: Preferences.Key<Boolean>,
title: String,
summary: String,
) {
val preference = addPreference(key, title, { summary }, null)
preference.check.visibility = View.VISIBLE
preference.view.setOnClickListener { Preferences[key] = !Preferences[key] }
preference.setCallback { preference.check.isChecked = Preferences[key] }
}
protected fun <T> LinearLayoutCompat.addEdit(
key: Preferences.Key<T>, title: String, valueToString: (T) -> String,
stringToValue: (String) -> T?, configureEdit: (TextInputEditText) -> Unit,
) {
addPreference(key, title, { valueToString(Preferences[key]) }) { it ->
val scroll = NestedScrollView(it)
scroll.resources.sizeScaled(20).let { scroll.setPadding(it, 0, it, 0) }
val edit = TextInputEditText(it)
configureEdit(edit)
edit.id = android.R.id.edit
edit.resources.sizeScaled(16)
.let { edit.setPadding(edit.paddingLeft, it, edit.paddingRight, it) }
edit.setText(valueToString(Preferences[key]))
edit.hint = edit.text.toString()
edit.text?.let { editable -> edit.setSelection(editable.length) }
edit.requestFocus()
scroll.addView(
edit,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
MaterialAlertDialogBuilder(it)
.setTitle(title)
.setView(scroll)
.setPositiveButton(R.string.ok) { _, _ ->
val value = stringToValue(edit.text.toString()) ?: key.default.value
post { Preferences[key] = value }
}
.setNegativeButton(R.string.cancel, null)
.create()
.apply {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
}
}
protected fun LinearLayoutCompat.addEditString(key: Preferences.Key<String>, title: String) {
addEdit(key, title, { it }, { it }, { })
}
protected fun LinearLayoutCompat.addEditInt(
key: Preferences.Key<Int>,
title: String,
range: IntRange?,
) {
addEdit(key, title, { it.toString() }, { it.toIntOrNull() }) {
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
if (range != null) it.filters =
arrayOf(InputFilter { source, start, end, dest, dstart, dend ->
val value = (dest.substring(0, dstart) + source.substring(start, end) +
dest.substring(dend, dest.length)).toIntOrNull()
if (value != null && value in range) null else ""
})
}
}
protected fun <T : Preferences.Enumeration<T>> LinearLayoutCompat.addEnumeration(
key: Preferences.Key<T>,
title: String,
valueToString: (T) -> String,
) {
addPreference(key, title, { valueToString(Preferences[key]) }) {
val values = key.default.value.values
MaterialAlertDialogBuilder(it)
.setTitle(title)
.setSingleChoiceItems(
values.map(valueToString).toTypedArray(),
values.indexOf(Preferences[key])
) { dialog, which ->
dialog.dismiss()
post { Preferences[key] = values[which] }
}
.setNegativeButton(R.string.cancel, null)
.create()
}
}
protected fun <T> LinearLayoutCompat.addList(
key: Preferences.Key<T>,
title: String,
values: List<T>,
valueToString: (T) -> String,
) {
addPreference(key, title, { valueToString(Preferences[key]) }) {
MaterialAlertDialogBuilder(it)
.setTitle(title)
.setSingleChoiceItems(
values.map(valueToString).toTypedArray(),
values.indexOf(Preferences[key])
) { dialog, which ->
dialog.dismiss()
post { Preferences[key] = values[which] }
}
.setNegativeButton(R.string.cancel, null)
.create()
}
}
protected class Preference<T>(
private val key: Preferences.Key<T>,
fragment: Fragment,
parent: ViewGroup,
titleText: String,
private val summaryProvider: () -> String,
private val dialogProvider: ((Context) -> AlertDialog)?,
) {
val view = parent.inflate(R.layout.preference_item)
val title = view.findViewById<MaterialTextView>(R.id.title)!!
val summary = view.findViewById<MaterialTextView>(R.id.summary)!!
val check = view.findViewById<SwitchMaterial>(R.id.check)!!
private var callback: (() -> Unit)? = null
init {
title.text = titleText
parent.addView(view)
if (dialogProvider != null) {
view.setOnClickListener {
PreferenceDialog(key.name)
.show(
fragment.childFragmentManager,
"${PreferenceDialog::class.java.name}.${key.name}"
)
}
}
update()
}
fun setCallback(callback: () -> Unit) {
this.callback = callback
update()
}
fun setEnabled(enabled: Boolean) {
view.isEnabled = enabled
title.isEnabled = enabled
summary.isEnabled = enabled
check.isEnabled = enabled
}
fun update() {
summary.text = summaryProvider()
summary.visibility = if (summary.text.isNotEmpty()) View.VISIBLE else View.GONE
callback?.invoke()
}
fun createDialog(context: Context): AlertDialog {
return dialogProvider!!(context)
}
}
class PreferenceDialog() : DialogFragment() {
companion object {
private const val EXTRA_KEY = "key"
}
constructor(key: String) : this() {
arguments = Bundle().apply {
putString(EXTRA_KEY, key)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val preferences = (parentFragment as PrefsNavFragmentX).preferences
val key = requireArguments().getString(EXTRA_KEY)!!
.let { name -> preferences.keys.find { it.name == name }!! }
val preference = preferences[key]!!
return preference.createDialog(requireContext())
}
}
}