1
0
Fork 0

For #10834 - Adding Sync Tabs Feature in Fenix

master
Vishwa Patel 2020-05-26 16:25:52 -04:00 committed by Emily Kager
parent 0300f15df1
commit 4da22c605a
30 changed files with 605 additions and 16 deletions

View File

@ -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

View File

@ -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),

View File

@ -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
*/

View File

@ -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 ->

View File

@ -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<PlacesHistoryStorage>,
bookmarkStorage: Lazy<PlacesBookmarksStorage>,
passwordsStorage: Lazy<SyncableLoginsStorage>
passwordsStorage: Lazy<SyncableLoginsStorage>,
remoteTabsStorage: Lazy<RemoteTabsStorage>
) {
// 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()

View File

@ -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) }

View File

@ -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 }

View File

@ -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<Events.browserMenuActionKeys, String>?

View File

@ -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 ->

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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 ->

View File

@ -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<CheckBoxPreference>(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<CheckBoxPreference>(tabsNameKey)?.apply {
isVisible = FeatureFlags.syncedTabs
isEnabled = syncEnginesStatus.containsKey(SyncEngine.Tabs)
isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { FeatureFlags.syncedTabs }
}
}
private fun syncNow() {

View File

@ -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<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(
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<AdapterItem>() {
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()
}
}

View File

@ -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<Tab>() {
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
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<Tab>
get() = emptySet()
}

View File

@ -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()
}
}

View File

@ -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<SyncedDeviceTabs>) {
synced_tabs_list.visibility = View.VISIBLE
sync_tabs_status.visibility = View.GONE
val allDeviceTabs = emptyList<SyncedTabsAdapter.AdapterItem>().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
}
}

View File

@ -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 <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit)
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> 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 <T : AdapterItem> 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
}
}
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/history_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/sync_tabs_progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:indeterminate="true"
android:layout_width="match_parent"
android:layout_height="8dp"
android:translationY="-3dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/sync_tabs_status"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:text="@string/sync_connect_device"
android:textColor="?secondaryText"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/synced_tabs_pull_to_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/synced_tabs_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/history_list_item"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<org.mozilla.fenix.sync.SyncedTabsLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/synced_tabs_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="60dp"
android:paddingEnd="20dp"
android:minHeight="@dimen/library_item_height"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/synced_tab_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?primaryText"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@+id/synced_tab_item_url"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Tab Title" />
<TextView
android:id="@+id/synced_tab_item_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
tools:text="https://example.com/"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/synced_tab_item_title"
app:layout_constraintBottom_toBottomOf="parent" />
<View
android:id="@+id/synced_tab_item_separator"
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="?neutralFaded"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/synced_tabs_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/synced_tabs_group_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
app:drawableStartCompat="@drawable/mozac_ic_device_desktop"
app:drawableTint="?primaryText"
android:textSize="17sp"
android:textAppearance="@style/Header14TextStyle"
android:textColor="?primaryText"
android:paddingStart="20dp"
android:paddingEnd="0dp"
android:layout_marginBottom="8dp"
tools:text="Header" />
</RelativeLayout>

View File

@ -36,6 +36,7 @@
<action android:id="@+id/action_global_deleteBrowsingDataFragment" app:destination="@id/deleteBrowsingDataFragment" />
<action android:id="@+id/action_global_webExtensionActionPopupFragment" app:destination="@id/webExtensionActionPopupFragment" />
<action android:id="@+id/action_global_settingsFragment" app:destination="@id/settingsFragment" />
<action android:id="@+id/action_global_syncedTabsFragment" app:destination="@+id/syncedTabsFragment" />
<action android:id="@+id/action_global_privateBrowsingFragment" app:destination="@id/privateBrowsingFragment"/>
<action android:id="@+id/action_global_bookmarkFragment" app:destination="@id/bookmarkFragment"/>
<action android:id="@+id/action_global_historyFragment" app:destination="@id/historyFragment"/>
@ -149,6 +150,9 @@
android:name="shouldAnimate"
app:argType="boolean"
android:defaultValue="false" />
<action
android:id="@+id/action_browserFragment_to_syncedTabsFragment"
app:destination="@id/syncedTabsFragment" />
<action
android:id="@+id/action_browserFragment_to_settingsFragment"
app:destination="@id/settingsFragment" />
@ -295,6 +299,13 @@
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/syncedTabsFragment"
android:name="org.mozilla.fenix.sync.SyncedTabsFragment"
android:label="@string/synced_tabs"
tools:layout="@layout/fragment_synced_tabs">
</fragment>
<fragment
android:id="@+id/loginDetailFragment"
android:name="org.mozilla.fenix.settings.logins.LoginDetailFragment"

