Reorganize app source code

This commit is contained in:
topjohnwu
2020-08-18 06:31:15 -07:00
parent d7a26dbf27
commit 846bbb4da1
77 changed files with 158 additions and 191 deletions
@@ -0,0 +1,119 @@
package com.topjohnwu.magisk.arch
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavDirections
import androidx.navigation.findNavController
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ui.theme.Theme
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
BaseActivity(), BaseUIComponent<VM> {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
protected open val themeRes: Int = Theme.selected.themeRes
private val navHostFragment get() = supportFragmentManager.findFragmentById(navHost)
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
override val viewRoot: View get() = binding.root
open val navigation by lazy {
runCatching { findNavController(navHost) }.getOrNull()
}
open val navHost: Int = 0
open val snackbarView get() = binding.root
init {
val theme = Config.darkTheme
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
currentFragment?.onActivityResult(requestCode, resultCode, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(themeRes)
// We need to set the window background explicitly since for whatever reason it's not
// propagated upstream
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
.use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) }
super.onCreate(savedInstanceState)
startObserveEvents()
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
it.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
(binding.root as? ViewGroup)?.startAnimations()
return super.onPreBind(binding)
}
})
}
ensureInsets()
directionsDispatcher.observe(this) {
it?.navigate()
// we don't want the directions to be re-dispatched, so we preemptively set them to null
if (it != null) {
directionsDispatcher.value = null
}
}
}
override fun onResume() {
super.onResume()
viewModel.requestRefresh()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(this)
is ActivityExecutor -> event(this)
else -> Unit
}
override fun onBackPressed() {
if (navigation == null || currentFragment?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
fun NavDirections.navigate() {
navigation?.navigate(this)
}
companion object {
private val directionsDispatcher = MutableLiveData<NavDirections?>()
fun postDirections(navDirections: NavDirections) =
directionsDispatcher.postValue(navDirections)
}
}
@@ -0,0 +1,63 @@
package com.topjohnwu.magisk.arch
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
val viewRoot: View
val viewModel: VM
fun startObserveEvents() {
viewModel.viewEvents.observe(this) {
onEventDispatched(it)
}
}
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
/**
* Called for all [ViewEvent]s published by associated viewModel.
*/
fun onEventDispatched(event: ViewEvent) {}
fun ensureInsets() {
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
insets.asInsets()
.also { viewModel.insets = it }
.let { consumeSystemWindowInsets(it) }
?.subtractBy(insets) ?: insets
}
if (ViewCompat.isAttachedToWindow(viewRoot)) {
ViewCompat.requestApplyInsets(viewRoot)
} else {
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) = Unit
override fun onViewAttachedToWindow(v: View) {
ViewCompat.requestApplyInsets(v)
}
})
}
}
private fun WindowInsetsCompat.asInsets() = Insets.of(
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom
)
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
Insets.of(
insets.systemWindowInsetLeft - left,
insets.systemWindowInsetTop - top,
insets.systemWindowInsetRight - right,
insets.systemWindowInsetBottom - bottom
)
).build()
}
@@ -0,0 +1,90 @@
package com.topjohnwu.magisk.arch
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.ktx.startAnimations
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
Fragment(), BaseUIComponent<VM> {
protected val activity get() = requireActivity() as BaseUIActivity<*, *>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
override val viewRoot: View get() = binding.root
private val navigation get() = activity.navigation
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveEvents()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
return binding.root
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> event(activity)
is FragmentExecutor -> event(this)
else -> Unit
}
open fun onKeyEvent(event: KeyEvent): Boolean {
return false
}
open fun onBackPressed(): Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
this@BaseUIFragment.onPreBind(binding)
return true
}
})
ensureInsets()
}
override fun onResume() {
super.onResume()
viewModel.requestRefresh()
}
protected open fun onPreBind(binding: Binding) {
(binding.root as? ViewGroup)?.startAnimations()
}
fun NavDirections.navigate() {
navigation?.navigate(this)
}
}
interface ReselectionTarget {
fun onReselected()
}
@@ -0,0 +1,117 @@
package com.topjohnwu.magisk.arch
import android.Manifest
import androidx.annotation.CallSuper
import androidx.core.graphics.Insets
import androidx.databinding.Bindable
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.NavigationWrapper
import com.topjohnwu.magisk.utils.ObservableHost
import com.topjohnwu.magisk.utils.set
import kotlinx.coroutines.Job
import org.koin.core.KoinComponent
abstract class BaseViewModel(
initialState: State = State.LOADING
) : ViewModel(), ObservableHost, KoinComponent {
override var callbacks: PropertyChangeRegistry? = null
enum class State {
LOADED, LOADING, LOADING_FAILED
}
@get:Bindable
val loading get() = state == State.LOADING
@get:Bindable
val loaded get() = state == State.LOADED
@get:Bindable
val loadFailed get() = state == State.LOADING_FAILED
val isConnected get() = Info.isConnected
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
@get:Bindable
var insets = Insets.NONE
set(value) = set(value, field, { field = it }, BR.insets)
var state= initialState
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
private val _viewEvents = MutableLiveData<ViewEvent>()
private var runningJob: Job? = null
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
requestRefresh()
}
}
init {
isConnected.addOnPropertyChangedCallback(refreshCallback)
}
/** This should probably never be called manually, it's called manually via delegate. */
@Synchronized
fun requestRefresh() {
if (runningJob?.isActive == true) {
return
}
runningJob = refresh()
}
protected open fun refresh(): Job? = null
@CallSuper
override fun onCleared() {
isConnected.removeOnPropertyChangedCallback(refreshCallback)
super.onCleared()
}
fun withView(action: BaseActivity.() -> Unit) {
ViewActionEvent(action).publish()
}
fun withPermissions(vararg permissions: String, callback: (Boolean) -> Unit) {
PermissionEvent(permissions.toList(), callback).publish()
}
fun withExternalRW(callback: () -> Unit) {
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish()
} else {
callback()
}
}
}
fun back() = BackPressEvent().publish()
fun <Event : ViewEvent> Event.publish() {
_viewEvents.postValue(this)
}
fun <Event : ViewEventWithScope> Event.publish() {
scope = viewModelScope
_viewEvents.postValue(this)
}
fun NavDirections.publish() {
_viewEvents.postValue(NavigationWrapper(this))
}
}
@@ -0,0 +1,48 @@
package com.topjohnwu.magisk.arch
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind
inline fun <T : ComparableRvItem<*>> diffListOf(
vararg newItems: T
) = diffListOf(newItems.toList())
inline fun <T : ComparableRvItem<*>> diffListOf(
newItems: List<T>
) = DiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems) }
inline fun <T : ComparableRvItem<*>> filterableListOf(
vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems.toList()) }
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: T
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
item.onBindingBound(binding)
}
}
inline fun <T : RvItem> itemBindingOf(
crossinline body: (ItemBinding<*>) -> Unit = {}
) = OnItemBind<T> { itemBinding, _, item ->
item.bind(itemBinding)
body(itemBinding)
}
@@ -0,0 +1,17 @@
package com.topjohnwu.magisk.arch
import android.os.Handler
import androidx.core.os.postDelayed
import com.topjohnwu.superuser.internal.UiThreadHandler
interface Queryable {
val queryDelay: Long
val queryHandler: Handler get() = UiThreadHandler.handler
fun submitQuery() {
queryHandler.postDelayed(queryDelay) { query() }
}
fun query()
}
@@ -0,0 +1,28 @@
package com.topjohnwu.magisk.arch
import android.content.Context
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.core.base.BaseActivity
import kotlinx.coroutines.CoroutineScope
/**
* Class for passing events from ViewModels to Activities/Fragments
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
*/
abstract class ViewEvent
abstract class ViewEventWithScope: ViewEvent() {
lateinit var scope: CoroutineScope
}
interface ContextExecutor {
operator fun invoke(context: Context)
}
interface ActivityExecutor {
operator fun invoke(activity: BaseActivity)
}
interface FragmentExecutor {
operator fun invoke(fragment: Fragment)
}