Skip to content

Commit

Permalink
Merge pull request getodk#5829 from seadowg/fix-menu-crash
Browse files Browse the repository at this point in the history
Extract Main Menu to Fragment to prevent lifecycle crashes
  • Loading branch information
grzesiek2010 authored and seadowg committed Nov 30, 2023
1 parent fd75501 commit c7aae47
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 248 deletions.
Original file line number Diff line number Diff line change
@@ -1,45 +1,20 @@
package org.odk.collect.android.mainmenu

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.odk.collect.android.R
import org.odk.collect.android.activities.ActivityUtils
import org.odk.collect.android.activities.CrashHandlerActivity
import org.odk.collect.android.activities.DeleteSavedFormActivity
import org.odk.collect.android.activities.FirstLaunchActivity
import org.odk.collect.android.activities.FormDownloadListActivity
import org.odk.collect.android.activities.InstanceChooserList
import org.odk.collect.android.activities.WebViewActivity
import org.odk.collect.android.application.MapboxClassInstanceCreator.createMapBoxInitializationFragment
import org.odk.collect.android.application.MapboxClassInstanceCreator.isMapboxAvailable
import org.odk.collect.android.databinding.MainMenuBinding
import org.odk.collect.android.formlists.blankformlist.BlankFormListActivity
import org.odk.collect.android.formmanagement.FormFillingIntentFactory
import org.odk.collect.android.injection.DaggerUtils
import org.odk.collect.android.instancemanagement.send.InstanceUploaderListActivity
import org.odk.collect.android.projects.ProjectIconView
import org.odk.collect.android.projects.ProjectSettingsDialog
import org.odk.collect.android.utilities.ApplicationConstants
import org.odk.collect.android.utilities.ThemeUtils
import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing
import org.odk.collect.androidshared.ui.FragmentFactoryBuilder
import org.odk.collect.androidshared.ui.SnackbarUtils
import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard.allowClick
import org.odk.collect.crashhandler.CrashHandler
import org.odk.collect.permissions.PermissionsProvider
import org.odk.collect.projects.Project.Saved
import org.odk.collect.settings.SettingsProvider
import org.odk.collect.settings.keys.ProjectKeys
import org.odk.collect.strings.localization.LocalizedActivity
import javax.inject.Inject

Expand All @@ -54,15 +29,8 @@ class MainMenuActivity : LocalizedActivity() {
@Inject
lateinit var permissionsProvider: PermissionsProvider

private lateinit var binding: MainMenuBinding
private lateinit var mainMenuViewModel: MainMenuViewModel
private lateinit var currentProjectViewModel: CurrentProjectViewModel

private val formLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
displayFormSavedSnackbar(it.data?.data)
}

override fun onCreate(savedInstanceState: Bundle?) {
initSplashScreen()

Expand All @@ -77,98 +45,33 @@ class MainMenuActivity : LocalizedActivity() {
DaggerUtils.getComponent(this).inject(this)

val viewModelProvider = ViewModelProvider(this, viewModelFactory)
mainMenuViewModel = viewModelProvider[MainMenuViewModel::class.java]
currentProjectViewModel = viewModelProvider[CurrentProjectViewModel::class.java]
val permissionsViewModel = viewModelProvider[RequestPermissionsViewModel::class.java]

this.supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()
.forClass(PermissionsDialogFragment::class) {
PermissionsDialogFragment(
permissionsProvider,
permissionsViewModel
)
}
.forClass(ProjectSettingsDialog::class) {
ProjectSettingsDialog(viewModelFactory)
}
.build()

super.onCreate(savedInstanceState)
binding = MainMenuBinding.inflate(layoutInflater)

ThemeUtils(this).setDarkModeForCurrentProject()

if (!currentProjectViewModel.hasCurrentProject()) {
super.onCreate(null)
ActivityUtils.startActivityAndCloseAllOthers(this, FirstLaunchActivity::class.java)
return
}

setContentView(binding.root)

currentProjectViewModel.currentProject.observe(this) { (_, name): Saved ->
invalidateOptionsMenu()
title = name
}

initToolbar()
initMapbox()
initButtons()
initAppName()

if (permissionsViewModel.shouldAskForPermissions()) {
showIfNotShowing(PermissionsDialogFragment::class.java, this.supportFragmentManager)
}
}

override fun onResume() {
super.onResume()
if (isFinishing) {
return // Guard against onResume calls after we've finished in onCreate
}

currentProjectViewModel.refresh()
mainMenuViewModel.refreshInstances()
setButtonsVisibility()
manageGoogleDriveDeprecationBanner()
}

private fun setButtonsVisibility() {
binding.reviewData.visibility =
if (mainMenuViewModel.shouldEditSavedFormButtonBeVisible()) View.VISIBLE else View.GONE
binding.sendData.visibility =
if (mainMenuViewModel.shouldSendFinalizedFormButtonBeVisible()) View.VISIBLE else View.GONE
binding.viewSentForms.visibility =
if (mainMenuViewModel.shouldViewSentFormButtonBeVisible()) View.VISIBLE else View.GONE
binding.getForms.visibility =
if (mainMenuViewModel.shouldGetBlankFormButtonBeVisible()) View.VISIBLE else View.GONE
binding.manageForms.visibility =
if (mainMenuViewModel.shouldDeleteSavedFormButtonBeVisible()) View.VISIBLE else View.GONE
}

override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val projectsMenuItem = menu.findItem(R.id.projects)
(projectsMenuItem.actionView as ProjectIconView).apply {
project = currentProjectViewModel.currentProject.value
setOnClickListener { onOptionsItemSelected(projectsMenuItem) }
contentDescription = getString(org.odk.collect.strings.R.string.projects)
}
return super.onPrepareOptionsMenu(menu)
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return super.onCreateOptionsMenu(menu)
}
} else {
this.supportFragmentManager.fragmentFactory = FragmentFactoryBuilder()
.forClass(PermissionsDialogFragment::class) {
PermissionsDialogFragment(
permissionsProvider,
viewModelProvider[RequestPermissionsViewModel::class.java]
)
}
.forClass(ProjectSettingsDialog::class) {
ProjectSettingsDialog(viewModelFactory)
}
.forClass(MainMenuFragment::class) {
MainMenuFragment(viewModelFactory, settingsProvider)
}
.build()

override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (!allowClick(ApplicationConstants.ScreenName.MAIN_MENU.name)) {
return true
}
if (item.itemId == R.id.projects) {
showIfNotShowing(ProjectSettingsDialog::class.java, supportFragmentManager)
return true
super.onCreate(savedInstanceState)
setContentView(R.layout.main_menu_activity)
}
return super.onOptionsItemSelected(item)
}

