Refactor navigation to avoid overriding onBackPressed

This commit is contained in:
Jonas Lochmann 2022-09-12 02:00:00 +02:00
parent 09e6a30ee3
commit b5a5bc276e
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
10 changed files with 205 additions and 159 deletions

View file

@ -31,6 +31,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import io.timelimit.android.Application
import io.timelimit.android.R
@ -51,13 +52,7 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticatedUser
import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs
import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragment
import io.timelimit.android.ui.overview.main.MainFragment
import io.timelimit.android.ui.parentmode.ParentModeFragment
import io.timelimit.android.ui.payment.ActivityPurchaseModel
import io.timelimit.android.ui.setup.SetupTermsFragment
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
import io.timelimit.android.ui.util.SyncStatusModel
import java.security.SecureRandom
@ -101,7 +96,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
}
private val currentNavigatorFragment = MutableLiveData<Fragment?>()
private val application: Application by lazy { getApplication() as Application }
private val syncModel: SyncStatusModel by lazy {
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
}
@ -147,13 +141,11 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
}
// up button
val shouldShowBackButtonForNavigatorFragment = currentNavigatorFragment.map { fragment ->
(!(fragment is MainFragment)) && (!(fragment is SetupTermsFragment)) && (!(fragment is ParentModeFragment))
}
val shouldShowUpButton = shouldShowBackButtonForNavigatorFragment
shouldShowUpButton.observe(this, Observer { supportActionBar!!.setDisplayHomeAsUpEnabled(it) })
getNavController().addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener {
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
supportActionBar!!.setDisplayHomeAsUpEnabled(controller.previousBackStackEntry != null)
}
})
// init if not yet done
DefaultAppLogic.with(this)
@ -183,6 +175,33 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
syncModel.statusText.observe(this, Observer { supportActionBar!!.subtitle = it })
handleParameters(intent)
val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged()
val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged()
hasDeviceId.observe(this) {
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
if (!it) getActivityViewModel().logOut()
if (
it && rootDestination != R.id.overviewFragment ||
!it && rootDestination == R.id.overviewFragment
) {
restartContent()
}
}
hasParentKey.observe(this) {
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
if (
it && rootDestination != R.id.parentModeFragment ||
!it && rootDestination == R.id.parentModeFragment
) {
restartContent()
}
}
}
override fun onOptionsItemSelected(item: MenuItem) = when {
@ -259,29 +278,26 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
if (handleParameters(intent)) return
val currentFragment = currentNavigatorFragment.value
// at these screens, some users restart the App
// if they want to continue after opening the mail
// because they don't understand how to use the list of running Apps ...
// Due to that, on the relevant screens, the App does not
// go back to the start when opening it again
if (
currentFragment is SetupParentModeFragment ||
currentFragment is RestoreParentPasswordFragment ||
currentFragment is LinkParentMailFragment
) {
return
val isImportantScreen = when (getNavController().currentDestination?.id) {
R.id.setupParentModeFragment -> true
R.id.restoreParentPasswordFragment -> true
R.id.linkParentMailFragment -> true
else -> false
}
getNavController().popBackStack(R.id.overviewFragment, true)
getNavController().handleDeepLink(
getNavController().createDeepLink()
.setDestination(R.id.overviewFragment)
.createTaskStackBuilder()
.intents
.first()
)
if (!isImportantScreen) restartContent()
}
private fun restartContent() {
while (getNavController().popBackStack()) {/* do nothing */}
getNavController().clearBackStack(R.id.launchFragment)
getNavController().navigate(R.id.launchFragment)
}
override fun getActivityViewModel(): ActivityViewModel {
@ -296,15 +312,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
return getNavHostFragment().navController
}
override fun onBackPressed() {
if (currentNavigatorFragment.value is SetupTermsFragment || currentNavigatorFragment.value is ParentModeFragment) {
// hack to prevent the user from going to the launch screen of the App if it is not set up
finish()
} else {
super.onBackPressed()
}
}
override fun showAuthenticationScreen() {
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG)

View file

@ -0,0 +1,64 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.launch
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
import io.timelimit.android.ui.overview.main.MainFragmentDirections
class LaunchFragment: Fragment() {
private val model by viewModels<LaunchModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ObsoleteDialogFragment.show(requireActivity(), false)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.circular_progress_indicator, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navigation = Navigation.findNavController(view)
model.action.observe(viewLifecycleOwner) {
when (it) {
LaunchModel.Action.Setup -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToSetupTermsFragment(), R.id.launchFragment)
LaunchModel.Action.Overview -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
is LaunchModel.Action.Child -> {
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToManageChildFragment(it.id, fromRedirect = true), R.id.overviewFragment)
}
LaunchModel.Action.DeviceSetup -> {
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(), R.id.overviewFragment)
}
LaunchModel.Action.ParentMode -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToParentModeFragment(), R.id.launchFragment)
}
}
}
}

