From 0eb28c3265928f1930d3e67b941a6758b527297b Mon Sep 17 00:00:00 2001 From: Viktor De Pasquale Date: Wed, 16 Oct 2019 17:27:11 +0200 Subject: [PATCH] Added navigation delegation to bypass default one By making a delegate like such we protect ourselves against intrusions in views' logic --- .../magisk/model/events/ViewEvents.kt | 47 +++++++- .../model/navigation/MagiskNavigationEvent.kt | 10 +- .../topjohnwu/magisk/redesign/MainActivity.kt | 2 +- .../magisk/redesign/compat/CompatActivity.kt | 19 ++- .../magisk/redesign/compat/CompatDelegate.kt | 7 +- .../magisk/redesign/compat/CompatFragment.kt | 5 +- .../compat/CompatNavigationDelegate.kt | 112 ++++++++++++++++++ .../magisk/redesign/compat/CompatView.kt | 1 + 8 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatNavigationDelegate.kt diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index 206e765d1..b9a5ebd64 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -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, val callback: PublishSubject -) : 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, + 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() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt index 793c2a4c2..89be3ea61 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt @@ -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 { diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt index b46f6fa38..81772bc15 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/MainActivity.kt @@ -29,6 +29,7 @@ open class MainActivity : CompatActivity( override val layoutRes = R.layout.activity_main_md2 override val viewModel by viewModel() 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> = listOf( @@ -38,7 +39,6 @@ open class MainActivity : CompatActivity( 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatActivity.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatActivity.kt index a10b51e8e..33c5e9092 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatActivity.kt @@ -10,13 +10,19 @@ abstract class CompatActivity(), CompatView { override val viewRoot: View get() = binding.root + override val navigation: CompatNavigationDelegate>? 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 ) { + 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) } diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt index 8089f9cd8..6683b726c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatFragment.kt @@ -10,13 +10,16 @@ abstract class CompatFragment(), CompatView { 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() { diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatNavigationDelegate.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatNavigationDelegate.kt new file mode 100644 index 000000000..5a9a67463 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatNavigationDelegate.kt @@ -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( + 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatView.kt b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatView.kt index 4eb84f0c8..6257716c7 100644 --- a/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatView.kt +++ b/app/src/main/java/com/topjohnwu/magisk/redesign/compat/CompatView.kt @@ -7,6 +7,7 @@ internal interface CompatView { val viewRoot: View val viewModel: ViewModel + val navigation: CompatNavigationDelegate<*>? fun peekSystemWindowInsets(insets: Insets) = Unit fun consumeSystemWindowInsets(insets: Insets) = Insets.NONE