Added navigation delegation to bypass default one

By making a delegate like such we protect ourselves against intrusions in views' logic
This commit is contained in:
Viktor De Pasquale 2019-10-16 17:27:11 +02:00
parent 2daa131fb2
commit 0eb28c3265
8 changed files with 195 additions and 8 deletions

View File

@ -1,8 +1,15 @@
package com.topjohnwu.magisk.model.events
import android.app.Activity
import androidx.appcompat.app.AppCompatActivity
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import io.reactivex.subjects.PublishSubject
@ -19,7 +26,9 @@ class EnvFixEvent : ViewEvent()
class UpdateSafetyNetEvent : ViewEvent()
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent()
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: AppCompatActivity) = activity.run(action)
}
class OpenFilePickerEvent : ViewEvent()
@ -31,8 +40,40 @@ class PageChangedEvent : ViewEvent()
class PermissionEvent(
val permissions: List<String>,
val callback: PublishSubject<Boolean>
) : ViewEvent()
) : ViewEvent(), ActivityExecutor {
class BackPressEvent : ViewEvent()
private val permissionRequest = PermissionRequestBuilder().apply {
onSuccess {
callback.onNext(true)
}
onFailure {
callback.onNext(false)
callback.onError(SecurityException("User refused permissions"))
}
}.build()
override fun invoke(activity: AppCompatActivity) = Dexter.withActivity(activity)
.withPermissions(permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>,
token: PermissionToken
) = token.continuePermissionRequest()
override fun onPermissionsChecked(
report: MultiplePermissionsReport
) = if (report.areAllPermissionsGranted()) {
permissionRequest.onSuccess()
} else {
permissionRequest.onFailure()
}
}).check()
}
class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: AppCompatActivity) {
activity.onBackPressed()
}
}
class DieEvent : ViewEvent()

View File

@ -3,21 +3,29 @@ package com.topjohnwu.magisk.model.navigation
import android.os.Bundle
import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import com.skoumal.teanity.viewevents.NavigationDslMarker
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.ActivityExecutor
import com.topjohnwu.magisk.redesign.compat.CompatActivity
import kotlin.reflect.KClass
class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions,
val animOptions: MagiskAnimBuilder
) : ViewEvent() {
) : ViewEvent(), ActivityExecutor {
companion object {
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build()
}
override fun invoke(activity: AppCompatActivity) {
if (activity !is CompatActivity<*, *>) return
activity.navigation.navigateTo(this)
}
@NavigationDslMarker
class Builder {

View File

@ -29,6 +29,7 @@ open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(
override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host
override val navHost: Int = R.id.main_nav_host
override val defaultPosition: Int = 0
override val baseFragments: List<KClass<out Fragment>> = listOf(
@ -38,7 +39,6 @@ open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(
LogFragment::class,
SettingsFragment::class
)
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
//implementation doesn't favor us against the design library, so on re-create it's often given

View File

@ -10,13 +10,19 @@ abstract class CompatActivity<ViewModel : CompatViewModel, Binding : ViewDataBin
MagiskActivity<ViewModel, Binding>(), CompatView<ViewModel> {
override val viewRoot: View get() = binding.root
override val navigation: CompatNavigationDelegate<CompatActivity<ViewModel, Binding>>? by lazy {
CompatNavigationDelegate(this)
}
private val delegate by lazy { CompatDelegate(this) }
internal abstract val navHost: Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
delegate.ensureInsets()
delegate.onCreate()
navigation?.onCreate(savedInstanceState)
}
override fun onResume() {
@ -25,12 +31,23 @@ abstract class CompatActivity<ViewModel : CompatViewModel, Binding : ViewDataBin
delegate.onResume()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigation?.onSaveInstanceState(outState)
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
delegate.onEventExecute(event, this)
}
override fun onBackPressed() {
if (navigation?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this)
}

View File

@ -15,6 +15,11 @@ class CompatDelegate internal constructor(
private val view: CompatView<*>
) {
fun onCreate() {
ensureInsets()
}
fun onResume() {
view.viewModel.requestRefresh()
}
@ -33,7 +38,7 @@ class CompatDelegate internal constructor(
(event as? ActivityExecutor)?.invoke(fragment.requireActivity() as AppCompatActivity)
}
fun ensureInsets() {
private fun ensureInsets() {
ViewCompat.setOnApplyWindowInsetsListener(view.viewRoot) { _, insets ->
insets.asInsets()
.also { view.peekSystemWindowInsets(it) }

View File

@ -10,13 +10,16 @@ abstract class CompatFragment<ViewModel : CompatViewModel, Binding : ViewDataBin
: MagiskFragment<ViewModel, Binding>(), CompatView<ViewModel> {
override val viewRoot: View get() = binding.root
override val navigation by lazy { compatActivity.navigation }
private val delegate by lazy { CompatDelegate(this) }
private val compatActivity get() = requireActivity() as CompatActivity<*, *>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
delegate.ensureInsets()
delegate.onCreate()
}
override fun onResume() {

View File

@ -0,0 +1,112 @@
package com.topjohnwu.magisk.redesign.compat
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.FragmentTransaction
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import timber.log.Timber
class CompatNavigationDelegate<Source>(
private val source: Source,
private val listener: FragNavController.TransactionListener? = null
) : FragNavController.RootFragmentListener where Source : CompatActivity<*, *>, Source : Navigator {
private val controller by lazy {
check(source.navHost != 0) { "Did you forget to override \"navHostId\"?" }
FragNavController(source.supportFragmentManager, source.navHost)
}
val isRoot get() = controller.isRootFragment
//region Listener
override val numberOfRootFragments: Int
get() = source.baseFragments.size
override fun getRootFragment(index: Int) =
source.baseFragments[index].java.newInstance()
//endregion
fun onCreate(savedInstanceState: Bundle?) = controller.run {
rootFragmentListener = source
transactionListener = listener
initialize(0, savedInstanceState)
}
fun onSaveInstanceState(outState: Bundle) =
controller.onSaveInstanceState(outState)
fun onBackPressed(): Boolean {
val fragment = controller.currentFrag as? CompatFragment<*, *>
if (fragment?.onBackPressed() == true) {
return true
}
return runCatching { controller.popFragment() }.fold({ true }, { false })
}
// ---
fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
controller.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
controller.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.takeIf { it != -1 } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { controller.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(source, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { source.startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
source.baseFragments
.indexOfFirst { it.java.name == destination.name }
.takeIf { it > 0 }
?.let { controller.switchTab(it) } ?: destination.newInstance()
.also { it.arguments = event.navDirections.args }
.let { controller.pushFragment(it) }
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
}

View File

@ -7,6 +7,7 @@ internal interface CompatView<ViewModel : CompatViewModel> {
val viewRoot: View
val viewModel: ViewModel
val navigation: CompatNavigationDelegate<*>?
fun peekSystemWindowInsets(insets: Insets) = Unit
fun consumeSystemWindowInsets(insets: Insets) = Insets.NONE