1
0
Fork 0

For #12287: Add Synced Tabs to Tabs Tray

master
Jonathan Almeida 2020-08-18 00:09:27 -04:00 committed by Jonathan Almeida
parent 2e62dd5c87
commit f614c0b18d
25 changed files with 676 additions and 143 deletions

View File

@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment), FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment), FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment), FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment) FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment)
} }

View File

@ -100,6 +100,7 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
@ -597,6 +598,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment -> BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
} }
/** /**

View File

@ -10,14 +10,18 @@ import androidx.navigation.NavController
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.NoTabsViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TitleViewHolder
import org.mozilla.fenix.sync.ext.toAdapterList
import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice import mozilla.components.concept.sync.Device as SyncDevice
class SyncedTabsAdapter( class SyncedTabsAdapter(
private val listener: (SyncTab) -> Unit private val newListener: SyncedTabsView.Listener
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) { ) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
@ -27,30 +31,26 @@ class SyncedTabsAdapter(
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView)
NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
holder.bind(getItem(position), listener) holder.bind(getItem(position), newListener)
} }
override fun getItemViewType(position: Int) = when (getItem(position)) { override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID
is AdapterItem.Title -> TitleViewHolder.LAYOUT_ID
is AdapterItem.NoTabs -> NoTabsViewHolder.LAYOUT_ID
} }
fun updateData(syncedTabs: List<SyncedDeviceTabs>) { fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
val allDeviceTabs = mutableListOf<AdapterItem>() val allDeviceTabs = syncedTabs.toAdapterList()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
submitList(allDeviceTabs) submitList(allDeviceTabs)
} }
@ -59,7 +59,11 @@ class SyncedTabsAdapter(
when (oldItem) { when (oldItem) {
is AdapterItem.Device -> is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.Tab, is AdapterItem.Error -> is AdapterItem.NoTabs ->
newItem is AdapterItem.NoTabs && oldItem.device.id == newItem.device.id
is AdapterItem.Tab,
is AdapterItem.Error,
is AdapterItem.Title ->
oldItem == newItem oldItem == newItem
} }
@ -68,9 +72,35 @@ class SyncedTabsAdapter(
oldItem == newItem oldItem == newItem
} }
/**
* The various types of adapter items that can be found in a [SyncedTabsAdapter].
*/
sealed class AdapterItem { sealed class AdapterItem {
/**
* A title header of the Synced Tabs UI that has a refresh button in it. This may be seen
* only in some views depending on where the Synced Tabs UI is displayed.
*/
object Title : AdapterItem()
/**
* A device header for displaying a synced device.
*/
data class Device(val device: SyncDevice) : AdapterItem() data class Device(val device: SyncDevice) : AdapterItem()
/**
* A tab that was synced.
*/
data class Tab(val tab: SyncTab) : AdapterItem() data class Tab(val tab: SyncTab) : AdapterItem()
/**
* A placeholder for a device that has no tabs synced.
*/
data class NoTabs(val device: SyncDevice) : AdapterItem()
/**
* A message displayed if an error was encountered.
*/
data class Error( data class Error(
val descriptionResId: Int, val descriptionResId: Int,
val navController: NavController? = null val navController: NavController? = null

View File

@ -7,7 +7,6 @@ package org.mozilla.fenix.sync
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.fragment.app.findFragment import androidx.fragment.app.findFragment
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -18,8 +17,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.feature.syncedtabs.view.SyncedTabsView import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import java.lang.IllegalStateException import java.lang.IllegalStateException
class SyncedTabsLayout @JvmOverloads constructor( class SyncedTabsLayout @JvmOverloads constructor(
@ -30,7 +32,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
override var listener: SyncedTabsView.Listener? = null override var listener: SyncedTabsView.Listener? = null
private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) } private val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val coroutineScope = CoroutineScope(Dispatchers.Main) private val coroutineScope = CoroutineScope(Dispatchers.Main)
init { init {
@ -53,8 +55,8 @@ class SyncedTabsLayout @JvmOverloads constructor(
null null
} }
val descriptionResId = stringResourceForError(error) val descriptionResId = error.toStringRes()
val errorItem = getErrorItem(navController, error, descriptionResId) val errorItem = error.toAdapterItem(descriptionResId, navController)
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem) val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
adapter.submitList(errorList) adapter.submitList(errorList)
@ -96,27 +98,21 @@ class SyncedTabsLayout @JvmOverloads constructor(
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true
} }
}
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = 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_sign_in_message * We have to do this weird daisy-chaining of callbacks because the listener is nullable and
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth * when we get a null reference, we never get a new binding to the non-null listener.
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs */
} class ListenerDelegate(
private val listener: (() -> SyncedTabsView.Listener?)
internal fun getErrorItem( ) : SyncedTabsView.Listener {
navController: NavController?, override fun onRefresh() {
error: SyncedTabsView.ErrorType, listener.invoke()?.onRefresh()
@StringRes stringResId: Int }
): SyncedTabsAdapter.AdapterItem = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, override fun onTabClicked(tab: Tab) {
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE, listener.invoke()?.onTabClicked(tab)
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}
} }
} }

