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 package com.topjohnwu.magisk.model.events
import android.app.Activity 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.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.entity.module.Repo import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
@ -19,7 +26,9 @@ class EnvFixEvent : ViewEvent()
class UpdateSafetyNetEvent : 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() class OpenFilePickerEvent : ViewEvent()
@ -31,8 +40,40 @@ class PageChangedEvent : ViewEvent()
class PermissionEvent( class PermissionEvent(
val permissions: List<String>, val permissions: List<String>,
val callback: PublishSubject<Boolean> 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() class DieEvent : ViewEvent()

View File

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

View File

@ -29,6 +29,7 @@ open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(
override val layoutRes = R.layout.activity_main_md2 override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel<MainViewModel>() override val viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host 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 defaultPosition: Int = 0
override val baseFragments: List<KClass<out Fragment>> = listOf( override val baseFragments: List<KClass<out Fragment>> = listOf(
@ -38,7 +39,6 @@ open class MainActivity : CompatActivity<MainViewModel, ActivityMainMd2Binding>(
LogFragment::class, LogFragment::class,
SettingsFragment::class SettingsFragment::class
) )
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies //This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener //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 //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> { MagiskActivity<ViewModel, Binding>(), CompatView<ViewModel> {
override val viewRoot: View get() = binding.root 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) } private val delegate by lazy { CompatDelegate(this) }
internal abstract val navHost: Int
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
delegate.ensureInsets() delegate.onCreate()
navigation?.onCreate(savedInstanceState)
} }
override fun onResume() { override fun onResume() {
@ -25,12 +31,23 @@ abstract class CompatActivity<ViewModel : CompatViewModel, Binding : ViewDataBin
delegate.onResume() delegate.onResume()
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigation?.onSaveInstanceState(outState)
}
override fun onEventDispatched(event: ViewEvent) { override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event) super.onEventDispatched(event)
delegate.onEventExecute(event, this) delegate.onEventExecute(event, this)
} }
override fun onBackPressed() {
if (navigation?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this) protected fun ViewEvent.dispatchOnSelf() = onEventDispatched(this)
} }

View File

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

View File

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