1
0
Fork 0
fenix/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt

425 lines
18 KiB
Kotlin
Raw Normal View History

/* 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 android.app.KeyguardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.text.InputFilter
import android.text.format.DateUtils
2019-08-09 02:14:03 +02:00
import android.view.View
import androidx.appcompat.app.AlertDialog
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
2019-08-09 01:39:23 +02:00
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellationObserver
2019-08-09 01:39:23 +02:00
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.fxa.sync.SyncReason
2019-07-11 11:33:37 +02:00
import mozilla.components.service.fxa.sync.SyncStatusObserver
import mozilla.components.service.fxa.sync.getLastSynced
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToPx
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.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.ext.settings
2019-11-25 21:36:47 +01:00
import org.mozilla.fenix.ext.showToolbar
2020-06-15 20:24:14 +02:00
import org.mozilla.fenix.settings.requirePreference
2019-10-24 18:29:41 +02:00
@SuppressWarnings("TooManyFunctions", "LargeClass")
class AccountSettingsFragment : PreferenceFragmentCompat() {
private lateinit var accountManager: FxaAccountManager
private lateinit var accountSettingsStore: AccountSettingsFragmentStore
private lateinit var accountSettingsInteractor: AccountSettingsInteractor
// Navigate away from this fragment when we encounter auth problems or logout events.
private val accountStateObserver = object : AccountObserver {
override fun onAuthenticationProblems() {
viewLifecycleOwner.lifecycleScope.launch {
findNavController().popBackStack()
}
}
override fun onLoggedOut() {
viewLifecycleOwner.lifecycleScope.launch {
findNavController().popBackStack()
// Remove the device name when we log out.
context?.let {
val deviceNameKey = it.getPreferenceKey(R.string.pref_key_sync_device_name)
preferenceManager.sharedPreferences.edit().remove(deviceNameKey).apply()
}
}
}
}
override fun onResume() {
super.onResume()
2019-11-25 21:36:47 +01:00
showToolbar(getString(R.string.preferences_account_settings))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requireComponents.analytics.metrics.track(Event.SyncAccountOpened)
}
override fun onDestroy() {
super.onDestroy()
requireComponents.analytics.metrics.track(Event.SyncAccountClosed)
}
@ExperimentalCoroutinesApi
2019-08-09 02:14:03 +02:00
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
consumeFrom(accountSettingsStore) {
updateLastSyncTimePref(it)
updateDeviceName(it)
}
accountSettingsInteractor = AccountSettingsInteractor(
findNavController(),
::syncNow,
::syncDeviceName,
accountSettingsStore
)
}
2019-10-24 18:29:41 +02:00
@Suppress("ComplexMethod", "LongMethod")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.account_settings_preferences, rootKey)
accountSettingsStore = StoreProvider.get(this) {
AccountSettingsFragmentStore(
AccountSettingsFragmentState(
lastSyncedDate =
2019-08-09 02:14:03 +02:00
if (getLastSynced(requireContext()) == 0L)
LastSyncTime.Never
else
LastSyncTime.Success(getLastSynced(requireContext())),
2019-10-24 18:29:41 +02:00
deviceName = requireComponents.backgroundServices.defaultDeviceName(
requireContext()
)
)
)
}
accountManager = requireComponents.backgroundServices.accountManager
accountManager.register(accountStateObserver, this, true)
// Sign out
2020-06-15 20:24:14 +02:00
val preferenceSignOut = requirePreference<Preference>(R.string.pref_key_sign_out)
preferenceSignOut.onPreferenceClickListener = getClickListenerForSignOut()
// Sync now
2020-06-15 20:24:14 +02:00
val preferenceSyncNow = requirePreference<Preference>(R.string.pref_key_sync_now)
preferenceSyncNow.apply {
onPreferenceClickListener = getClickListenerForSyncNow()
icon = icon.mutate().apply {
setTint(context.getColorFromAttr(R.attr.primaryText))
}
// Current sync state
if (requireComponents.backgroundServices.accountManager.isSyncActive()) {
2020-06-15 20:24:14 +02:00
title = getString(R.string.sync_syncing_in_progress)
isEnabled = false
} else {
2020-06-15 20:24:14 +02:00
isEnabled = true
}
}
// Device Name
val deviceConstellation = accountManager.authenticatedAccount()?.deviceConstellation()
2020-06-15 20:24:14 +02:00
requirePreference<EditTextPreference>(R.string.pref_key_sync_device_name).apply {
onPreferenceChangeListener = getChangeListenerForDeviceName()
deviceConstellation?.state()?.currentDevice?.let { device ->
summary = device.displayName
text = device.displayName
accountSettingsStore.dispatch(AccountSettingsFragmentAction.UpdateDeviceName(device.displayName))
}
setOnBindEditTextListener { editText ->
editText.filters = arrayOf(InputFilter.LengthFilter(DEVICE_NAME_MAX_LENGTH))
editText.minHeight = DEVICE_NAME_EDIT_TEXT_MIN_HEIGHT_DP.dpToPx(resources.displayMetrics)
}
}
// Make sure out sync engine checkboxes are up-to-date and disabled if currently syncing
updateSyncEngineStates()
setDisabledWhileSyncing(accountManager.isSyncActive())
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_history).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.History, newValue as Boolean)
2019-10-24 18:29:41 +02:00
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
}
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_bookmarks).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Bookmarks, newValue as Boolean)
2019-10-24 18:29:41 +02:00
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
}
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_logins).apply {
2019-10-24 18:29:41 +02:00
setOnPreferenceChangeListener { _, newValue ->
val manager =
activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (manager.isKeyguardSecure ||
newValue == false ||
!context.settings().shouldShowSecurityPinWarningSync
) {
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
} else {
showPinDialogWarning(newValue as Boolean)
}
true
}
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_tabs).apply {
setOnPreferenceChangeListener { _, newValue ->
SyncEnginesStorage(context).setStatus(SyncEngine.Tabs, newValue as Boolean)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
true
}
}
deviceConstellation?.registerDeviceObserver(
deviceConstellationObserver,
owner = this,
autoPause = true
)
// NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and
// 'owner' when appropriate.
2019-07-11 11:33:37 +02:00
requireComponents.backgroundServices.accountManager.registerForSyncEvents(
syncStatusObserver, owner = this, autoPause = true
)
}
private fun showPinDialogWarning(newValue: Boolean) {
context?.let {
AlertDialog.Builder(it).apply {
setTitle(getString(R.string.logins_warning_dialog_title))
setMessage(
getString(R.string.logins_warning_dialog_message)
)
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue)
@Suppress("DeferredResultUnused")
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
}
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
it.dismiss()
val intent = Intent(
Settings.ACTION_SECURITY_SETTINGS
)
startActivity(intent)
}
create()
}.show().secure(activity)
it.settings().incrementShowLoginsSecureWarningSyncCount()
}
}
private fun updateSyncEngineStates() {
val syncEnginesStatus = SyncEnginesStorage(requireContext()).getStatus()
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_bookmarks).apply {
isEnabled = syncEnginesStatus.containsKey(SyncEngine.Bookmarks)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Bookmarks) { true }
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_history).apply {
isEnabled = syncEnginesStatus.containsKey(SyncEngine.History)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.History) { true }
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_logins).apply {
2019-10-24 18:29:41 +02:00
isEnabled = syncEnginesStatus.containsKey(SyncEngine.Passwords)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { true }
2019-10-24 18:29:41 +02:00
}
2020-06-15 20:24:14 +02:00
requirePreference<CheckBoxPreference>(R.string.pref_key_sync_tabs).apply {
isEnabled = syncEnginesStatus.containsKey(SyncEngine.Tabs)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { true }
}
}
2019-08-09 01:39:23 +02:00
private fun syncNow() {
viewLifecycleOwner.lifecycleScope.launch {
requireComponents.analytics.metrics.track(Event.SyncAccountSyncNow)
// Trigger a sync.
2019-10-24 18:29:41 +02:00
requireComponents.backgroundServices.accountManager.syncNowAsync(SyncReason.User)
.await()
2019-08-21 15:38:37 +02:00
// Poll for device events & update devices.
accountManager.authenticatedAccount()
2019-08-21 15:38:37 +02:00
?.deviceConstellation()?.run {
refreshDevicesAsync().await()
pollForCommandsAsync().await()
2019-08-21 15:38:37 +02:00
}
}
}
2019-08-09 02:14:03 +02:00
private fun syncDeviceName(newValue: String): Boolean {
if (newValue.trim().isEmpty()) {
return false
}
// This may fail, and we'll have a disparity in the UI until `updateDeviceName` is called.
viewLifecycleOwner.lifecycleScope.launch(Main) {
context?.let {
accountManager.authenticatedAccount()
?.deviceConstellation()
?.setDeviceNameAsync(newValue, it)
?.await()
}
}
2019-08-09 02:14:03 +02:00
return true
}
private fun getClickListenerForSignOut(): Preference.OnPreferenceClickListener {
return Preference.OnPreferenceClickListener {
accountSettingsInteractor.onSignOut()
true
}
}
private fun getClickListenerForSyncNow(): Preference.OnPreferenceClickListener {
return Preference.OnPreferenceClickListener {
accountSettingsInteractor.onSyncNow()
true
}
}
private fun getChangeListenerForDeviceName(): Preference.OnPreferenceChangeListener {
return Preference.OnPreferenceChangeListener { _, newValue ->
2019-08-09 02:14:03 +02:00
accountSettingsInteractor.onChangeDeviceName(newValue as String) {
FenixSnackbar.make(
view = requireView(),
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = false
)
2019-10-24 18:29:41 +02:00
.setText(getString(R.string.empty_device_name_error))
.show()
2019-08-09 02:14:03 +02:00
}
}
}
private fun setDisabledWhileSyncing(isSyncing: Boolean) {
2020-06-15 20:24:14 +02:00
requirePreference<PreferenceCategory>(R.string.preferences_sync_category).isEnabled = !isSyncing
requirePreference<EditTextPreference>(R.string.pref_key_sync_device_name).isEnabled = !isSyncing
}
private val syncStatusObserver = object : SyncStatusObserver {
2020-06-15 20:24:14 +02:00
private val pref by lazy { requirePreference<Preference>(R.string.pref_key_sync_now) }
override fun onStarted() {
viewLifecycleOwner.lifecycleScope.launch {
view?.announceForAccessibility(getString(R.string.sync_syncing_in_progress))
2020-06-15 20:24:14 +02:00
pref.title = getString(R.string.sync_syncing_in_progress)
pref.isEnabled = false
setDisabledWhileSyncing(true)
}
}
// Sync stopped successfully.
override fun onIdle() {
viewLifecycleOwner.lifecycleScope.launch {
2020-06-15 20:24:14 +02:00
pref.title = getString(R.string.preferences_sync_now)
pref.isEnabled = true
2020-06-15 20:24:14 +02:00
val time = getLastSynced(requireContext())
accountSettingsStore.dispatch(AccountSettingsFragmentAction.SyncEnded(time))
// Make sure out sync engine checkboxes are up-to-date.
updateSyncEngineStates()
setDisabledWhileSyncing(false)
}
}
// Sync stopped after encountering a problem.
override fun onError(error: Exception?) {
viewLifecycleOwner.lifecycleScope.launch {
2020-06-15 20:24:14 +02:00
pref.title = getString(R.string.preferences_sync_now)
// We want to only enable the sync button, and not the checkboxes here
pref.isEnabled = true
val failedTime = getLastSynced(requireContext())
accountSettingsStore.dispatch(
AccountSettingsFragmentAction.SyncFailed(
failedTime
2019-10-24 18:29:41 +02:00
)
2020-06-15 20:24:14 +02:00
)
}
}
}
private val deviceConstellationObserver = object : DeviceConstellationObserver {
override fun onDevicesUpdate(constellation: ConstellationState) {
constellation.currentDevice?.displayName?.also {
accountSettingsStore.dispatch(AccountSettingsFragmentAction.UpdateDeviceName(it))
}
}
}
private fun updateDeviceName(state: AccountSettingsFragmentState) {
2020-06-15 20:24:14 +02:00
val preferenceDeviceName = requirePreference<Preference>(R.string.pref_key_sync_device_name)
preferenceDeviceName.summary = state.deviceName
}
private fun updateLastSyncTimePref(state: AccountSettingsFragmentState) {
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(state.lastSyncedDate.lastSync)
)
}
2020-06-15 20:24:14 +02:00
requirePreference<Preference>(R.string.pref_key_sync_now).summary = value
}
companion object {
private const val DEVICE_NAME_MAX_LENGTH = 128
private const val DEVICE_NAME_EDIT_TEXT_MIN_HEIGHT_DP = 48
}
}