mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/4125: Migrate Sign in to Sync to Libstate
This commit is contained in:
parent
059ee1671d
commit
8391e933be
@ -2,26 +2,23 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings
|
||||
package org.mozilla.fenix.settings.account
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.text.format.DateUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.forEach
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.ConstellationState
|
||||
import mozilla.components.concept.sync.DeviceConstellationObserver
|
||||
import mozilla.components.lib.state.ext.observe
|
||||
import mozilla.components.service.fxa.FxaException
|
||||
import mozilla.components.service.fxa.FxaPanicException
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
@ -30,13 +27,16 @@ import mozilla.components.service.fxa.sync.getLastSynced
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.getPreferenceKey
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
private lateinit var accountManager: FxaAccountManager
|
||||
private lateinit var accountSettingsStore: AccountSettingsStore
|
||||
private lateinit var accountSettingsInteractor: AccountSettingsInteractor
|
||||
|
||||
// Navigate away from this fragment when we encounter auth problems or logout events.
|
||||
private val accountStateObserver = object : AccountObserver {
|
||||
@ -78,9 +78,37 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.account_settings_preferences, rootKey)
|
||||
|
||||
accountSettingsStore = StoreProvider.get(this) {
|
||||
AccountSettingsStore(
|
||||
AccountSettingsState(
|
||||
lastSyncedDate =
|
||||
if (getLastSynced(requireContext()) == 0L)
|
||||
LastSyncTime.Never
|
||||
else
|
||||
LastSyncTime.Success(getLastSynced(requireContext())),
|
||||
deviceName = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
accountSettingsStore.observe(this) {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
updateLastSyncTimePref(it)
|
||||
updateDeviceName(it)
|
||||
}
|
||||
}
|
||||
|
||||
accountManager = requireComponents.backgroundServices.accountManager
|
||||
accountManager.register(accountStateObserver, this, true)
|
||||
|
||||
accountSettingsInteractor = AccountSettingsInteractor(
|
||||
findNavController(),
|
||||
::onSyncNow,
|
||||
::makeSnackbar,
|
||||
::syncDeviceName,
|
||||
accountSettingsStore
|
||||
)
|
||||
|
||||
// Sign out
|
||||
val signOut = context!!.getPreferenceKey(R.string.pref_key_sign_out)
|
||||
val preferenceSignOut = findPreference<Preference>(signOut)
|
||||
@ -91,7 +119,6 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
val preferenceSyncNow = findPreference<Preference>(syncNow)
|
||||
preferenceSyncNow?.let {
|
||||
it.onPreferenceClickListener = getClickListenerForSyncNow()
|
||||
updateLastSyncedTimePref(context!!, it)
|
||||
|
||||
// Current sync state
|
||||
if (requireComponents.backgroundServices.accountManager.isSyncActive()) {
|
||||
@ -110,6 +137,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
deviceConstellation?.state()?.currentDevice?.let { device ->
|
||||
summary = device.displayName
|
||||
text = device.displayName
|
||||
accountSettingsStore.dispatch(AccountSettingsAction.UpdateDeviceName(device.displayName))
|
||||
}
|
||||
setOnBindEditTextListener { editText ->
|
||||
editText.filters = arrayOf(InputFilter.LengthFilter(DEVICE_NAME_MAX_LENGTH))
|
||||
@ -125,60 +153,63 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSyncNow() {
|
||||
lifecycleScope.launch {
|
||||
requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow)
|
||||
// Trigger a sync.
|
||||
requireComponents.backgroundServices.accountManager.syncNowAsync().await()
|
||||
// Poll for device events.
|
||||
accountManager.authenticatedAccount()
|
||||
?.deviceConstellation()
|
||||
?.refreshDeviceStateAsync()
|
||||
?.await()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeSnackbar(newValue: String): Boolean {
|
||||
// The network request requires a nonempty string, so don't persist any changes if the user inputs one.
|
||||
if (newValue.trim().isEmpty()) {
|
||||
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_LONG)
|
||||
.setText(getString(R.string.empty_device_name_error))
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun syncDeviceName(newValue: String) {
|
||||
// This may fail, and we'll have a disparity in the UI until `updateDeviceName` is called.
|
||||
lifecycleScope.launch(IO) {
|
||||
try {
|
||||
accountManager.authenticatedAccount()
|
||||
?.deviceConstellation()
|
||||
?.setDeviceNameAsync(newValue)
|
||||
?.await()
|
||||
} catch (e: FxaPanicException) {
|
||||
throw e
|
||||
} catch (e: FxaException) {
|
||||
Logger.error("Setting device name failed.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClickListenerForSignOut(): Preference.OnPreferenceClickListener {
|
||||
return Preference.OnPreferenceClickListener {
|
||||
nav(
|
||||
R.id.accountSettingsFragment,
|
||||
AccountSettingsFragmentDirections.actionAccountSettingsFragmentToSignOutFragment()
|
||||
)
|
||||
accountSettingsInteractor.onSignOut()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClickListenerForSyncNow(): Preference.OnPreferenceClickListener {
|
||||
return Preference.OnPreferenceClickListener {
|
||||
lifecycleScope.launch {
|
||||
requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow)
|
||||
// Trigger a sync.
|
||||
requireComponents.backgroundServices.accountManager.syncNowAsync().await()
|
||||
// Poll for device events.
|
||||
accountManager.authenticatedAccount()
|
||||
?.deviceConstellation()
|
||||
?.refreshDeviceStateAsync()
|
||||
?.await()
|
||||
}
|
||||
accountSettingsInteractor.onSyncNow()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChangeListenerForDeviceName(): Preference.OnPreferenceChangeListener {
|
||||
return Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
// The network request requires a nonempty string, so don't persist any changes if the user inputs one.
|
||||
if (newValue.toString().trim().isEmpty()) {
|
||||
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_LONG)
|
||||
.setText(getString(R.string.empty_device_name_error))
|
||||
.show()
|
||||
return@OnPreferenceChangeListener false
|
||||
}
|
||||
// Optimistically set the device name to what user requested.
|
||||
val deviceNameKey = context!!.getPreferenceKey(R.string.pref_key_sync_device_name)
|
||||
val preferenceDeviceName = findPreference<Preference>(deviceNameKey)
|
||||
preferenceDeviceName?.summary = newValue as String
|
||||
|
||||
// This may fail, and we'll have a disparity in the UI until `updateDeviceName` is called.
|
||||
lifecycleScope.launch(IO) {
|
||||
try {
|
||||
accountManager.authenticatedAccount()
|
||||
?.deviceConstellation()
|
||||
?.setDeviceNameAsync(newValue)
|
||||
?.await()
|
||||
} catch (e: FxaPanicException) {
|
||||
throw e
|
||||
} catch (e: FxaException) {
|
||||
Logger.error("Setting device name failed.", e)
|
||||
}
|
||||
}
|
||||
true
|
||||
accountSettingsInteractor.onChangeDeviceName(newValue as String)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,8 +220,6 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
view?.announceForAccessibility(getString(R.string.sync_syncing_in_progress))
|
||||
pref?.title = getString(R.string.sync_syncing_in_progress)
|
||||
pref?.isEnabled = false
|
||||
|
||||
updateSyncingItemsPreference()
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,7 +230,9 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
pref?.let {
|
||||
pref.title = getString(R.string.preferences_sync_now)
|
||||
pref.isEnabled = true
|
||||
updateLastSyncedTimePref(context!!, pref, failed = false)
|
||||
|
||||
val time = getLastSynced(requireContext())
|
||||
accountSettingsStore.dispatch(AccountSettingsAction.SyncEnded(time))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,7 +244,9 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
pref?.let {
|
||||
pref.title = getString(R.string.preferences_sync_now)
|
||||
pref.isEnabled = true
|
||||
updateLastSyncedTimePref(context!!, pref, failed = true)
|
||||
|
||||
val failedTime = getLastSynced(requireContext())
|
||||
accountSettingsStore.dispatch(AccountSettingsAction.SyncFailed(failedTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,48 +254,39 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val deviceConstellationObserver = object : DeviceConstellationObserver {
|
||||
override fun onDevicesUpdate(constellation: ConstellationState) {
|
||||
val deviceNameKey = context!!.getPreferenceKey(R.string.pref_key_sync_device_name)
|
||||
val preferenceDeviceName = findPreference<Preference>(deviceNameKey)
|
||||
preferenceDeviceName?.summary = constellation.currentDevice?.displayName
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSyncingItemsPreference() {
|
||||
val syncCategory = context!!.getPreferenceKey(R.string.preferences_sync_category)
|
||||
val preferencesSyncCategory = findPreference<Preference>(syncCategory) as PreferenceCategory
|
||||
val stringSet = mutableSetOf<String>()
|
||||
|
||||
preferencesSyncCategory.forEach {
|
||||
(it as? CheckBoxPreference)?.let { checkboxPreference ->
|
||||
if (checkboxPreference.isChecked) {
|
||||
stringSet.add(checkboxPreference.key)
|
||||
}
|
||||
constellation.currentDevice?.displayName?.also {
|
||||
accountSettingsStore.dispatch(AccountSettingsAction.UpdateDeviceName(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLastSyncedTimePref(context: Context, pref: Preference, failed: Boolean = false) {
|
||||
val lastSyncTime = getLastSynced(context)
|
||||
private fun updateDeviceName(state: AccountSettingsState) {
|
||||
val deviceNameKey = context!!.getPreferenceKey(R.string.pref_key_sync_device_name)
|
||||
val preferenceDeviceName = findPreference<Preference>(deviceNameKey)
|
||||
preferenceDeviceName?.summary = state.deviceName
|
||||
}
|
||||
|
||||
pref.summary = if (!failed && lastSyncTime == 0L) {
|
||||
// Never tried to sync.
|
||||
getString(R.string.sync_never_synced_summary)
|
||||
} else if (failed && lastSyncTime == 0L) {
|
||||
// Failed to sync, never succeeded before.
|
||||
getString(R.string.sync_failed_never_synced_summary)
|
||||
} else if (!failed && lastSyncTime != 0L) {
|
||||
// Successfully synced.
|
||||
getString(
|
||||
private fun updateLastSyncTimePref(state: AccountSettingsState) {
|
||||
val value = when (state.lastSyncedDate) {
|
||||
LastSyncTime.Never -> getString(R.string.sync_never_synced_summary)
|
||||
is LastSyncTime.Failed -> {
|
||||
if (state.lastSyncedDate.lastSync == 0L) {
|
||||
getString(R.string.sync_failed_never_synced_summary)
|
||||
} else {
|
||||
getString(
|
||||
R.string.sync_failed_summary,
|
||||
DateUtils.getRelativeTimeSpanString(state.lastSyncedDate.lastSync)
|
||||
)
|
||||
}
|
||||
}
|
||||
is LastSyncTime.Success -> getString(
|
||||
R.string.sync_last_synced_summary,
|
||||
DateUtils.getRelativeTimeSpanString(lastSyncTime)
|
||||
)
|
||||
} else {
|
||||
// Failed to sync, succeeded before.
|
||||
getString(
|
||||
R.string.sync_failed_summary,
|
||||
DateUtils.getRelativeTimeSpanString(lastSyncTime)
|
||||
DateUtils.getRelativeTimeSpanString(state.lastSyncedDate.lastSync)
|
||||
)
|
||||
}
|
||||
|
||||
val syncNow = context!!.getPreferenceKey(R.string.pref_key_sync_now)
|
||||
findPreference<Preference>(syncNow)?.summary = value
|
||||
}
|
||||
|
||||
companion object {
|
@ -0,0 +1,62 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.account
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.nav
|
||||
|
||||
interface AccountSettingsUserActions {
|
||||
|
||||
/**
|
||||
* Called whenever the "Sync now" button is tapped
|
||||
*/
|
||||
fun onSyncNow()
|
||||
|
||||
/**
|
||||
* Called whenever user sets a new device name
|
||||
* @param newDeviceName the device name to change to
|
||||
* @return Boolean indicating whether the new device name has been accepted or not
|
||||
*/
|
||||
fun onChangeDeviceName(newDeviceName: String): Boolean
|
||||
|
||||
/**
|
||||
* Called whenever the "Sign out" button is tapped
|
||||
*/
|
||||
fun onSignOut()
|
||||
}
|
||||
|
||||
class AccountSettingsInteractor(
|
||||
private val navController: NavController,
|
||||
private val syncNow: () -> Unit,
|
||||
private val checkValidName: (String) -> Boolean,
|
||||
private val setDeviceName: (String) -> Unit,
|
||||
private val store: AccountSettingsStore
|
||||
) : AccountSettingsUserActions {
|
||||
|
||||
override fun onSyncNow() {
|
||||
syncNow.invoke()
|
||||
}
|
||||
|
||||
override fun onChangeDeviceName(newDeviceName: String): Boolean {
|
||||
val isValidName = checkValidName.invoke(newDeviceName)
|
||||
if (!isValidName) {
|
||||
return false
|
||||
}
|
||||
// Optimistically set the device name to what user requested.
|
||||
store.dispatch(AccountSettingsAction.UpdateDeviceName(newDeviceName))
|
||||
|
||||
setDeviceName.invoke(newDeviceName)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSignOut() {
|
||||
val directions = AccountSettingsFragmentDirections.actionAccountSettingsFragmentToSignOutFragment()
|
||||
navController.nav(
|
||||
R.id.accountSettingsFragment,
|
||||
directions
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings.account
|
||||
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [AccountSettingsState] and applying [AccountAction]s.
|
||||
*/
|
||||
class AccountSettingsStore(
|
||||
initialState: AccountSettingsState
|
||||
) : Store<AccountSettingsState, AccountSettingsAction>(
|
||||
initialState,
|
||||
::accountStateReducer
|
||||
)
|
||||
|
||||
sealed class LastSyncTime {
|
||||
object Never : LastSyncTime()
|
||||
data class Failed(val lastSync: Long) : LastSyncTime()
|
||||
data class Success(val lastSync: Long) : LastSyncTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Account Settings Screen
|
||||
*/
|
||||
data class AccountSettingsState(
|
||||
val lastSyncedDate: LastSyncTime,
|
||||
val deviceName: String
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `SearchStore` to modify `SearchState` through the reducer.
|
||||
*/
|
||||
sealed class AccountSettingsAction : Action {
|
||||
data class SyncFailed(val time: Long) : AccountSettingsAction()
|
||||
data class SyncEnded(val time: Long) : AccountSettingsAction()
|
||||
data class UpdateDeviceName(val name: String) : AccountSettingsAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The SearchState Reducer.
|
||||
*/
|
||||
fun accountStateReducer(state: AccountSettingsState, action: AccountSettingsAction): AccountSettingsState {
|
||||
return when (action) {
|
||||
is AccountSettingsAction.SyncFailed -> state.copy(lastSyncedDate = LastSyncTime.Failed(action.time))
|
||||
is AccountSettingsAction.SyncEnded -> state.copy(lastSyncedDate = LastSyncTime.Success(action.time))
|
||||
is AccountSettingsAction.UpdateDeviceName -> state.copy(deviceName = action.name)
|
||||
}
|
||||
}
|
@ -329,7 +329,7 @@
|
||||
android:label="@string/preferences_accessibility" />
|
||||
<fragment
|
||||
android:id="@+id/accountSettingsFragment"
|
||||
android:name="org.mozilla.fenix.settings.AccountSettingsFragment"
|
||||
android:name="org.mozilla.fenix.settings.account.AccountSettingsFragment"
|
||||
android:label="@string/preferences_account_settings">
|
||||
<action
|
||||
android:id="@+id/action_accountSettingsFragment_to_signOutFragment"
|
||||
|
@ -0,0 +1,75 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.settings
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.settings.account.AccountSettingsAction
|
||||
import org.mozilla.fenix.settings.account.AccountSettingsFragmentDirections
|
||||
import org.mozilla.fenix.settings.account.AccountSettingsInteractor
|
||||
import org.mozilla.fenix.settings.account.AccountSettingsStore
|
||||
|
||||
class AccountSettingsInteractorTest {
|
||||
|
||||
@Test
|
||||
fun onSyncNow() {
|
||||
var ranSyncNow = false
|
||||
|
||||
val interactor = AccountSettingsInteractor(
|
||||
mockk(),
|
||||
{ ranSyncNow = true },
|
||||
mockk(),
|
||||
mockk(),
|
||||
mockk()
|
||||
)
|
||||
|
||||
interactor.onSyncNow()
|
||||
|
||||
assertEquals(ranSyncNow, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onChangeDeviceName() {
|
||||
val store: AccountSettingsStore = mockk(relaxed = true)
|
||||
|
||||
val interactor = AccountSettingsInteractor(
|
||||
mockk(),
|
||||
mockk(),
|
||||
{ true },
|
||||
{},
|
||||
store
|
||||
)
|
||||
|
||||
interactor.onChangeDeviceName("New Name")
|
||||
|
||||
verify { store.dispatch(AccountSettingsAction.UpdateDeviceName("New Name")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSignOut() {
|
||||
val navController: NavController = mockk(relaxed = true)
|
||||
every { navController.currentDestination } returns NavDestination("").apply { id = R.id.accountSettingsFragment }
|
||||
|
||||
val interactor = AccountSettingsInteractor(
|
||||
navController,
|
||||
mockk(),
|
||||
mockk(),
|
||||
mockk(),
|
||||
mockk()
|
||||
)
|
||||
|
||||
interactor.onSignOut()
|
||||
|
||||
verify {
|
||||
navController.navigate(AccountSettingsFragmentDirections.actionAccountSettingsFragmentToSignOutFragment())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user