View file

@ -0,0 +1,68 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.launch
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.castDown
import io.timelimit.android.livedata.waitUntilValueMatches
import io.timelimit.android.logic.DefaultAppLogic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class LaunchModel(application: Application): AndroidViewModel(application) {
private val actionInternal = MutableLiveData<Action>()
private val logic = DefaultAppLogic.with(application)
val action = actionInternal.castDown()
init {
viewModelScope.launch {
withContext(Dispatchers.Main) {
logic.isInitialized.waitUntilValueMatches { it == true }
actionInternal.value = Threads.database.executeAndWait {
val hasDeviceId = logic.database.config().getOwnDeviceIdSync() != null
val hasParentKey = logic.database.config().getParentModeKeySync() != null
if (hasDeviceId) {
val config = logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
if (config?.userRelatedData?.user?.type == UserType.Child) Action.Child(config.userRelatedData.user.id)
else if (config?.userRelatedData == null) Action.DeviceSetup
else Action.Overview
}
else if (hasParentKey) Action.ParentMode
else Action.Setup
}
}
}
}
sealed class Action {
object Setup: Action()
object Overview: Action()
data class Child(val id: String): Action()
object DeviceSetup: Action()
object ParentMode: Action()
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,30 +19,19 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.fragment.SingleFragmentWrapper
import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
import io.timelimit.android.ui.overview.overview.OverviewFragment
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
class MainFragment : SingleFragmentWrapper(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers, FragmentWithCustomTitle {
private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
private var didRedirectToUserScreen = false
override val showAuthButton: Boolean = true
override fun createChildFragment(): Fragment = OverviewFragment()
@ -53,60 +42,6 @@ class MainFragment : SingleFragmentWrapper(), OverviewFragmentParentHandlers, Ab
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
logic.isInitialized.switchMap { isInitialized ->
if (isInitialized) {
logic.database.config().getOwnDeviceId().map { it == null }
} else {
liveDataFromNonNullValue(false)
}
}.observe(viewLifecycleOwner, Observer { shouldShowSetup ->
if (shouldShowSetup == true) {
runAsync {
val hasParentKey = Threads.database.executeAndWait { logic.database.config().getParentModeKeySync() != null }
if (isAdded && !parentFragmentManager.isStateSaved) {
if (hasParentKey) {
logic.platformIntegration.disableDeviceAdmin()
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToParentModeFragment(),
R.id.overviewFragment
)
} else {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToSetupTermsFragment(),
R.id.overviewFragment
)
}
}
}
} else {
if (savedInstanceState == null && !didRedirectToUserScreen) {
didRedirectToUserScreen = true
runAsync {
val user = logic.deviceUserEntry.waitForNullableValue()
if (user?.type == UserType.Child) {
if (isAdded && !parentFragmentManager.isStateSaved) {
openManageChildScreen(user.id, fromRedirect = true)
}
}
if (user != null) {
if (isAdded && !parentFragmentManager.isStateSaved) {
ObsoleteDialogFragment.show(requireActivity(), false)
}
}
}
}
}
})
}
override fun openAddDeviceScreen() {
AddDeviceFragment().show(parentFragmentManager)
}
@ -118,12 +53,10 @@ class MainFragment : SingleFragmentWrapper(), OverviewFragmentParentHandlers, Ab
)
}
override fun openManageChildScreen(childId: String) = openManageChildScreen(childId = childId, fromRedirect = false)
private fun openManageChildScreen(childId: String, fromRedirect: Boolean) {
override fun openManageChildScreen(childId: String) {
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToManageChildFragment(childId = childId, fromRedirect = fromRedirect),
R.id.overviewFragment
MainFragmentDirections.actionOverviewFragmentToManageChildFragment(childId = childId, fromRedirect = false),
R.id.overviewFragment
)
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -21,8 +21,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.navigation.NavController
import androidx.navigation.Navigation
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.databinding.FragmentUninstallBinding
@ -43,7 +41,6 @@ class UninstallFragment : Fragment(), FragmentWithCustomTitle {
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() }
private var showBackdoorButton = false
private lateinit var navigation: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -52,8 +49,6 @@ class UninstallFragment : Fragment(), FragmentWithCustomTitle {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
navigation = Navigation.findNavController(container!!)
val binding = FragmentUninstallBinding.inflate(inflater, container, false)
binding.uninstall.isEnabled = binding.checkConfirm.isChecked
@ -74,10 +69,6 @@ class UninstallFragment : Fragment(), FragmentWithCustomTitle {
binding.showBackdoorButton = showBackdoorButton
auth.logic.deviceId.observe(viewLifecycleOwner) {
if (it == null) { navigation.popBackStack() }
}
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
fragment = this,

View file

@ -81,7 +81,6 @@ class SetupLocalModeFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentSetupLocalModeBinding.inflate(inflater, container, false)
val navigation = Navigation.findNavController(container!!)
binding.setPasswordView.allowNoPassword.value = true
@ -97,8 +96,6 @@ class SetupLocalModeFragment : Fragment() {
model.status.observe(viewLifecycleOwner) {
if (it == SetupLocalModeModel.Status.Done) {
MustReadFragment.newInstance(R.string.must_read_child_manipulation).show(fragmentManager!!)
navigation.popBackStack(R.id.overviewFragment, false)
}
}

View file

@ -140,8 +140,6 @@ class SetupSelectModeFragment : Fragment() {
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupParentModeFragment(),
R.id.setupSelectModeFragment
)
} else if (requestCode == REQUEST_SETUP_PARENT_MODE && resultCode == Activity.RESULT_OK) {
navigation.popBackStack(R.id.overviewFragment, false)
}
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -22,13 +22,10 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import com.google.android.material.snackbar.Snackbar
import io.timelimit.android.R
import io.timelimit.android.databinding.SetupRemoteChildFragmentBinding
import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.extensions.setOnEnterListenr
import io.timelimit.android.ui.overview.main.MainFragmentDirections
class SetupRemoteChildFragment : Fragment() {
private val model: SetupRemoteChildViewModel by lazy {
@ -65,18 +62,6 @@ class SetupRemoteChildFragment : Fragment() {
}.let { }
})
model.isSetupDone.observe(this, Observer {
if (it!!) {
val navigation = Navigation.findNavController(binding.root)
navigation.popBackStack(R.id.overviewFragment, false)
navigation.safeNavigate(
MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(),
R.id.overviewFragment
)
}
})
return binding.root
}
}

View file

@ -28,7 +28,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.observe
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
@ -195,12 +194,6 @@ class SetupParentModeFragment : Fragment(), AuthenticateByMailFragmentListener {
}
}
model.isSetupDone.observe(this, Observer {
if (it!!) {
Navigation.findNavController(binding.root).popBackStack(R.id.overviewFragment, false)
}
})
UpdateConsentCard.bind(
view = binding.update,
lifecycleOwner = viewLifecycleOwner,

View file

@ -16,7 +16,7 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/overviewFragment">
app:startDestination="@id/launchFragment">
<fragment
android:id="@+id/overviewFragment"
@ -37,13 +37,6 @@
app:popExitAnim="@anim/nav_default_pop_exit_anim"
android:id="@+id/action_overviewFragment_to_addUserFragment"
app:destination="@id/addUserFragment" />
<action
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
android:id="@+id/action_overviewFragment_to_setupTermsFragment"
app:destination="@id/setupTermsFragment" />
<action
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
@ -86,9 +79,6 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_overviewFragment_to_parentModeFragment"
app:destination="@id/parentModeFragment" />
<action
android:id="@+id/action_overviewFragment_to_aboutFragmentWrapped"
app:destination="@id/aboutFragmentWrapped"
@ -622,4 +612,24 @@
android:id="@+id/diagnoseCryptoFragment"
android:name="io.timelimit.android.ui.diagnose.DiagnoseCryptoFragment"
android:label="DiagnoseCryptoFragment" />
<fragment
android:id="@+id/launchFragment"
android:name="io.timelimit.android.ui.launch.LaunchFragment"
android:label="LaunchFragment">
<action
android:id="@+id/action_launchFragment_to_overviewFragment"
app:destination="@id/overviewFragment"
app:popUpTo="@id/launchFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_launchFragment_to_setupTermsFragment"
app:destination="@id/setupTermsFragment"
app:popUpTo="@id/launchFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_launchFragment_to_parentModeFragment"
app:destination="@id/parentModeFragment"
app:popUpTo="@id/launchFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>