Revision control

Copy as Markdown

/* 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.reference.browser.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceClickListener
import androidx.preference.PreferenceFragmentCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.service.fxa.sync.SyncStatusObserver
import mozilla.components.service.fxa.sync.getLastSynced
import org.mozilla.reference.browser.IntentReceiverActivity
import org.mozilla.reference.browser.R
import org.mozilla.reference.browser.R.string.pref_key_sign_out
import org.mozilla.reference.browser.R.string.pref_key_sync_history
import org.mozilla.reference.browser.R.string.pref_key_sync_manage_account
import org.mozilla.reference.browser.R.string.pref_key_sync_now
import org.mozilla.reference.browser.R.string.pref_key_sync_passwords
import org.mozilla.reference.browser.R.string.pref_key_sync_tabs
import org.mozilla.reference.browser.components.BackgroundServices.Companion.SUPPORTED_SYNC_ENGINES
import org.mozilla.reference.browser.ext.getPreferenceKey
import org.mozilla.reference.browser.ext.requireComponents
import org.mozilla.reference.browser.sync.BrowserFxAEntryPoint
class AccountSettingsFragment : PreferenceFragmentCompat() {
private val syncStatusObserver = object : SyncStatusObserver {
override fun onStarted() {
CoroutineScope(Dispatchers.Main).launch {
val pref = findPreference<Preference>(requireContext().getPreferenceKey(pref_key_sync_now))
pref?.title = getString(R.string.syncing)
pref?.isEnabled = false
}
}
// Sync stopped successfully.
override fun onIdle() {
CoroutineScope(Dispatchers.Main).launch {
val pref = findPreference<Preference>(requireContext().getPreferenceKey(pref_key_sync_now))
pref?.title = getString(R.string.sync_now)
pref?.isEnabled = true
updateLastSyncedTimePref(context!!, pref, failed = false)
updateSyncEngineStates()
}
}
// Sync stopped after encountering a problem.
override fun onError(error: Exception?) {
CoroutineScope(Dispatchers.Main).launch {
val pref = findPreference<Preference>(requireContext().getPreferenceKey(pref_key_sync_now))
pref?.title = getString(R.string.sync_now)
pref?.isEnabled = true
updateLastSyncedTimePref(context!!, pref, failed = true)
}
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.account_preferences, rootKey)
val signOutKey = requireContext().getPreferenceKey(pref_key_sign_out)
val syncNowKey = requireContext().getPreferenceKey(pref_key_sync_now)
val manageAccountKey = requireContext().getPreferenceKey(pref_key_sync_manage_account)
// Sign Out
val preferenceSignOut = findPreference<CustomColorPreference>(signOutKey)
preferenceSignOut?.onPreferenceClickListener = getClickListenerForSignOut()
// Sync Now
val preferenceSyncNow = findPreference<Preference>(syncNowKey)
updateLastSyncedTimePref(requireContext(), preferenceSyncNow)
preferenceSyncNow?.onPreferenceClickListener = getClickListenerForSyncNow()
// Manage Account
val preferenceManageAccount = findPreference<Preference>(manageAccountKey)
preferenceManageAccount?.onPreferenceClickListener = getClickListenerForManageAccount()
SUPPORTED_SYNC_ENGINES.forEach {
val preferenceKey = requireContext().getPreferenceKey(it.prefId())
(findPreference<CheckBoxPreference>(preferenceKey) as CheckBoxPreference).apply {
setOnPreferenceChangeListener { _, newValue ->
updateSyncEngineState(context, it, newValue as Boolean)
true
}
}
}
updateSyncEngineStates()
// NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and
// 'owner' when appropriate.
requireComponents.backgroundServices.accountManager.registerForSyncEvents(
syncStatusObserver,
owner = this,
autoPause = true,
)
}
fun updateLastSyncedTimePref(context: Context, pref: Preference?, failed: Boolean = false) {
val lastSyncTime = getLastSynced(context)
pref?.summary = if (!failed && lastSyncTime == 0L) {
// Never tried to sync.
getString(R.string.preferences_sync_never_synced_summary)
} else if (failed && lastSyncTime == 0L) {
// Failed to sync, never succeeded before.
getString(R.string.preferences_sync_failed_never_synced_summary)
} else if (!failed && lastSyncTime != 0L) {
// Successfully synced.
getString(
R.string.preferences_sync_last_synced_summary,
DateUtils.getRelativeTimeSpanString(lastSyncTime),
)
} else {
// Failed to sync, succeeded before.
getString(
R.string.preferences_sync_failed_summary,
DateUtils.getRelativeTimeSpanString(lastSyncTime),
)
}
}
private fun getClickListenerForSignOut(): OnPreferenceClickListener {
return OnPreferenceClickListener {
CoroutineScope(Dispatchers.Main).launch {
requireComponents.backgroundServices.accountManager.logout()
activity?.onBackPressedDispatcher?.onBackPressed()
}
true
}
}
private fun getClickListenerForSyncNow(): OnPreferenceClickListener {
return OnPreferenceClickListener {
CoroutineScope(Dispatchers.Main).launch {
// Trigger a sync & update devices.
requireComponents.backgroundServices.accountManager.syncNow(SyncReason.User)
// Poll for device events.
requireComponents.backgroundServices.accountManager.authenticatedAccount()
?.deviceConstellation()?.run {
refreshDevices()
pollForCommands()
}
}
true
}
}
private fun getClickListenerForManageAccount(): OnPreferenceClickListener {
return OnPreferenceClickListener {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
context?.let {
val account =
requireComponents.backgroundServices.accountManager.authenticatedAccount()
val url = account?.getManageAccountURL(BrowserFxAEntryPoint.AccountSettings)
if (url != null) {
val intent = createCustomTabIntent(it, url)
startActivity(intent)
}
}
}
true
}
}
private fun createCustomTabIntent(context: Context, url: String): Intent = CustomTabsIntent.Builder()
.setInstantAppsEnabled(false)
.build()
.intent
.setData(url.toUri())
.setClassName(context, IntentReceiverActivity::class.java.name)
.setPackage(context.packageName)
private fun updateSyncEngineState(context: Context, engine: SyncEngine, newState: Boolean) {
SyncEnginesStorage(context).setStatus(engine, newState)
CoroutineScope(Dispatchers.Main).launch {
requireComponents.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
}
}
private fun updateSyncEngineStates() {
val syncEnginesStatus = SyncEnginesStorage(requireContext()).getStatus()
SUPPORTED_SYNC_ENGINES.forEach { engine ->
val preferenceKey = requireContext().getPreferenceKey(engine.prefId())
(findPreference<CheckBoxPreference>(preferenceKey) as CheckBoxPreference).apply {
isEnabled = syncEnginesStatus.containsKey(engine)
isChecked = syncEnginesStatus.getOrElse(engine) { true }
}
}
}
private fun SyncEngine.prefId(): Int = when (this) {
SyncEngine.History -> pref_key_sync_history
SyncEngine.Passwords -> pref_key_sync_passwords
SyncEngine.Tabs -> pref_key_sync_tabs
else -> throw IllegalStateException("Accessing unsupported sync engines")
}
}