View File

@ -31,6 +31,7 @@
<dimen name="phone_feature_label_recommended_text_size">14sp</dimen>
<dimen name="site_permissions_exceptions_item_text_size">18sp</dimen>
<dimen name="site_permissions_exceptions_item_height">56dp</dimen>
<dimen name="synced_tab_item_min_height">56dp</dimen>
<dimen name="component_collection_creation_list_margin">16dp</dimen>
<dimen name="exceptions_description_margin">12dp</dimen>
<dimen name="preference_seek_bar_padding">24dp</dimen>

View File

@ -67,6 +67,7 @@
<string name="pref_key_sync_history" translatable="false">pref_key_sync_history</string>
<string name="pref_key_sync_bookmarks" translatable="false">pref_key_sync_bookmarks</string>
<string name="pref_key_sync_logins" translatable="false">pref_key_sync_logins</string>
<string name="pref_key_sync_tabs" translatable="false">pref_key_sync_tabs</string>
<string name="pref_key_sign_out" translatable="false">pref_key_sign_out</string>
<string name="pref_key_cached_account" translatable="false">pref_key_cached_account</string>
<string name="pref_key_sync_pair" translatable="false">pref_key_sync_pair</string>

View File

@ -95,6 +95,8 @@
<string name="browser_menu_add_to_homescreen">Add to Home screen</string>
<!-- Browser menu toggle that installs a Progressive Web App shortcut to the site on the device home screen. -->
<string name="browser_menu_install_on_homescreen">Install</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">Synced Tabs</string>
<!-- Browser menu button that opens the find in page menu -->
<string name="browser_menu_find_in_page">Find in page</string>
<!-- Browser menu button that creates a private tab -->
@ -286,6 +288,8 @@
<string name="preferences_sync_bookmarks">Bookmarks</string>
<!-- Preference for syncing logins -->
<string name="preferences_sync_logins">Logins</string>
<!-- Preference for syncing tabs -->
<string name="preferences_sync_tabs">Tabs</string>
<!-- Preference for signing out -->
<string name="preferences_sign_out">Sign out</string>
<!-- Preference displays and allows changing current FxA device name -->
@ -416,6 +420,8 @@
<string name="library_desktop_bookmarks_unfiled">Other Bookmarks</string>
<!-- Option in Library to open History page -->
<string name="library_history">History</string>
<!-- Option in Library to open Synced Tabs page -->
<string name="library_synced_tabs">Synced Tabs</string>
<!-- Option in Library to open Reading List -->
<string name="library_reading_list">Reading List</string>
<!-- Menu Item Label for Search in Library -->
@ -1378,4 +1384,15 @@
<string name="voice_search_content_description">Voice search</string>
<!-- Voice search prompt description displayed after the user presses the voice search button -->
<string name="voice_search_explainer">Speak now</string>
<!-- Synced Tabs -->
<!-- Text displayed when user is not logged into a Firefox Account -->
<string name="synced_tabs_connect_to_sync_account">Connect with a Firefox Account.</string>
<!-- Text displayed to ask user to connect another device as no devices found with account -->
<string name="synced_tabs_connect_another_device">Connect another device.</string>
<!-- Text displayed asking user to re-authenticate -->
<string name="synced_tabs_reauth">Please re-authenticate.</string>
<!-- Text displayed when user has disabled tab syncing in Firefox Sync Account -->
<string name="synced_tabs_enable_tab_syncing">Please enable tab syncing.</string>
</resources>

View File

@ -42,5 +42,13 @@
android:key="@string/pref_key_sync_logins"
android:layout="@layout/checkbox_left_preference"
android:title="@string/preferences_sync_logins" />
<androidx.preference.CheckBoxPreference
android:defaultValue="false"
app:isPreferenceVisible="false"
android:key="@string/pref_key_sync_tabs"
android:layout="@layout/checkbox_left_preference"
android:title="@string/preferences_sync_tabs"/>
</androidx.preference.PreferenceCategory>
</PreferenceScreen>

View File

@ -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,

View File

@ -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}"