diff --git a/app/build.gradle b/app/build.gradle index afafd95b3..49a665ce7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,6 +248,7 @@ dependencies { implementation Deps.mozilla_concept_engine implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_toolbar + implementation Deps.mozilla_concept_sync implementation Deps.mozilla_browser_awesomebar implementation Deps.mozilla_feature_downloads @@ -266,6 +267,7 @@ dependencies { implementation Deps.mozilla_feature_intent implementation Deps.mozilla_feature_prompts implementation Deps.mozilla_feature_session + implementation Deps.mozilla_feature_sync implementation Deps.mozilla_feature_toolbar implementation Deps.mozilla_feature_tabs implementation Deps.mozilla_feature_findinpage diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index 6858d811a..0ce82129a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -5,6 +5,12 @@ package org.mozilla.fenix.components import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.feature.sync.BackgroundSyncManager +import mozilla.components.feature.sync.GlobalSyncableStoreProvider import mozilla.components.service.fxa.Config import mozilla.components.service.fxa.FxaAccountManager @@ -13,7 +19,8 @@ import mozilla.components.service.fxa.FxaAccountManager * background worker. */ class BackgroundServices( - context: Context + context: Context, + historyStorage: PlacesHistoryStorage ) { companion object { const val CLIENT_ID = "a2270f727f45f648" @@ -28,5 +35,16 @@ class BackgroundServices( private val scopes: Array = arrayOf("profile", "https://identity.mozilla.com/apps/oldsync") private val config = Config.release(CLIENT_ID, REDIRECT_URL) - val accountManager = FxaAccountManager(context, config, scopes).also { it.initAsync() } + init { + // Make the "history" store accessible to workers spawned by the sync manager. + GlobalSyncableStoreProvider.configureStore("history" to historyStorage) + } + + val syncManager = BackgroundSyncManager("https://identity.mozilla.com/apps/oldsync").also { + it.addStore("history") + } + + val accountManager = FxaAccountManager(context, config, scopes, syncManager).also { + CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } + } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 7d474d096..7eb71a345 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -10,7 +10,7 @@ import android.content.Context * Provides access to all components. */ class Components(private val context: Context) { - val backgroundServices by lazy { BackgroundServices(context) } + val backgroundServices by lazy { BackgroundServices(context, core.historyStorage) } val services by lazy { Services(backgroundServices.accountManager, useCases.tabsUseCases) } val core by lazy { Core(context) } val search by lazy { Search(context) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/AccountSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/AccountSettingsFragment.kt index e953199fa..fff0f4674 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/AccountSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/AccountSettingsFragment.kt @@ -4,7 +4,9 @@ package org.mozilla.fenix.settings +import android.content.Context import android.os.Bundle +import android.text.format.DateUtils import androidx.appcompat.app.AppCompatActivity import androidx.navigation.Navigation import androidx.preference.Preference @@ -13,9 +15,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import mozilla.components.concept.sync.SyncStatusObserver +import mozilla.components.feature.sync.getLastSynced import org.mozilla.fenix.R import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.requireComponents +import java.lang.Exception import kotlin.coroutines.CoroutineContext class AccountSettingsFragment : PreferenceFragmentCompat(), CoroutineScope { @@ -38,9 +43,28 @@ class AccountSettingsFragment : PreferenceFragmentCompat(), CoroutineScope { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.account_settings_preferences, rootKey) - val signIn = context?.getPreferenceKey(R.string.pref_key_sign_out) - val preferenceSignOut = findPreference(signIn) + // Sign out + val signOut = context?.getPreferenceKey(R.string.pref_key_sign_out) + val preferenceSignOut = findPreference(signOut) preferenceSignOut.onPreferenceClickListener = getClickListenerForSignOut() + + // Sync now + val syncNow = context?.getPreferenceKey(R.string.pref_key_sync_now) + val preferenceSyncNow = findPreference(syncNow) + preferenceSyncNow.onPreferenceClickListener = getClickListenerForSyncNow() + + // Current sync state + updateLastSyncedTimePref(context!!, preferenceSyncNow) + if (requireComponents.backgroundServices.syncManager.isSyncRunning()) { + preferenceSyncNow.title = getString(R.string.sync_syncing) + preferenceSyncNow.isEnabled = false + } else { + preferenceSyncNow.isEnabled = true + } + + // NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and + // 'owner' when appropriate. + requireComponents.backgroundServices.syncManager.register(syncStatusObserver, owner = this, autoPause = true) } private fun getClickListenerForSignOut(): Preference.OnPreferenceClickListener { @@ -52,4 +76,66 @@ class AccountSettingsFragment : PreferenceFragmentCompat(), CoroutineScope { true } } + + private fun getClickListenerForSyncNow(): Preference.OnPreferenceClickListener { + return Preference.OnPreferenceClickListener { + requireComponents.backgroundServices.syncManager.syncNow() + true + } + } + + private val syncStatusObserver = object : SyncStatusObserver { + override fun onStarted() { + CoroutineScope(Dispatchers.Main).launch { + val pref = findPreference(context?.getPreferenceKey(R.string.pref_key_sync_now)) + + pref.title = getString(R.string.sync_syncing) + pref.isEnabled = false + } + } + + // Sync stopped successfully. + override fun onIdle() { + CoroutineScope(Dispatchers.Main).launch { + val pref = findPreference(context?.getPreferenceKey(R.string.pref_key_sync_now)) + pref.title = getString(R.string.preferences_sync_now) + pref.isEnabled = true + updateLastSyncedTimePref(context!!, pref, failed = false) + } + } + + // Sync stopped after encountering a problem. + override fun onError(error: Exception?) { + CoroutineScope(Dispatchers.Main).launch { + val pref = findPreference(context?.getPreferenceKey(R.string.pref_key_sync_now)) + pref.title = getString(R.string.preferences_sync_now) + pref.isEnabled = true + updateLastSyncedTimePref(context!!, pref, failed = 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.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( + R.string.sync_last_synced_summary, + DateUtils.getRelativeTimeSpanString(lastSyncTime) + ) + } else { + // Failed to sync, succeeded before. + getString( + R.string.sync_failed_summary, + DateUtils.getRelativeTimeSpanString(lastSyncTime) + ) + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cab1abaaa..d07a962e8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -129,6 +129,16 @@ History Sign out + + Syncing + + Sync failed. Last success: %s + + Sync failed. Last synced: never + + Last synced: %s + + Last synced: never diff --git a/app/src/main/res/xml/account_settings_preferences.xml b/app/src/main/res/xml/account_settings_preferences.xml index 40b207780..02a87c14e 100644 --- a/app/src/main/res/xml/account_settings_preferences.xml +++ b/app/src/main/res/xml/account_settings_preferences.xml @@ -6,8 +6,7 @@ + android:title="@string/preferences_sync_now" /> diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 61b71ebfb..81c7d74ae 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -64,6 +64,7 @@ object Deps { const val mozilla_concept_tabstray = "org.mozilla.components:concept-tabstray:${Versions.mozilla_android_components}" const val mozilla_concept_toolbar = "org.mozilla.components:concept-toolbar:${Versions.mozilla_android_components}" const val mozilla_concept_storage = "org.mozilla.components:concept-storage:${Versions.mozilla_android_components}" + const val mozilla_concept_sync = "org.mozilla.components:concept-sync:${Versions.mozilla_android_components}" const val mozilla_browser_awesomebar = "org.mozilla.components:browser-awesomebar:${Versions.mozilla_android_components}" const val mozilla_browser_engine_gecko_nightly = "org.mozilla.components:browser-engine-gecko-nightly:${Versions.mozilla_android_components}"