diff --git a/app/build.gradle b/app/build.gradle index 488a7dc75..b8e501ac6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' kapt { @@ -80,4 +80,7 @@ dependencies { exclude group: 'androidx.work', module: 'work-runtime-ktx' exclude group: 'androidx.room', module: 'room-runtime' } + + def navigation = "3.2.0" + implementation "com.ncapdevi:frag-nav:${navigation}" } 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 new file mode 100644 index 000000000..8cdc0fda3 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/MagiskNavigationEvent.kt @@ -0,0 +1,82 @@ +package com.topjohnwu.magisk.model.navigation + +import android.os.Bundle +import androidx.annotation.AnimRes +import androidx.annotation.AnimatorRes +import androidx.fragment.app.Fragment +import com.skoumal.teanity.viewevents.NavigationDslMarker +import com.skoumal.teanity.viewevents.ViewEvent +import kotlin.reflect.KClass + +class MagiskNavigationEvent( + val navDirections: MagiskNavDirectionsBuilder, + val navOptions: MagiskNavOptions, + val animOptions: MagiskAnimBuilder +) : ViewEvent() { + + companion object { + operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build() + } + + @NavigationDslMarker + class Builder { + + private var animOptions: MagiskAnimBuilder = MagiskAnimBuilder() + private var navOptions: MagiskNavOptions = MagiskNavOptions() + private val directionsBuilder = MagiskNavDirectionsBuilder() + + fun args(builder: Bundle.() -> Unit) = directionsBuilder.args(builder) + + fun navAnim(builder: MagiskAnimBuilder.() -> Unit) { + animOptions = MagiskAnimBuilder().apply(builder) + } + + fun navOptions(builder: MagiskNavOptions.() -> Unit) { + navOptions = MagiskNavOptions().apply(builder) + } + + fun navDirections(builder: MagiskNavDirectionsBuilder.() -> Unit) { + directionsBuilder.apply(builder) + } + + internal fun build() = MagiskNavigationEvent(directionsBuilder, navOptions, animOptions) + } +} + +@NavigationDslMarker +class MagiskNavDirectionsBuilder { + + var destination: KClass? = null + var isActivity: Boolean = false + val args: Bundle = Bundle() + + fun args(builder: Bundle.() -> Unit) = args.apply(builder) + +} + +@NavigationDslMarker +class MagiskNavOptions { + var popUpTo: KClass<*>? = null + var inclusive: Boolean = false + var clearTask: Boolean = false + var singleTop: Boolean = false +} + +@NavigationDslMarker +class MagiskAnimBuilder { + @AnimRes + @AnimatorRes + var enter = 0 + + @AnimRes + @AnimatorRes + var exit = 0 + + @AnimRes + @AnimatorRes + var popEnter = 0 + + @AnimRes + @AnimatorRes + var popExit = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt index 7a971e5f5..ea1e6ac98 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigation.kt @@ -1,34 +1,43 @@ package com.topjohnwu.magisk.model.navigation -import com.skoumal.teanity.viewevents.NavigationEvent -import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.ui.hide.MagiskHideFragment +import com.topjohnwu.magisk.ui.home.MagiskFragment +import com.topjohnwu.magisk.ui.log.LogFragment +import com.topjohnwu.magisk.ui.module.ModulesFragment +import com.topjohnwu.magisk.ui.module.ReposFragment +import com.topjohnwu.magisk.ui.settings.SettingsFragment +import com.topjohnwu.magisk.ui.superuser.SuperuserFragment object Navigation { - fun home() = NavigationEvent { - navDirections { destination = R.id.magiskFragment } - navOptions { popUpTo = R.id.magiskFragment } + fun home() = MagiskNavigationEvent { + navDirections { destination = MagiskFragment::class } + navOptions { popUpTo = MagiskFragment::class } } - fun superuser() = NavigationEvent { - navDirections { destination = R.id.superuserFragment } + fun superuser() = MagiskNavigationEvent { + navDirections { destination = SuperuserFragment::class } } - fun modules() = NavigationEvent { - navDirections { destination = R.id.modulesFragment } + fun modules() = MagiskNavigationEvent { + navDirections { destination = ModulesFragment::class } } - fun repos() = NavigationEvent { - navDirections { destination = R.id.reposFragment } + fun repos() = MagiskNavigationEvent { + navDirections { destination = ReposFragment::class } } - fun hide() = NavigationEvent { - navDirections { destination = R.id.magiskHideFragment } + fun hide() = MagiskNavigationEvent { + navDirections { destination = MagiskHideFragment::class } } - fun log() = NavigationEvent { - navDirections { destination = R.id.logFragment } + fun log() = MagiskNavigationEvent { + navDirections { destination = LogFragment::class } + } + + fun settings() = MagiskNavigationEvent { + navDirections { destination = SettingsFragment::class } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt new file mode 100644 index 000000000..e2b755a9d --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/navigation/Navigator.kt @@ -0,0 +1,13 @@ +package com.topjohnwu.magisk.model.navigation + +import androidx.fragment.app.Fragment +import kotlin.reflect.KClass + +interface Navigator { + + //TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it + val baseFragments: List> + + fun navigateTo(event: MagiskNavigationEvent) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt index de80683ad..237ee46d3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -3,23 +3,42 @@ package com.topjohnwu.magisk.ui import android.content.Intent import android.os.Bundle import androidx.core.view.GravityCompat -import androidx.navigation.ui.setupWithNavController +import androidx.fragment.app.Fragment import com.topjohnwu.magisk.ClassMap import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.R import com.topjohnwu.magisk.databinding.ActivityMainBinding import com.topjohnwu.magisk.model.navigation.Navigation import com.topjohnwu.magisk.ui.base.MagiskActivity +import com.topjohnwu.magisk.ui.hide.MagiskHideFragment +import com.topjohnwu.magisk.ui.log.LogFragment +import com.topjohnwu.magisk.ui.module.ModulesFragment +import com.topjohnwu.magisk.ui.module.ReposFragment +import com.topjohnwu.magisk.ui.settings.SettingsFragment +import com.topjohnwu.magisk.ui.superuser.SuperuserFragment import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.net.Networking import com.topjohnwu.superuser.Shell import org.koin.androidx.viewmodel.ext.android.viewModel +import kotlin.reflect.KClass +import com.topjohnwu.magisk.ui.home.MagiskFragment as HomeFragment open class MainActivity : MagiskActivity() { override val layoutRes: Int = R.layout.activity_main override val viewModel: MainViewModel by viewModel() override val navHostId: Int = R.id.main_nav_host + override val defaultPosition: Int = 0 + + override val baseFragments: List> = listOf( + HomeFragment::class, + SuperuserFragment::class, + MagiskHideFragment::class, + ModulesFragment::class, + ReposFragment::class, + LogFragment::class, + SettingsFragment::class + ) /*override fun getDarkTheme(): Int { return R.style.AppTheme_Dark @@ -35,7 +54,6 @@ open class MainActivity : MagiskActivity() { checkHideSection() setSupportActionBar(binding.mainInclude.mainToolbar) - binding.navView.setupWithNavController(navController) } override fun onBackPressed() { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt index 883adca5f..73d8b80c8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt @@ -1,5 +1,7 @@ package com.topjohnwu.magisk.ui +import android.view.MenuItem +import com.topjohnwu.magisk.R import com.topjohnwu.magisk.model.navigation.Navigation import com.topjohnwu.magisk.ui.base.MagiskViewModel @@ -8,4 +10,18 @@ class MainViewModel : MagiskViewModel() { fun navPressed() = Navigation.Main.OPEN_NAV.publish() + fun navigationItemPressed(item: MenuItem): Boolean { + when (item.itemId) { + R.id.magiskFragment -> Navigation.home() + R.id.superuserFragment -> Navigation.superuser() + R.id.magiskHideFragment -> Navigation.hide() + R.id.modulesFragment -> Navigation.modules() + R.id.reposFragment -> Navigation.repos() + R.id.logFragment -> Navigation.log() + R.id.settings -> Navigation.settings() + else -> null + }?.publish()?.let { return@navigationItemPressed true } + return false + } + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt index 1e5688f4a..5992a04e8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt @@ -1,13 +1,126 @@ package com.topjohnwu.magisk.ui.base +import android.content.Intent +import android.os.Bundle +import androidx.annotation.CallSuper import androidx.core.net.toUri import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import com.ncapdevi.fragnav.FragNavController +import com.ncapdevi.fragnav.FragNavTransactionOptions +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder +import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent +import com.topjohnwu.magisk.model.navigation.Navigator import com.topjohnwu.magisk.utils.Utils +import timber.log.Timber +import kotlin.reflect.KClass abstract class MagiskActivity : - MagiskLeanbackActivity() { + MagiskLeanbackActivity(), FragNavController.RootFragmentListener, + Navigator { + + override val numberOfRootFragments: Int get() = baseFragments.size + override val baseFragments: List> = listOf() + + protected open val defaultPosition: Int = 0 + + protected val navigationController by lazy { + if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?") + FragNavController(supportFragmentManager, navHostId) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + navigationController.apply { + rootFragmentListener = this@MagiskActivity + initialize(defaultPosition, savedInstanceState) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + navigationController.onSaveInstanceState(outState) + } + + @CallSuper + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is MagiskNavigationEvent -> navigateTo(event) + } + } + + override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance() + + override fun navigateTo(event: MagiskNavigationEvent) { + val directions = event.navDirections + + navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder() + .customAnimations(event.animOptions) + .build() + + navigationController.currentStack + ?.indexOfFirst { it.javaClass == event.navOptions.popUpTo } + ?.let { if (it == -1) null else it } // invalidate if class is not found + ?.let { if (event.navOptions.inclusive) it + 1 else it } + ?.let { navigationController.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(this, 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 { startActivity(it) } + } + + private fun navigateToFragment(event: MagiskNavigationEvent) { + val destination = event.navDirections.destination?.java ?: let { + Timber.e("Cannot navigate to null destination") + return + } + + when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) { + -1 -> destination.newInstance() + .apply { arguments = event.navDirections.args } + .let { navigationController.pushFragment(it) } + // When it's desired that fragments of same class are put on top of one another edit this + else -> navigationController.switchTab(index) + } + } + + override fun onBackPressed() { + val fragment = navigationController.currentFrag as? MagiskFragment<*, *> + + if (fragment?.onBackPressed() == true) { + return + } + + try { + navigationController.popFragment() + } catch (e: UnsupportedOperationException) { + super.onBackPressed() + } + } fun openUrl(url: String) = Utils.openLink(this, url.toUri()) + private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) = + customAnimations(options.enter, options.exit, options.popEnter, options.popExit) + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt index 8d186a228..fe7f97922 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt @@ -1,16 +1,35 @@ package com.topjohnwu.magisk.ui.base +import androidx.annotation.CallSuper import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment import com.skoumal.teanity.view.TeanityFragment +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent +import com.topjohnwu.magisk.model.navigation.Navigator +import kotlin.reflect.KClass abstract class MagiskFragment : - TeanityFragment() { + TeanityFragment(), Navigator { protected val magiskActivity get() = activity as MagiskActivity<*, *> - fun openLink(url: String) { - magiskActivity.openUrl(url) + // We don't need nested fragments + override val baseFragments: List> = listOf() + + override fun navigateTo(event: MagiskNavigationEvent) = magiskActivity.navigateTo(event) + + @CallSuper + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is MagiskNavigationEvent -> navigateTo(event) + } } + fun openLink(url: String) = magiskActivity.openUrl(url) + + open fun onBackPressed(): Boolean = false + } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt index 4f1e27800..7f95bca87 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -6,6 +6,8 @@ import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.Toolbar import androidx.databinding.BindingAdapter +import androidx.drawerlayout.widget.DrawerLayout +import com.google.android.material.navigation.NavigationView @BindingAdapter("onNavigationClick") @@ -13,6 +15,17 @@ fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener view.setNavigationOnClickListener(listener) } +@BindingAdapter("onNavigationClick") +fun setOnNavigationClickedListener( + view: NavigationView, + listener: NavigationView.OnNavigationItemSelectedListener +) { + view.setNavigationItemSelectedListener { + (view.parent as? DrawerLayout)?.closeDrawers() + listener.onNavigationItemSelected(it) + } +} + @BindingAdapter("srcCompat") fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) { view.setImageResource(resId) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d21d436b5..f2300aafb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -29,6 +29,7 @@ android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" + onNavigationClick="@{(item) -> viewModel.navigationItemPressed(item)}" app:menu="@menu/drawer" /> diff --git a/app/src/main/res/layout/activity_main_content.xml b/app/src/main/res/layout/activity_main_content.xml index da5318b8e..b8e5cc4e7 100644 --- a/app/src/main/res/layout/activity_main_content.xml +++ b/app/src/main/res/layout/activity_main_content.xml @@ -31,15 +31,12 @@ - + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> diff --git a/app/src/main/res/navigation/navigation_main.xml b/app/src/main/res/navigation/navigation_main.xml deleted file mode 100644 index 83313cb43..000000000 --- a/app/src/main/res/navigation/navigation_main.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - diff --git a/build.gradle b/build.gradle index 246eb4fb9..e7e0d303f 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,6 @@ buildscript { classpath 'com.android.tools:r8:1.4.79' classpath 'com.android.tools.build:gradle:3.3.2' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.30' - classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files