private fun initSplashScreen() {
Expand All @@ -183,136 +86,4 @@ class MainMenuActivity : LocalizedActivity() {
setTheme(R.style.Theme_Collect)
}
}

private fun initToolbar() {
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
}

private fun initMapbox() {
if (isMapboxAvailable()) {
supportFragmentManager
.beginTransaction()
.add(R.id.map_box_initialization_fragment, createMapBoxInitializationFragment())
.commit()
}
}

private fun initButtons() {
binding.enterData.setOnClickListener {
formLauncher.launch(
Intent(this, BlankFormListActivity::class.java)
)
}

binding.reviewData.setOnClickListener {
formLauncher.launch(
Intent(this, InstanceChooserList::class.java).apply {
putExtra(
ApplicationConstants.BundleKeys.FORM_MODE,
ApplicationConstants.FormModes.EDIT_SAVED
)
}
)
}

binding.sendData.setOnClickListener {
startActivity(Intent(this, InstanceUploaderListActivity::class.java))
}

binding.viewSentForms.setOnClickListener {
startActivity(
Intent(this, InstanceChooserList::class.java).apply {
putExtra(
ApplicationConstants.BundleKeys.FORM_MODE,
ApplicationConstants.FormModes.VIEW_SENT
)
}
)
}

binding.getForms.setOnClickListener {
val protocol =
settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_PROTOCOL)
if (!protocol.equals(ProjectKeys.PROTOCOL_GOOGLE_SHEETS, ignoreCase = true)) {
val intent = Intent(
applicationContext,
FormDownloadListActivity::class.java
)

startActivity(intent)
} else {
MaterialAlertDialogBuilder(this)
.setMessage(org.odk.collect.strings.R.string.cannot_start_new_forms_in_google_drive_projects)
.setPositiveButton(org.odk.collect.strings.R.string.ok, null)
.create()
.show()
}
}

binding.manageForms.setOnClickListener {
startActivity(Intent(this, DeleteSavedFormActivity::class.java))
}

mainMenuViewModel.sendableInstancesCount.observe(this) { finalized: Int ->
binding.sendData.setNumberOfForms(finalized)
}
mainMenuViewModel.editableInstancesCount.observe(this) { unsent: Int ->
binding.reviewData.setNumberOfForms(unsent)
}
mainMenuViewModel.sentInstancesCount.observe(this) { sent: Int ->
binding.viewSentForms.setNumberOfForms(sent)
}
}

private fun initAppName() {
binding.appName.text = String.format(
"%s %s",
getString(org.odk.collect.strings.R.string.collect_app_name),
mainMenuViewModel.version
)

val versionSHA = mainMenuViewModel.versionCommitDescription
if (versionSHA != null) {
binding.versionSha.text = versionSHA
} else {
binding.versionSha.visibility = View.GONE
}
}

private fun manageGoogleDriveDeprecationBanner() {
val unprotectedSettings = settingsProvider.getUnprotectedSettings()
val protocol = unprotectedSettings.getString(ProjectKeys.KEY_PROTOCOL)
if (ProjectKeys.PROTOCOL_GOOGLE_SHEETS == protocol) {
binding.googleDriveDeprecationBanner.root.visibility = View.VISIBLE
binding.googleDriveDeprecationBanner.learnMoreButton.setOnClickListener {
val intent = Intent(this, WebViewActivity::class.java)
intent.putExtra("url", "https://forum.getodk.org/t/40097")
startActivity(intent)
}
} else {
binding.googleDriveDeprecationBanner.root.visibility = View.GONE
}
}

private fun displayFormSavedSnackbar(uri: Uri?) {
if (uri == null) {
return
}

val formSavedSnackbarDetails = mainMenuViewModel.getFormSavedSnackbarDetails(uri)

formSavedSnackbarDetails?.let {
SnackbarUtils.showLongSnackbar(
binding.root,
getString(it.first),
action = it.second?.let { action ->
SnackbarUtils.Action(getString(action)) {
formLauncher.launch(FormFillingIntentFactory.editInstanceIntent(this, uri))
}
},
displayDismissButton = true
)
}
}
}
Loading

0 comments on commit c7aae47

Please sign in to comment.