View File

@ -7,29 +7,36 @@ package org.mozilla.fenix.sync
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.sync_tabs_error_row.view.* import kotlinx.android.synthetic.main.sync_tabs_error_row.view.*
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import mozilla.components.browser.storage.sync.Tab import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
* The various view-holders that can be found in a [SyncedTabsAdapter]. For more
* descriptive information on the different types, see the docs for [AdapterItem].
*/
sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) abstract fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener)
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) { override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindTab(item as AdapterItem.Tab) bindTab(item as AdapterItem.Tab)
itemView.setOnClickListener { itemView.setOnClickListener {
interactor(item.tab) interactor.onTabClicked(item.tab)
} }
} }
@ -46,7 +53,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) { override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
val errorItem = item as AdapterItem.Error val errorItem = item as AdapterItem.Error
setErrorMargins() setErrorMargins()
@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) { override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindHeader(item as AdapterItem.Device) bindHeader(item as AdapterItem.Device)
} }
@ -93,6 +100,36 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
} }
} }
class NoTabsViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) = Unit
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_no_item
}
}
class TitleViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
itemView.refresh_icon.setOnClickListener { v ->
val rotation = AnimationUtils.loadAnimation(
itemView.context,
R.anim.full_rotation
).apply {
repeatCount = Animation.ABSOLUTE
}
v.startAnimation(rotation)
interactor.onRefresh()
}
}
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_title
}
}
internal fun setErrorMargins() { internal fun setErrorMargins() {
val lp = LinearLayout.LayoutParams( val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,

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.ext
import androidx.annotation.StringRes
import androidx.navigation.NavController
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsAdapter
/**
* Converts the error type to the appropriate matching string resource for displaying to the user.
*/
fun ErrorType.toStringRes() = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message
ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
/**
* Converts an error type to an [SyncedTabsAdapter.AdapterItem.Error].
*/
fun ErrorType.toAdapterItem(
@StringRes stringResId: Int,
navController: NavController? = null
) = when (this) {
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId)
ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem
.Error(descriptionResId = stringResId, navController = navController)
}

View File

@ -0,0 +1,22 @@
/* 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.ext
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
fun List<SyncedDeviceTabs>.toAdapterList(
): MutableList<AdapterItem> {
val allDeviceTabs = mutableListOf<AdapterItem>()
forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
return allDeviceTabs
}

View File

@ -0,0 +1,55 @@
/* 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.tabtray
import android.view.View
import androidx.fragment.app.FragmentManager.findFragment
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.sync.ListenerDelegate
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.ext.toAdapterList
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import kotlin.coroutines.CoroutineContext
class SyncedTabsController(
private val view: View,
coroutineContext: CoroutineContext = Dispatchers.Main
) : SyncedTabsView {
override var listener: SyncedTabsView.Listener? = null
val adapter = SyncedTabsAdapter(ListenerDelegate { listener })
private val scope: CoroutineScope = CoroutineScope(coroutineContext)
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
scope.launch {
val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList()
// Reverse layout for TabTrayView which does things backwards.
adapter.submitList(tabsList.reversed())
}
}
override fun onError(error: SyncedTabsView.ErrorType) {
scope.launch {
val navController: NavController? = try {
findFragment<TabTrayDialogFragment>(view).findNavController()
} catch (exception: IllegalStateException) {
null
}
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
adapter.submitList(listOf(errorItem))
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.engine.profiler.Profiler import mozilla.components.concept.engine.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -30,6 +32,7 @@ interface TabTrayController {
fun onNewTabTapped(private: Boolean) fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed() fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean) fun onShareTabsClicked(private: Boolean)
fun onSyncedTabClicked(syncTab: SyncTab)
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun onCloseAllTabsClicked(private: Boolean) fun onCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean fun handleBackPressed(): Boolean
@ -59,6 +62,7 @@ interface TabTrayController {
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class DefaultTabTrayController( class DefaultTabTrayController(
private val activity: HomeActivity,
private val profiler: Profiler?, private val profiler: Profiler?,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val browsingModeManager: BrowsingModeManager, private val browsingModeManager: BrowsingModeManager,
@ -117,6 +121,14 @@ class DefaultTabTrayController(
navController.navigate(directions) navController.navigate(directions)
} }
override fun onSyncedTabClicked(syncTab: SyncTab) {
activity.openToBrowserAndLoad(
searchTermOrURL = syncTab.active().url,
newTab = true,
from = BrowserDirection.FromTabTray
)
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) { override fun onCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) { val sessionsToClose = if (private) {

View File

@ -177,6 +177,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
adapter, adapter,
interactor = TabTrayFragmentInteractor( interactor = TabTrayFragmentInteractor(
DefaultTabTrayController( DefaultTabTrayController(
activity = activity,
profiler = activity.components.core.engine.profiler, profiler = activity.components.core.engine.profiler,
sessionManager = activity.components.core.sessionManager, sessionManager = activity.components.core.sessionManager,
browsingModeManager = activity.browsingModeManager, browsingModeManager = activity.browsingModeManager,
@ -194,7 +195,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
isPrivate = isPrivate, isPrivate = isPrivate,
startingInLandscape = requireContext().resources.configuration.orientation == startingInLandscape = requireContext().resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE, Configuration.ORIENTATION_LANDSCAPE,
lifecycleScope = viewLifecycleOwner.lifecycleScope lifecycleOwner = viewLifecycleOwner
) { private -> ) { private ->
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private } val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.tabtray package org.mozilla.fenix.tabtray
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.storage.sync.Tab as SyncTab
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface TabTrayInteractor { interface TabTrayInteractor {
@ -33,6 +34,11 @@ interface TabTrayInteractor {
*/ */
fun onCloseAllTabsClicked(private: Boolean) fun onCloseAllTabsClicked(private: Boolean)
/**
* Called when the user clicks on a synced tab entry.
*/
fun onSyncedTabClicked(syncTab: SyncTab)
/** /**
* Called when the physical back button is clicked. * Called when the physical back button is clicked.
*/ */
@ -89,6 +95,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.onCloseAllTabsClicked(private) controller.onCloseAllTabsClicked(private)
} }
override fun onSyncedTabClicked(syncTab: SyncTab) {
controller.onSyncedTabClicked(syncTab)
}
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return controller.handleBackPressed() return controller.handleBackPressed()
} }

