diff --git a/app/build.gradle b/app/build.gradle index 8c799134a..ee41a6678 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -488,6 +488,7 @@ dependencies { implementation Deps.mozilla_feature_qr implementation Deps.mozilla_feature_search implementation Deps.mozilla_feature_session + implementation Deps.mozilla_feature_syncedtabs 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/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index be0689de1..168e5cfca 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -18,6 +18,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromHome(R.id.homeFragment), FromSearch(R.id.searchFragment), FromSettings(R.id.settingsFragment), + FromSyncedTabs(R.id.syncedTabsFragment), FromBookmarks(R.id.bookmarkFragment), FromHistory(R.id.historyFragment), FromExceptions(R.id.exceptionsFragment), diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index ec31eca7e..d6ef089d7 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -44,6 +44,11 @@ object FeatureFlags { */ val loginsEdit = Config.channel.isNightlyOrDebug + /** + * Enable tab sync feature + */ + val syncedTabs = Config.channel.isNightlyOrDebug + /** * Enables new tab tray pref */ diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index b2ff47761..bdee5ac29 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -32,7 +32,9 @@ import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.tabstray.BrowserTabsTray import mozilla.components.concept.engine.EngineView +import mozilla.components.concept.tabstray.TabsTray import mozilla.components.feature.contextmenu.ext.DefaultSelectionActionDelegate import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.support.base.feature.UserInteractionHandler @@ -51,37 +53,36 @@ import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.exceptions.ExceptionsFragmentDirections -import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.checkAndUpdateScreenshotPermission -import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.HomeFragmentDirections -import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor -import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor +import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor +import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.search.SearchFragmentDirections import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections -import org.mozilla.fenix.settings.logins.SavedLoginsAuthFragmentDirections import org.mozilla.fenix.settings.SettingsFragmentDirections import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections import org.mozilla.fenix.settings.about.AboutFragmentDirections +import org.mozilla.fenix.settings.logins.SavedLoginsAuthFragmentDirections import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections +import org.mozilla.fenix.sync.SyncedTabsFragmentDirections +import org.mozilla.fenix.tabtray.FenixTabsAdapter import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.RunWhenReadyQueue -import mozilla.components.concept.tabstray.TabsTray -import mozilla.components.browser.tabstray.BrowserTabsTray -import org.mozilla.fenix.tabtray.FenixTabsAdapter /** * The main activity of the application. The application is primarily a single Activity (this one) @@ -367,6 +368,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { SearchFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromSettings -> SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromSyncedTabs -> + SyncedTabsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromBookmarks -> BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromHistory -> 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 c7d371156..4b8d87bf8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.browser.storage.sync.RemoteTabsStorage import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.DeviceCapability @@ -17,6 +18,7 @@ import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.feature.accounts.push.FxaPushSupportFeature import mozilla.components.feature.accounts.push.SendTabFeature +import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.fxa.DeviceConfig import mozilla.components.service.fxa.ServerConfig @@ -35,6 +37,7 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.sync.SyncedTabsIntegration import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.RunWhenReadyQueue @@ -49,7 +52,8 @@ class BackgroundServices( crashReporter: CrashReporter, historyStorage: Lazy, bookmarkStorage: Lazy, - passwordsStorage: Lazy + passwordsStorage: Lazy, + remoteTabsStorage: Lazy ) { // Allows executing tasks which depend on the account manager, but do not need to eagerly initialize it. val accountManagerAvailableQueue = RunWhenReadyQueue() @@ -83,16 +87,28 @@ class BackgroundServices( val syncConfig = if (FeatureFlags.asFeatureSyncDisabled) { null } else { + + val supportedEngines = if (FeatureFlags.syncedTabs) { + setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs) + } else { + setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords) + } + SyncConfig( - setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords), + supportedEngines, syncPeriodInMinutes = 240L) // four hours } init { - // Make the "history", "bookmark", and "passwords" stores accessible to workers spawned by the sync manager. + /* Make the "history", "bookmark", "passwords", and "tabs" stores accessible to workers + spawned by the sync manager. */ GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage) + + if (FeatureFlags.syncedTabs) { + GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage) + } } private val telemetryAccountObserver = TelemetryAccountObserver( @@ -104,6 +120,10 @@ class BackgroundServices( val accountManager by lazy { makeAccountManager(context, serverConfig, deviceConfig, syncConfig) } + val syncedTabsStorage by lazy { + SyncedTabsStorage(accountManager, context.components.core.store, remoteTabsStorage.value) + } + @VisibleForTesting(otherwise = PRIVATE) fun makeAccountManager( context: Context, @@ -148,6 +168,8 @@ class BackgroundServices( notificationManager.showReceivedTabs(context, device, tabs) } + SyncedTabsIntegration(context, accountManager).launch() + accountAbnormalities.accountManagerInitializedAsync( accountManager, accountManager.initAsync() 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 7468e500b..eef88bfe6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -37,7 +37,8 @@ class Components(private val context: Context) { analytics.crashReporter, core.lazyHistoryStorage, core.lazyBookmarksStorage, - core.lazyPasswordsStorage + core.lazyPasswordsStorage, + core.lazyRemoteTabsStorage ) } val services by lazy { Services(context, backgroundServices.accountManager) } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 73a54dec8..76972c54d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -22,6 +22,7 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.browser.storage.sync.RemoteTabsStorage import mozilla.components.browser.thumbnails.ThumbnailsMiddleware import mozilla.components.browser.thumbnails.storage.ThumbnailStorage import mozilla.components.concept.engine.DefaultSettings @@ -212,6 +213,11 @@ class Core(private val context: Context) { val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) } val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) } + /** + * The storage component to sync and persist tabs in a Firefox Sync account. + */ + val lazyRemoteTabsStorage = lazy { RemoteTabsStorage() } + // For most other application code (non-startup), these wrappers are perfectly fine and more ergonomic. val historyStorage by lazy { lazyHistoryStorage.value } val bookmarksStorage by lazy { lazyBookmarksStorage.value } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt index 3bcc9ead4..e7aa1477b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Metrics.kt @@ -419,7 +419,7 @@ sealed class Event { NEW_PRIVATE_TAB, SHARE, REPORT_SITE_ISSUE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER, - BOOKMARKS, HISTORY + BOOKMARKS, HISTORY, SYNC_TABS } override val extras: Map? diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index d97f0b0c1..6494e63c1 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -158,6 +158,10 @@ class DefaultBrowserToolbarController( val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() navController.nav(R.id.browserFragment, directions) } + ToolbarMenu.Item.SyncedTabs -> { + navController.nav( + R.id.browserFragment, BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment()) + } is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke( item.isChecked, currentSession @@ -372,6 +376,7 @@ class DefaultBrowserToolbarController( ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN + ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT is ToolbarMenu.Item.ReaderMode -> diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index 16f495cbc..2eed25039 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -25,9 +25,10 @@ import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.support.ktx.android.content.getColorFromAttr -import org.mozilla.fenix.Config -import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.Config +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.ReleaseChannel import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.ext.asActivity @@ -190,6 +191,7 @@ class DefaultToolbarMenu( val menuItems = listOfNotNull( historyItem, bookmarksItem, + if (FeatureFlags.syncedTabs) syncedTabs else null, settings, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, BrowserMenuDivider(), @@ -259,6 +261,14 @@ class DefaultToolbarMenu( onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) } + private val syncedTabs = BrowserMenuImageText( + label = context.getString(R.string.synced_tabs), + imageResource = R.drawable.ic_tab_collection, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) + } + private val installToHomescreen = BrowserMenuHighlightableItem( label = context.getString(R.string.browser_menu_install_on_homescreen), startImageResource = R.drawable.ic_add_to_homescreen, diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index 82224b52d..38a7a14d6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -23,6 +23,7 @@ interface ToolbarMenu { object AddToTopSites : Item() object InstallToHomeScreen : Item() object AddToHomeScreen : Item() + object SyncedTabs : Item() object AddonsManager : Item() object Quit : Item() data class ReaderMode(val isChecked: Boolean) : Item() diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index e4b1a5d94..6b446859a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -675,6 +675,14 @@ class HomeFragment : Fragment() { HomeFragmentDirections.actionGlobalSettingsFragment() ) } + HomeMenu.Item.SyncedTabs -> { + invokePendingDeleteJobs() + hideOnboardingIfNeeded() + nav( + R.id.homeFragment, + HomeFragmentDirections.actionGlobalSyncedTabsFragment() + ) + } HomeMenu.Item.Bookmarks -> { invokePendingDeleteJobs() hideOnboardingIfNeeded() diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt index 5bcf30bf6..7b64a44a0 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -21,6 +21,7 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.support.ktx.android.content.getColorFromAttr +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings @@ -39,6 +40,7 @@ class HomeMenu( object WhatsNew : Item() object Help : Item() object Settings : Item() + object SyncedTabs : Item() object History : Item() object Bookmarks : Item() object Quit : Item() @@ -118,6 +120,14 @@ class HomeMenu( onItemTapped.invoke(Item.Settings) } + val syncedTabsItem = BrowserMenuImageText( + context.getString(R.string.library_synced_tabs), + R.drawable.ic_tab_collection, + primaryTextColor + ) { + onItemTapped.invoke(Item.SyncedTabs) + } + val helpItem = BrowserMenuImageText( context.getString(R.string.browser_menu_help), R.drawable.ic_help, @@ -141,6 +151,7 @@ class HomeMenu( BrowserMenuDivider(), bookmarksItem, historyItem, + if (FeatureFlags.syncedTabs) syncedTabsItem else null, BrowserMenuDivider(), settingsItem, helpItem, @@ -157,6 +168,7 @@ class HomeMenu( BrowserMenuDivider(), bookmarksItem, historyItem, + if (FeatureFlags.syncedTabs) syncedTabsItem else null, BrowserMenuDivider(), whatsNewItem ).also { items -> diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt index 6862af391..258756a99 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt @@ -35,6 +35,7 @@ import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncStatusObserver import mozilla.components.service.fxa.sync.getLastSynced import mozilla.components.support.ktx.android.util.dpToPx +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider @@ -206,6 +207,16 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { } } + val tabsNameKey = getPreferenceKey(R.string.pref_key_sync_tabs) + findPreference(tabsNameKey)?.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, @@ -263,6 +274,12 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { isEnabled = syncEnginesStatus.containsKey(SyncEngine.Passwords) isChecked = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { true } } + val tabsNameKey = getPreferenceKey(R.string.pref_key_sync_tabs) + findPreference(tabsNameKey)?.apply { + isVisible = FeatureFlags.syncedTabs + isEnabled = syncEnginesStatus.containsKey(SyncEngine.Tabs) + isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { FeatureFlags.syncedTabs } + } } private fun syncNow() { diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt new file mode 100644 index 000000000..d45c9e8c7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -0,0 +1,60 @@ +/* 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.sync + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import mozilla.components.concept.sync.Device as SyncDevice +import mozilla.components.browser.storage.sync.Tab as SyncTab +import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder +import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder + +class SyncedTabsAdapter( + private val listener: (SyncTab) -> Unit +) : ListAdapter( + DiffCallback +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { + val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + + return when (viewType) { + DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) + TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) + else -> throw IllegalStateException() + } + } + + override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { + val item = when (holder) { + is DeviceViewHolder -> getItem(position) as AdapterItem.Device + is TabViewHolder -> getItem(position) as AdapterItem.Tab + } + holder.bind(item, listener) + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID + is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + } + } + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + areContentsTheSame(oldItem, newItem) + + @Suppress("DiffUtilEquals") + override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = + oldItem == newItem + } + + sealed class AdapterItem { + data class Device(val device: SyncDevice) : AdapterItem() + data class Tab(val tab: SyncTab) : AdapterItem() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt new file mode 100644 index 000000000..8f27a2cb9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt @@ -0,0 +1,67 @@ +/* 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.sync + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.android.synthetic.main.fragment_synced_tabs.* +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.feature.syncedtabs.SyncedTabsFeature +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.library.LibraryPageFragment + +class SyncedTabsFragment : LibraryPageFragment() { + private val syncedTabsFeature = ViewBoundFeatureWrapper() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_synced_tabs, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val backgroundServices = requireContext().components.backgroundServices + + syncedTabsFeature.set( + feature = SyncedTabsFeature( + storage = backgroundServices.syncedTabsStorage, + accountManager = backgroundServices.accountManager, + view = synced_tabs_layout, + lifecycleOwner = this.viewLifecycleOwner, + onTabClicked = ::handleTabClicked + ), + owner = this, + view = view + ) + } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.library_synced_tabs)) + } + + private fun handleTabClicked(tab: Tab) { + + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = tab.active().url, + newTab = true, + from = BrowserDirection.FromSyncedTabs + ) + } + + override val selectedItems: Set + get() = emptySet() +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt new file mode 100644 index 000000000..04459010c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt @@ -0,0 +1,38 @@ +/* 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.sync + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleOwner +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.ext.components + +class SyncedTabsIntegration( + private val context: Context, + private val accountManager: FxaAccountManager +) { + fun launch() { + val accountObserver = SyncedTabsAccountObserver(context) + + accountManager.register( + accountObserver, + owner = ProcessLifecycleOwner.get(), + autoPause = true + ) + } +} + +internal class SyncedTabsAccountObserver(private val context: Context) : AccountObserver { + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + context.components.backgroundServices.syncedTabsStorage.start() + } + + override fun onLoggedOut() { + context.components.backgroundServices.syncedTabsStorage.stop() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt new file mode 100644 index 000000000..d3868da94 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -0,0 +1,79 @@ +/* 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.sync + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.synthetic.main.component_sync_tabs.view.* +import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.R + +class SyncedTabsLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), SyncedTabsView { + + override var listener: SyncedTabsView.Listener? = null + + private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) } + + init { + inflate(getContext(), R.layout.component_sync_tabs, this) + + synced_tabs_list.layoutManager = LinearLayoutManager(context) + synced_tabs_list.adapter = adapter + + synced_tabs_pull_to_refresh.setOnRefreshListener { listener?.onRefresh() } + } + + override fun onError(error: SyncedTabsView.ErrorType) { + val stringResId = when (error) { + SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device + SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing + SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account + SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth + } + + sync_tabs_status.text = context.getText(stringResId) + + synced_tabs_list.visibility = View.GONE + sync_tabs_status.visibility = View.VISIBLE + } + + override fun displaySyncedTabs(syncedTabs: List) { + synced_tabs_list.visibility = View.VISIBLE + sync_tabs_status.visibility = View.GONE + + val allDeviceTabs = emptyList().toMutableList() + + syncedTabs.forEach { (device, tabs) -> + if (tabs.isEmpty()) { + return@forEach + } + + val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) } + + allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs + } + + adapter.submitList(allDeviceTabs) + } + + override fun startLoading() { + synced_tabs_list.visibility = View.VISIBLE + sync_tabs_status.visibility = View.GONE + + synced_tabs_pull_to_refresh.isRefreshing = true + } + + override fun stopLoading() { + synced_tabs_pull_to_refresh.isRefreshing = false + } +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt new file mode 100644 index 000000000..240a83e8c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -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.sync + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* +import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.concept.sync.DeviceType +import org.mozilla.fenix.R +import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem + +sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + abstract fun bind(item: T, interactor: (Tab) -> Unit) + + class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: (Tab) -> Unit) { + bindTab(item as AdapterItem.Tab) + + itemView.setOnClickListener { + interactor(item.tab) + } + } + + private fun bindTab(tab: AdapterItem.Tab) { + val active = tab.tab.active() + itemView.synced_tab_item_title.text = active.title + itemView.synced_tab_item_url.text = active.url + } + + companion object { + const val LAYOUT_ID = R.layout.sync_tabs_list_item + } + } + + class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: (Tab) -> Unit) { + bindHeader(item as AdapterItem.Device) + } + + private fun bindHeader(device: AdapterItem.Device) { + + val deviceLogoDrawable = when (device.device.deviceType) { + DeviceType.DESKTOP -> { R.drawable.mozac_ic_device_desktop } + else -> { R.drawable.mozac_ic_device_mobile } + } + + itemView.synced_tabs_group_name.text = device.device.displayName + itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds(deviceLogoDrawable, 0, 0, 0) + } + + companion object { + const val LAYOUT_ID = R.layout.view_synced_tabs_group + } + } +} diff --git a/app/src/main/res/layout/component_sync_tabs.xml b/app/src/main/res/layout/component_sync_tabs.xml new file mode 100644 index 000000000..6b82ef336 --- /dev/null +++ b/app/src/main/res/layout/component_sync_tabs.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_synced_tabs.xml b/app/src/main/res/layout/fragment_synced_tabs.xml new file mode 100644 index 000000000..190b36d96 --- /dev/null +++ b/app/src/main/res/layout/fragment_synced_tabs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/sync_tabs_list_item.xml b/app/src/main/res/layout/sync_tabs_list_item.xml new file mode 100644 index 000000000..693892370 --- /dev/null +++ b/app/src/main/res/layout/sync_tabs_list_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_synced_tabs_group.xml b/app/src/main/res/layout/view_synced_tabs_group.xml new file mode 100644 index 000000000..5543f4960 --- /dev/null +++ b/app/src/main/res/layout/view_synced_tabs_group.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 8a401fb08..915bc29ce 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -36,6 +36,7 @@ + @@ -149,6 +150,9 @@ android:name="shouldAnimate" app:argType="boolean" android:defaultValue="false" /> + @@ -295,6 +299,13 @@ app:popUpToInclusive="true" /> + + + 14sp 18sp 56dp + 56dp 16dp 12dp 24dp diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index 07b77edcf..1f56530dc 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -67,6 +67,7 @@ pref_key_sync_history pref_key_sync_bookmarks pref_key_sync_logins + pref_key_sync_tabs pref_key_sign_out pref_key_cached_account pref_key_sync_pair diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59ce7aef2..b68bb2ef8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -95,6 +95,8 @@ Add to Home screen Install + + Synced Tabs Find in page @@ -286,6 +288,8 @@ Bookmarks Logins + + Tabs Sign out @@ -416,6 +420,8 @@ Other Bookmarks History + + Synced Tabs Reading List @@ -1378,4 +1384,15 @@ Voice search Speak now + + + + Connect with a Firefox Account. + + Connect another device. + + Please re-authenticate. + + Please enable tab syncing. + diff --git a/app/src/main/res/xml/account_settings_preferences.xml b/app/src/main/res/xml/account_settings_preferences.xml index c07994024..eab4e96e6 100644 --- a/app/src/main/res/xml/account_settings_preferences.xml +++ b/app/src/main/res/xml/account_settings_preferences.xml @@ -42,5 +42,13 @@ android:key="@string/pref_key_sync_logins" android:layout="@layout/checkbox_left_preference" android:title="@string/preferences_sync_logins" /> + + + diff --git a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt index a18a9e250..9b989fd5b 100644 --- a/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/BackgroundServicesTest.kt @@ -25,7 +25,7 @@ import org.mozilla.fenix.components.metrics.MetricController class BackgroundServicesTest { class TestableBackgroundServices( val context: Context - ) : BackgroundServices(context, mockk(), mockk(), mockk(), mockk(), mockk()) { + ) : BackgroundServices(context, mockk(), mockk(), mockk(), mockk(), mockk(), mockk()) { override fun makeAccountManager( context: Context, serverConfig: ServerConfig, diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index d5e3ee539..11e1a307e 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -97,6 +97,7 @@ object Deps { const val mozilla_feature_qr = "org.mozilla.components:feature-qr:${Versions.mozilla_android_components}" const val mozilla_feature_search = "org.mozilla.components:feature-search:${Versions.mozilla_android_components}" const val mozilla_feature_session = "org.mozilla.components:feature-session:${Versions.mozilla_android_components}" + const val mozilla_feature_syncedtabs = "org.mozilla.components:feature-syncedtabs:${Versions.mozilla_android_components}" const val mozilla_feature_tabs = "org.mozilla.components:feature-tabs:${Versions.mozilla_android_components}" const val mozilla_feature_downloads = "org.mozilla.components:feature-downloads:${Versions.mozilla_android_components}" const val mozilla_feature_storage = "org.mozilla.components:feature-storage:${Versions.mozilla_android_components}"