fix: make settings dialog recreation-safe
This commit is contained in:
@@ -492,6 +492,24 @@ class BoardsFlowTest {
|
|||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun settingsDialogSurvivesActivityRecreate() {
|
||||||
|
MainActivity.dependencies.apiClientFactory = {
|
||||||
|
FakeBoardsApiClient(
|
||||||
|
boards = mutableListOf(BoardSummary("1", "Alpha")),
|
||||||
|
templates = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
openSettingsFromDrawer()
|
||||||
|
|
||||||
|
scenario.recreate()
|
||||||
|
|
||||||
|
onView(withId(R.id.settingsFragmentContainer)).check(matches(isDisplayed()))
|
||||||
|
onView(withId(R.id.settingsSaveAndCloseButton)).check(matches(isDisplayed()))
|
||||||
|
}
|
||||||
|
|
||||||
private fun openSettingsFromDrawer() {
|
private fun openSettingsFromDrawer() {
|
||||||
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
||||||
onView(withId(R.id.drawerSettingsButton)).perform(click())
|
onView(withId(R.id.drawerSettingsButton)).perform(click())
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
private lateinit var apiKeyStore: ApiKeyStore
|
private lateinit var apiKeyStore: ApiKeyStore
|
||||||
private lateinit var apiClient: KanbnApiClient
|
private lateinit var apiClient: KanbnApiClient
|
||||||
|
|
||||||
|
internal val sessionStoreForSettingsDialog: SessionStore
|
||||||
|
get() = sessionStore
|
||||||
|
|
||||||
|
internal val apiKeyStoreForSettingsDialog: ApiKeyStore
|
||||||
|
get() = apiKeyStore
|
||||||
|
|
||||||
|
internal val apiClientForSettingsDialog: KanbnApiClient
|
||||||
|
get() = apiClient
|
||||||
|
|
||||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
private lateinit var swipeRefresh: SwipeRefreshLayout
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: RecyclerView
|
||||||
private lateinit var emptyStateText: TextView
|
private lateinit var emptyStateText: TextView
|
||||||
@@ -413,7 +422,6 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
val themeChanged = bundle.getBoolean(SettingsDialogFragment.RESULT_KEY_THEME_CHANGED)
|
val themeChanged = bundle.getBoolean(SettingsDialogFragment.RESULT_KEY_THEME_CHANGED)
|
||||||
viewModel.onSettingsApplied(
|
viewModel.onSettingsApplied(
|
||||||
credentialsChanged = credentialsChanged,
|
credentialsChanged = credentialsChanged,
|
||||||
themeChanged = themeChanged,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,11 +431,7 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
SettingsDialogFragment
|
SettingsDialogFragment
|
||||||
.newInstance(
|
.newInstance()
|
||||||
sessionStore = sessionStore,
|
|
||||||
apiClient = apiClient,
|
|
||||||
apiKeyStore = apiKeyStore,
|
|
||||||
)
|
|
||||||
.show(supportFragmentManager, SettingsDialogFragment.TAG)
|
.show(supportFragmentManager, SettingsDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class BoardsViewModel(
|
|||||||
fetchBoards(initial = false, refresh = true)
|
fetchBoards(initial = false, refresh = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSettingsApplied(credentialsChanged: Boolean, themeChanged: Boolean) {
|
fun onSettingsApplied(credentialsChanged: Boolean) {
|
||||||
if (!credentialsChanged) {
|
if (!credentialsChanged) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package space.hackenslacker.kanbn4droid.app.settings
|
package space.hackenslacker.kanbn4droid.app.settings
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
@@ -13,29 +15,36 @@ import androidx.fragment.app.commitNow
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import space.hackenslacker.kanbn4droid.app.BoardsActivity
|
||||||
import space.hackenslacker.kanbn4droid.app.MainActivity
|
import space.hackenslacker.kanbn4droid.app.MainActivity
|
||||||
import space.hackenslacker.kanbn4droid.app.R
|
import space.hackenslacker.kanbn4droid.app.R
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||||
|
import space.hackenslacker.kanbn4droid.app.auth.HttpKanbnApiClient
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
||||||
|
import space.hackenslacker.kanbn4droid.app.auth.PreferencesApiKeyStore
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyCoordinator
|
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyCoordinator
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyResult
|
import space.hackenslacker.kanbn4droid.app.auth.SettingsApplyResult
|
||||||
|
import space.hackenslacker.kanbn4droid.app.auth.SessionPreferences
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||||
|
|
||||||
class SettingsDialogFragment(
|
class SettingsDialogFragment : DialogFragment() {
|
||||||
internal var sessionStore: SessionStore? = null,
|
lateinit var sessionStore: SessionStore
|
||||||
internal var apiClient: KanbnApiClient? = null,
|
private set
|
||||||
internal var apiKeyStore: ApiKeyStore? = null,
|
|
||||||
internal var coordinator: SettingsApplyCoordinator? = null,
|
private lateinit var apiClient: KanbnApiClient
|
||||||
) : DialogFragment() {
|
private lateinit var apiKeyStore: ApiKeyStore
|
||||||
|
|
||||||
private var progress: ProgressBar? = null
|
private var progress: ProgressBar? = null
|
||||||
private var saveButton: Button? = null
|
private var saveButton: Button? = null
|
||||||
private var errorText: TextView? = null
|
private var errorText: TextView? = null
|
||||||
private var controlsEnabled: Boolean = true
|
private var controlsEnabled: Boolean = true
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
resolveDependencies(context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val resolvedSessionStore = requireNotNull(sessionStore) { "SessionStore is required" }
|
|
||||||
val resolvedApiClient = requireNotNull(apiClient) { "KanbnApiClient is required" }
|
|
||||||
val resolvedApiKeyStore = requireNotNull(apiKeyStore) { "ApiKeyStore is required" }
|
|
||||||
val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_settings, null)
|
val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_settings, null)
|
||||||
|
|
||||||
childFragmentManager.commitNow {
|
childFragmentManager.commitNow {
|
||||||
@@ -61,25 +70,21 @@ class SettingsDialogFragment(
|
|||||||
|
|
||||||
dialog.setOnShowListener {
|
dialog.setOnShowListener {
|
||||||
saveButton?.setOnClickListener {
|
saveButton?.setOnClickListener {
|
||||||
onSaveClicked(resolvedSessionStore, resolvedApiClient, resolvedApiKeyStore)
|
onSaveClicked()
|
||||||
}
|
}
|
||||||
setApplyInProgress(false)
|
setApplyInProgress(false)
|
||||||
}
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSaveClicked(
|
private fun onSaveClicked() {
|
||||||
sessionStore: SessionStore,
|
|
||||||
apiClient: KanbnApiClient,
|
|
||||||
apiKeyStore: ApiKeyStore,
|
|
||||||
) {
|
|
||||||
if (!controlsEnabled) {
|
if (!controlsEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val resolvedCoordinator = coordinator
|
val hostActivity = requireActivity() as AppCompatActivity
|
||||||
?: MainActivity.dependencies.settingsApplyCoordinatorFactory?.invoke(
|
val resolvedCoordinator = MainActivity.dependencies.settingsApplyCoordinatorFactory?.invoke(
|
||||||
requireActivity() as androidx.appcompat.app.AppCompatActivity,
|
hostActivity,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
apiClient,
|
apiClient,
|
||||||
apiKeyStore,
|
apiKeyStore,
|
||||||
@@ -177,6 +182,31 @@ class SettingsDialogFragment(
|
|||||||
AppCompatDelegate.setDefaultNightMode(mode)
|
AppCompatDelegate.setDefaultNightMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveDependencies(context: Context) {
|
||||||
|
val host = activity as? BoardsActivity
|
||||||
|
if (host != null) {
|
||||||
|
sessionStore = host.sessionStoreForSettingsDialog
|
||||||
|
apiClient = host.apiClientForSettingsDialog
|
||||||
|
apiKeyStore = host.apiKeyStoreForSettingsDialog
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val appCompatActivity = activity as? AppCompatActivity
|
||||||
|
if (appCompatActivity != null) {
|
||||||
|
sessionStore = MainActivity.dependencies.sessionStoreFactory?.invoke(appCompatActivity)
|
||||||
|
?: SessionPreferences(appCompatActivity.applicationContext)
|
||||||
|
apiClient = MainActivity.dependencies.apiClientFactory?.invoke()
|
||||||
|
?: HttpKanbnApiClient()
|
||||||
|
apiKeyStore = MainActivity.dependencies.apiKeyStoreFactory?.invoke(appCompatActivity)
|
||||||
|
?: PreferencesApiKeyStore(appCompatActivity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStore = SessionPreferences(context.applicationContext)
|
||||||
|
apiClient = HttpKanbnApiClient()
|
||||||
|
apiKeyStore = PreferencesApiKeyStore(context)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG: String = "settings_dialog"
|
const val TAG: String = "settings_dialog"
|
||||||
const val REQUEST_KEY_SETTINGS_APPLIED: String = "settings_applied_result"
|
const val REQUEST_KEY_SETTINGS_APPLIED: String = "settings_applied_result"
|
||||||
@@ -185,18 +215,8 @@ class SettingsDialogFragment(
|
|||||||
|
|
||||||
private const val SETTINGS_PREFS_TAG: String = "settings_preferences_fragment"
|
private const val SETTINGS_PREFS_TAG: String = "settings_preferences_fragment"
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(): SettingsDialogFragment {
|
||||||
sessionStore: SessionStore,
|
return SettingsDialogFragment()
|
||||||
apiClient: KanbnApiClient,
|
|
||||||
apiKeyStore: ApiKeyStore,
|
|
||||||
coordinator: SettingsApplyCoordinator? = null,
|
|
||||||
): SettingsDialogFragment {
|
|
||||||
return SettingsDialogFragment(
|
|
||||||
sessionStore = sessionStore,
|
|
||||||
apiClient = apiClient,
|
|
||||||
apiKeyStore = apiKeyStore,
|
|
||||||
coordinator = coordinator,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ class BoardsViewModelTest {
|
|||||||
}
|
}
|
||||||
val viewModel = newViewModel(api)
|
val viewModel = newViewModel(api)
|
||||||
|
|
||||||
viewModel.onSettingsApplied(credentialsChanged = true, themeChanged = false)
|
viewModel.onSettingsApplied(credentialsChanged = true)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertTrue(api.listBoardsCalls >= 1)
|
assertTrue(api.listBoardsCalls >= 1)
|
||||||
@@ -449,7 +449,7 @@ class BoardsViewModelTest {
|
|||||||
}
|
}
|
||||||
val viewModel = newViewModel(api)
|
val viewModel = newViewModel(api)
|
||||||
|
|
||||||
viewModel.onSettingsApplied(credentialsChanged = false, themeChanged = true)
|
viewModel.onSettingsApplied(credentialsChanged = false)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals(0, api.listBoardsCalls)
|
assertEquals(0, api.listBoardsCalls)
|
||||||
@@ -468,7 +468,7 @@ class BoardsViewModelTest {
|
|||||||
val viewModel = newViewModel(api, sessionStore = sessionStore)
|
val viewModel = newViewModel(api, sessionStore = sessionStore)
|
||||||
|
|
||||||
val eventDeferred = async { viewModel.events.first() }
|
val eventDeferred = async { viewModel.events.first() }
|
||||||
viewModel.onSettingsApplied(credentialsChanged = true, themeChanged = false)
|
viewModel.onSettingsApplied(credentialsChanged = true)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
val event = eventDeferred.await()
|
val event = eventDeferred.await()
|
||||||
|
|||||||
Reference in New Issue
Block a user