diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index bd31de6e332..c3184c76583 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -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 @@ -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() @@ -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() { @@ -183,136 +86,4 @@ class MainMenuActivity : LocalizedActivity() { setTheme(R.style.Theme_Collect) } } - - private fun initToolbar() { - val toolbar = findViewById(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 - ) - } - } } diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuFragment.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuFragment.kt new file mode 100644 index 00000000000..9f5269cd2c3 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuFragment.kt @@ -0,0 +1,287 @@ +package org.odk.collect.android.mainmenu + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.odk.collect.android.activities.DeleteSavedFormActivity +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 +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.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.androidshared.ui.DialogFragmentUtils +import org.odk.collect.androidshared.ui.SnackbarUtils +import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard +import org.odk.collect.projects.Project +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys +import org.odk.collect.strings.R.string + +class MainMenuFragment( + private val viewModelFactory: ViewModelProvider.Factory, + private val settingsProvider: SettingsProvider +) : Fragment() { + + private lateinit var mainMenuViewModel: MainMenuViewModel + private lateinit var currentProjectViewModel: CurrentProjectViewModel + private lateinit var permissionsViewModel: RequestPermissionsViewModel + + private val formEntryFlowLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + displayFormSavedSnackbar(it.data?.data) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + val viewModelProvider = ViewModelProvider(this, viewModelFactory) + mainMenuViewModel = viewModelProvider[MainMenuViewModel::class.java] + currentProjectViewModel = viewModelProvider[CurrentProjectViewModel::class.java] + permissionsViewModel = viewModelProvider[RequestPermissionsViewModel::class.java] + + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return MainMenuBinding.inflate(inflater, container, false).root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + currentProjectViewModel.currentProject.observe(viewLifecycleOwner) { (_, name): Project.Saved -> + requireActivity().invalidateOptionsMenu() + requireActivity().title = name + } + + val binding = MainMenuBinding.bind(view) + initToolbar(binding) + initMapbox() + initButtons(binding) + initAppName(binding) + + if (permissionsViewModel.shouldAskForPermissions()) { + DialogFragmentUtils.showIfNotShowing( + PermissionsDialogFragment::class.java, + this.parentFragmentManager + ) + } + } + + override fun onResume() { + super.onResume() + + currentProjectViewModel.refresh() + mainMenuViewModel.refreshInstances() + + val binding = MainMenuBinding.bind(requireView()) + setButtonsVisibility(binding) + manageGoogleDriveDeprecationBanner(binding) + } + + override fun onPrepareOptionsMenu(menu: Menu) { + val projectsMenuItem = menu.findItem(org.odk.collect.android.R.id.projects) + (projectsMenuItem.actionView as ProjectIconView).apply { + project = currentProjectViewModel.currentProject.value + setOnClickListener { onOptionsItemSelected(projectsMenuItem) } + contentDescription = getString(string.projects) + } + } + + override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(org.odk.collect.android.R.menu.main_menu, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (!MultiClickGuard.allowClick(javaClass.name)) { + return true + } + if (item.itemId == org.odk.collect.android.R.id.projects) { + DialogFragmentUtils.showIfNotShowing( + ProjectSettingsDialog::class.java, + parentFragmentManager + ) + return true + } + + return super.onOptionsItemSelected(item) + } + + private fun initToolbar(binding: MainMenuBinding) { + val toolbar = binding.root.findViewById(org.odk.collect.android.R.id.toolbar) + (requireActivity() as AppCompatActivity).setSupportActionBar(toolbar) + } + + private fun initMapbox() { + if (MapboxClassInstanceCreator.isMapboxAvailable()) { + childFragmentManager + .beginTransaction() + .add( + org.odk.collect.android.R.id.map_box_initialization_fragment, + MapboxClassInstanceCreator.createMapBoxInitializationFragment()!! + ) + .commit() + } + } + + private fun initButtons(binding: MainMenuBinding) { + binding.enterData.setOnClickListener { + formEntryFlowLauncher.launch( + Intent(requireActivity(), BlankFormListActivity::class.java) + ) + } + + binding.reviewData.setOnClickListener { + formEntryFlowLauncher.launch( + Intent(requireActivity(), InstanceChooserList::class.java).apply { + putExtra( + ApplicationConstants.BundleKeys.FORM_MODE, + ApplicationConstants.FormModes.EDIT_SAVED + ) + } + ) + } + + binding.sendData.setOnClickListener { + formEntryFlowLauncher.launch( + Intent( + requireActivity(), + InstanceUploaderListActivity::class.java + ) + ) + } + + binding.viewSentForms.setOnClickListener { + startActivity( + Intent(requireActivity(), 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( + requireContext(), + FormDownloadListActivity::class.java + ) + + startActivity(intent) + } else { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(string.cannot_start_new_forms_in_google_drive_projects) + .setPositiveButton(string.ok, null) + .create() + .show() + } + } + + binding.manageForms.setOnClickListener { + startActivity(Intent(requireContext(), DeleteSavedFormActivity::class.java)) + } + + mainMenuViewModel.sendableInstancesCount.observe(viewLifecycleOwner) { finalized: Int -> + binding.sendData.setNumberOfForms(finalized) + } + mainMenuViewModel.editableInstancesCount.observe(viewLifecycleOwner) { unsent: Int -> + binding.reviewData.setNumberOfForms(unsent) + } + mainMenuViewModel.sentInstancesCount.observe(viewLifecycleOwner) { sent: Int -> + binding.viewSentForms.setNumberOfForms(sent) + } + } + + private fun initAppName(binding: MainMenuBinding) { + binding.appName.text = String.format( + "%s %s", + getString(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 setButtonsVisibility(binding: MainMenuBinding) { + 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 + } + + private fun manageGoogleDriveDeprecationBanner(binding: MainMenuBinding) { + 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(requireContext(), 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( + requireView(), + getString(it.first), + action = it.second?.let { action -> + SnackbarUtils.Action(getString(action)) { + formEntryFlowLauncher.launch( + FormFillingIntentFactory.editInstanceIntent( + requireContext(), + uri + ) + ) + } + }, + displayDismissButton = true + ) + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/projects/ProjectSettingsDialog.kt b/collect_app/src/main/java/org/odk/collect/android/projects/ProjectSettingsDialog.kt index f3c745d62de..45dd53e452a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/projects/ProjectSettingsDialog.kt +++ b/collect_app/src/main/java/org/odk/collect/android/projects/ProjectSettingsDialog.kt @@ -10,7 +10,6 @@ import android.view.View.VISIBLE import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.odk.collect.android.R import org.odk.collect.android.activities.AboutActivity import org.odk.collect.android.activities.ActivityUtils import org.odk.collect.android.databinding.ProjectSettingsDialogLayoutBinding @@ -45,6 +44,7 @@ class ProjectSettingsDialog(private val viewModelFactory: ViewModelProvider.Fact requireActivity(), viewModelFactory )[CurrentProjectViewModel::class.java] + currentProjectViewModel.refresh() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/collect_app/src/main/res/layout/main_menu_activity.xml b/collect_app/src/main/res/layout/main_menu_activity.xml new file mode 100644 index 00000000000..633bdf892aa --- /dev/null +++ b/collect_app/src/main/res/layout/main_menu_activity.xml @@ -0,0 +1,12 @@ + + + + + +