View File

@ -16,7 +16,8 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -35,7 +36,10 @@ import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.browser.tabstray.TabViewHolder import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -58,9 +62,10 @@ class TabTrayView(
private val interactor: TabTrayInteractor, private val interactor: TabTrayInteractor,
isPrivate: Boolean, isPrivate: Boolean,
startingInLandscape: Boolean, startingInLandscape: Boolean,
lifecycleScope: LifecycleCoroutineScope, lifecycleOwner: LifecycleOwner,
private val filterTabs: (Boolean) -> Unit private val filterTabs: (Boolean) -> Unit
) : LayoutContainer, TabLayout.OnTabSelectedListener { ) : LayoutContainer, TabLayout.OnTabSelectedListener {
val lifecycleScope = lifecycleOwner.lifecycleScope
val fabView = LayoutInflater.from(container.context) val fabView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_fab, container, true) .inflate(R.layout.component_tabstray_fab, container, true)
@ -79,13 +84,18 @@ class TabTrayView(
private var tabsTouchHelper: TabsTouchHelper private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
private val syncedTabsController = SyncedTabsController(view)
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
private var hasLoaded = false private var hasLoaded = false
override val containerView: View? override val containerView: View?
get() = container get() = container
private val components = container.context.components
init { init {
container.context.components.analytics.metrics.track(Event.TabsTrayOpened) components.analytics.metrics.track(Event.TabsTrayOpened)
toggleFabText(isPrivate) toggleFabText(isPrivate)
@ -102,7 +112,7 @@ class TabTrayView(
override fun onStateChanged(bottomSheet: View, newState: Int) { override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) { if (newState == BottomSheetBehavior.STATE_HIDDEN) {
container.context.components.analytics.metrics.track(Event.TabsTrayClosed) components.analytics.metrics.track(Event.TabsTrayClosed)
interactor.onTabTrayDismissed() interactor.onTabTrayDismissed()
} }
} }
@ -135,8 +145,20 @@ class TabTrayView(
setTopOffset(startingInLandscape) setTopOffset(startingInLandscape)
val concatAdapter = ConcatAdapter(tabsAdapter) syncedTabsFeature.set(
feature = SyncedTabsFeature(
context = container.context,
storage = components.backgroundServices.syncedTabsStorage,
accountManager = components.backgroundServices.accountManager,
view = syncedTabsController,
lifecycleOwner = lifecycleOwner,
onTabClicked = ::handleTabClicked
),
owner = lifecycleOwner,
view = view
)
val concatAdapter = ConcatAdapter(tabsAdapter)
view.tabsTray.apply { view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply { layoutManager = LinearLayoutManager(container.context).apply {
reverseLayout = true reverseLayout = true
@ -156,6 +178,9 @@ class TabTrayView(
// Put the 'Add to collections' button after the tabs have loaded. // Put the 'Add to collections' button after the tabs have loaded.
concatAdapter.addAdapter(0, collectionsButtonAdapter) concatAdapter.addAdapter(0, collectionsButtonAdapter)
// Put the Synced Tabs adapter at the end.
concatAdapter.addAdapter(0, syncedTabsController.adapter)
if (hasAccessibilityEnabled) { if (hasAccessibilityEnabled) {
tabsAdapter.notifyDataSetChanged() tabsAdapter.notifyDataSetChanged()
} }
@ -193,7 +218,7 @@ class TabTrayView(
} }
view.tab_tray_overflow.setOnClickListener { view.tab_tray_overflow.setOnClickListener {
container.context.components.analytics.metrics.track(Event.TabsTrayMenuOpened) components.analytics.metrics.track(Event.TabsTrayMenuOpened)
menu = tabTrayItemMenu.menuBuilder.build(container.context) menu = tabTrayItemMenu.menuBuilder.build(container.context)
menu?.show(it) menu?.show(it)
?.also { pu -> ?.also { pu ->
@ -209,6 +234,10 @@ class TabTrayView(
adjustNewTabButtonsForNormalMode() adjustNewTabButtonsForNormalMode()
} }
private fun handleTabClicked(tab: SyncTab) {
interactor.onSyncedTabClicked(tab)
}
private fun adjustNewTabButtonsForNormalMode() { private fun adjustNewTabButtonsForNormalMode() {
view.tab_tray_new_tab.apply { view.tab_tray_new_tab.apply {
isVisible = hasAccessibilityEnabled isVisible = hasAccessibilityEnabled
@ -234,7 +263,7 @@ class TabTrayView(
Event.NewTabTapped Event.NewTabTapped
} }
container.context.components.analytics.metrics.track(eventToSend) components.analytics.metrics.track(eventToSend)
} }
fun expand() { fun expand() {
@ -261,17 +290,14 @@ class TabTrayView(
scrollToTab(view.context.components.core.store.state.selectedTabId) scrollToTab(view.context.components.core.store.state.selectedTabId)
if (isPrivateModeSelected) { if (isPrivateModeSelected) {
container.context.components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped) components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
} else { } else {
container.context.components.analytics.metrics.track(Event.TabsTrayNormalModeTapped) components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
} }
} }
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/ override fun onTabReselected(tab: TabLayout.Tab?) = Unit
} override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
}
var mode: Mode = Mode.Normal var mode: Mode = Mode.Normal
private set private set
@ -513,7 +539,9 @@ class TabTrayView(
// We offset the tab index by the number of items in the other adapters. // We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`. // We add the offset, because the layoutManager is initialized with `reverseLayout`.
val recyclerViewIndex = selectedBrowserTabIndex + collectionsButtonAdapter.itemCount val recyclerViewIndex = selectedBrowserTabIndex +
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount
layoutManager?.scrollToPosition(recyclerViewIndex) layoutManager?.scrollToPosition(recyclerViewIndex)
} }

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="275"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" />

View File

@ -19,6 +19,7 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textSize="14sp" android:textSize="14sp"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="@color/tab_tray_item_text_normal_theme"
tools:text="@string/synced_tabs_no_tabs"/> tools:text="@string/synced_tabs_no_tabs"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

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/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

View File

@ -0,0 +1,29 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/Header16TextStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:layout_weight="1"
android:text="@string/synced_tabs" />
<ImageView
android:id="@+id/refresh_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="60dp"
android:layout_marginEnd="16dp"
app:srcCompat="@drawable/mozac_ic_refresh"
app:tint="?primaryText" />
</LinearLayout>

View File

@ -0,0 +1,26 @@
/* 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 io.mockk.mockk
import io.mockk.verify
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.junit.Test
class ListenerDelegateTest {
@Test
fun `delegate invokes nullable listener`() {
val listener: SyncedTabsView.Listener? = mockk(relaxed = true)
val delegate = ListenerDelegate { listener }
delegate.onRefresh()
verify { listener?.onRefresh() }
delegate.onTabClicked(mockk())
verify { listener?.onTabClicked(any()) }
}
}

View File

@ -11,6 +11,7 @@ import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
@ -21,7 +22,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsAdapterTest { class SyncedTabsAdapterTest {
private lateinit var listener: (Tab) -> Unit private lateinit var listener: SyncedTabsView.Listener
private lateinit var adapter: SyncedTabsAdapter private lateinit var adapter: SyncedTabsAdapter
private val oneTabDevice = SyncedDeviceTabs( private val oneTabDevice = SyncedDeviceTabs(
@ -77,10 +78,12 @@ class SyncedTabsAdapterTest {
fun `updateData() adds items for each device and tab`() { fun `updateData() adds items for each device and tab`() {
assertEquals(0, adapter.itemCount) assertEquals(0, adapter.itemCount)
adapter.updateData(listOf( adapter.updateData(
oneTabDevice, listOf(
threeTabDevice oneTabDevice,
)) threeTabDevice
)
)
assertEquals(5, adapter.itemCount) assertEquals(5, adapter.itemCount)
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0)) assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0))

View File

@ -4,16 +4,10 @@
package org.mozilla.fenix.sync package org.mozilla.fenix.sync
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R
class SyncedTabsLayoutTest { class SyncedTabsLayoutTest {
@ -25,73 +19,4 @@ class SyncedTabsLayoutTest {
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_NEEDS_REAUTHENTICATION)) assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_NEEDS_REAUTHENTICATION))
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_UNAVAILABLE)) assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_UNAVAILABLE))
} }
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
SyncedTabsLayout.stringResourceForError(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_ENGINE_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_sign_in_message,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_UNAVAILABLE)
)
assertEquals(
R.string.synced_tabs_reauth,
SyncedTabsLayout.stringResourceForError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
)
assertEquals(
R.string.synced_tabs_no_tabs,
SyncedTabsLayout.stringResourceForError(ErrorType.NO_TABS_AVAILABLE)
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
R.string.synced_tabs_connect_another_device
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_ENGINE_UNAVAILABLE,
R.string.synced_tabs_enable_tab_syncing
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_NEEDS_REAUTHENTICATION,
R.string.synced_tabs_reauth
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.NO_TABS_AVAILABLE,
R.string.synced_tabs_no_tabs
)
assertNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = SyncedTabsLayout.getErrorItem(
navController,
ErrorType.SYNC_UNAVAILABLE,
R.string.synced_tabs_sign_in_message
)
assertNotNull((errorItem as SyncedTabsAdapter.AdapterItem.Error).navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
} }

View File

@ -7,15 +7,18 @@ package org.mozilla.fenix.sync
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import io.mockk.Called
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
import kotlinx.android.synthetic.main.view_synced_tabs_title.view.*
import mozilla.components.browser.storage.sync.Tab import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
@ -32,6 +35,10 @@ class SyncedTabsViewHolderTest {
private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder
private lateinit var deviceView: View private lateinit var deviceView: View
private lateinit var deviceViewGroupName: TextView private lateinit var deviceViewGroupName: TextView
private lateinit var titleView: View
private lateinit var titleViewHolder: SyncedTabsViewHolder.TitleViewHolder
private lateinit var noTabsView: View
private lateinit var noTabsViewHolder: SyncedTabsViewHolder.NoTabsViewHolder
private val tab = Tab( private val tab = Tab(
history = listOf( history = listOf(
@ -59,6 +66,12 @@ class SyncedTabsViewHolderTest {
every { synced_tabs_group_name } returns deviceViewGroupName every { synced_tabs_group_name } returns deviceViewGroupName
} }
deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView) deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView)
titleView = inflater.inflate(SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID, null)
titleViewHolder = SyncedTabsViewHolder.TitleViewHolder(titleView)
noTabsView = inflater.inflate(SyncedTabsViewHolder.NoTabsViewHolder.LAYOUT_ID, null)
noTabsViewHolder = SyncedTabsViewHolder.NoTabsViewHolder(noTabsView)
} }
@Test @Test
@ -71,11 +84,11 @@ class SyncedTabsViewHolderTest {
@Test @Test
fun `TabViewHolder calls interactor on click`() { fun `TabViewHolder calls interactor on click`() {
val interactor = mockk<(Tab) -> Unit>(relaxed = true) val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor) tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor)
tabView.performClick() tabView.performClick()
verify { interactor(tab) } verify { interactor.onTabClicked(tab) }
} }
@Test @Test
@ -109,4 +122,28 @@ class SyncedTabsViewHolderTest {
) )
} }
} }
@Test
fun `TitleViewHolder calls interactor refresh`() {
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
titleViewHolder.bind(SyncedTabsAdapter.AdapterItem.Title, interactor)
titleView.findViewById<View>(R.id.refresh_icon).performClick()
verify { interactor.onRefresh() }
}
@Test
fun `NoTabsViewHolder does nothing`() {
val device = mockk<Device> {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
}
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
noTabsViewHolder.bind(SyncedTabsAdapter.AdapterItem.NoTabs(device), interactor)
titleView.performClick()
verify { interactor wasNot Called }
}
} }

View File

@ -0,0 +1,72 @@
package org.mozilla.fenix.sync.ext
import org.junit.Test
import androidx.navigation.NavController
import io.mockk.mockk
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import org.mozilla.fenix.R
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertEquals
class ErrorTypeKtTest {
@Test
fun `string resource for error`() {
assertEquals(
R.string.synced_tabs_connect_another_device,
ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_enable_tab_syncing,
ErrorType.SYNC_ENGINE_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_sign_in_message,
ErrorType.SYNC_UNAVAILABLE.toStringRes()
)
assertEquals(
R.string.synced_tabs_reauth,
ErrorType.SYNC_NEEDS_REAUTHENTICATION.toStringRes()
)
assertEquals(
R.string.synced_tabs_no_tabs,
ErrorType.NO_TABS_AVAILABLE.toStringRes()
)
}
@Test
fun `get error item`() {
val navController = mockk<NavController>()
var errorItem = ErrorType.MULTIPLE_DEVICES_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_connect_another_device, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_connect_another_device, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_ENGINE_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_enable_tab_syncing, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_enable_tab_syncing, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_NEEDS_REAUTHENTICATION.toAdapterItem(
R.string.synced_tabs_reauth, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_reauth, errorItem.descriptionResId)
errorItem = ErrorType.NO_TABS_AVAILABLE.toAdapterItem(
R.string.synced_tabs_no_tabs, navController
)
assertNull(errorItem.navController)
assertEquals(R.string.synced_tabs_no_tabs, errorItem.descriptionResId)
errorItem = ErrorType.SYNC_UNAVAILABLE.toAdapterItem(
R.string.synced_tabs_sign_in_message, navController
)
assertNotNull(errorItem.navController)
assertEquals(R.string.synced_tabs_sign_in_message, errorItem.descriptionResId)
}
}

View File

@ -0,0 +1,92 @@
/* 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.ext
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.concept.sync.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsAdapter
class SyncedTabsAdapterKtTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = emptyList()
)
private val oneTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = listOf(Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
))
)
private val twoTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Emerald"
every { deviceType } returns DeviceType.MOBILE
},
tabs = listOf(
Tab(
history = listOf(TabEntry(
title = "Mozilla",
url = "https://mozilla.org",
iconUrl = null
)),
active = 0,
lastUsed = 0L
),
Tab(
history = listOf(
TabEntry(
title = "Firefox",
url = "https://firefox.com",
iconUrl = null
)
),
active = 0,
lastUsed = 0L
)
)
)
@Test
fun `verify ordering of adapter items`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(5, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[2] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[3] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[4] is SyncedTabsAdapter.AdapterItem.Tab)
}
@Test
fun `verify no tabs displayed`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
assertEquals(0, adapterData.count())
}
}

View File

@ -26,6 +26,8 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -34,6 +36,7 @@ import org.mozilla.fenix.ext.sessionsOfType
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DefaultTabTrayControllerTest { class DefaultTabTrayControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val profiler: Profiler? = mockk(relaxed = true) private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk() private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true) private val sessionManager: SessionManager = mockk(relaxed = true)
@ -81,6 +84,7 @@ class DefaultTabTrayControllerTest {
every { tabCollection.title } returns "Collection title" every { tabCollection.title } returns "Collection title"
controller = DefaultTabTrayController( controller = DefaultTabTrayController(
activity = activity,
profiler = profiler, profiler = profiler,
sessionManager = sessionManager, sessionManager = sessionManager,
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
@ -156,6 +160,15 @@ class DefaultTabTrayControllerTest {
} }
} }
@Test
fun onSyncedTabClicked() {
controller.onSyncedTabClicked(mockk(relaxed = true))
verify {
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
}
}
@Test @Test
fun handleBackPressed() { fun handleBackPressed() {
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect( every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(

View File

@ -0,0 +1,78 @@
package org.mozilla.fenix.tabtray
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.sync.SyncedTabsViewHolder
@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsControllerTest {
private lateinit var view: View
private lateinit var controller: SyncedTabsController
@Before
fun setup() = runBlockingTest {
view = LayoutInflater.from(testContext).inflate(R.layout.about_list_item, null)
controller = SyncedTabsController(view, coroutineContext)
}
@Test
fun `display synced tabs in reverse`() {
val tabs = listOf(
SyncedDeviceTabs(
device = mockk(relaxed = true),
tabs = listOf(
mockk(relaxed = true),
mockk(relaxed = true)
)
)
)
controller.displaySyncedTabs(tabs)
val itemCount = controller.adapter.itemCount
// title + device name + 2 tabs
assertEquals(4, itemCount)
assertEquals(
SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 1)
)
assertEquals(
SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 2)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 3)
)
assertEquals(
SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(itemCount - 4)
)
}
@Test
fun `show error when we go kaput`() {
controller.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION)
assertEquals(1, controller.adapter.itemCount)
assertEquals(
SyncedTabsViewHolder.ErrorViewHolder.LAYOUT_ID,
controller.adapter.getItemViewType(0)
)
}
}

View File

@ -53,6 +53,12 @@ class TabTrayFragmentInteractorTest {
verify { controller.onCloseAllTabsClicked(true) } verify { controller.onCloseAllTabsClicked(true) }
} }
@Test
fun onSyncedTabClicked() {
interactor.onSyncedTabClicked(mockk(relaxed = true))
verify { controller.onSyncedTabClicked(any()) }
}
@Test @Test
fun onBackPressed() { fun onBackPressed() {
interactor.onBackPressed() interactor.onBackPressed()