mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/12287: Add Synced Tabs to Tabs Tray
This commit is contained in:
parent
05389ad4e5
commit
73c7e9c3f7
@ -31,5 +31,6 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
|
||||
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
|
||||
FromAddonDetailsFragment(R.id.addonDetailsFragment),
|
||||
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
|
||||
FromLoginDetailFragment(R.id.loginDetailFragment)
|
||||
FromLoginDetailFragment(R.id.loginDetailFragment),
|
||||
FromTabTray(R.id.tabTrayDialogFragment)
|
||||
}
|
||||
|
@ -100,6 +100,7 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection
|
||||
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
|
||||
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
|
||||
import org.mozilla.fenix.theme.DefaultThemeManager
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.utils.BrowsersCache
|
||||
@ -597,6 +598,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
BrowserDirection.FromLoginDetailFragment ->
|
||||
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
BrowserDirection.FromTabTray ->
|
||||
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,14 +10,18 @@ import androidx.navigation.NavController
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
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.ErrorViewHolder
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.NoTabsViewHolder
|
||||
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.concept.sync.Device as SyncDevice
|
||||
|
||||
class SyncedTabsAdapter(
|
||||
private val listener: (SyncTab) -> Unit
|
||||
private val newListener: SyncedTabsView.Listener
|
||||
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
|
||||
@ -27,30 +31,26 @@ class SyncedTabsAdapter(
|
||||
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
|
||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
|
||||
ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView)
|
||||
TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView)
|
||||
NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Tab -> TabViewHolder.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>) {
|
||||
val allDeviceTabs = mutableListOf<AdapterItem>()
|
||||
|
||||
syncedTabs.forEach { (device, tabs) ->
|
||||
if (tabs.isNotEmpty()) {
|
||||
allDeviceTabs.add(AdapterItem.Device(device))
|
||||
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val allDeviceTabs = syncedTabs.toAdapterList()
|
||||
submitList(allDeviceTabs)
|
||||
}
|
||||
|
||||
@ -59,7 +59,11 @@ class SyncedTabsAdapter(
|
||||
when (oldItem) {
|
||||
is AdapterItem.Device ->
|
||||
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
|
||||
}
|
||||
|
||||
@ -68,9 +72,35 @@ class SyncedTabsAdapter(
|
||||
oldItem == newItem
|
||||
}
|
||||
|
||||
/**
|
||||
* The various types of adapter items that can be found in a [SyncedTabsAdapter].
|
||||
*/
|
||||
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()
|
||||
|
||||
/**
|
||||
* A tab that was synced.
|
||||
*/
|
||||
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(
|
||||
val descriptionResId: Int,
|
||||
val navController: NavController? = null
|
||||
|
@ -7,7 +7,6 @@ package org.mozilla.fenix.sync
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.findFragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@ -18,8 +17,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||
import mozilla.components.browser.storage.sync.Tab
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.sync.ext.toAdapterItem
|
||||
import org.mozilla.fenix.sync.ext.toStringRes
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class SyncedTabsLayout @JvmOverloads constructor(
|
||||
@ -30,7 +32,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||
|
||||
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)
|
||||
|
||||
init {
|
||||
@ -53,8 +55,8 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||
null
|
||||
}
|
||||
|
||||
val descriptionResId = stringResourceForError(error)
|
||||
val errorItem = getErrorItem(navController, error, descriptionResId)
|
||||
val descriptionResId = error.toStringRes()
|
||||
val errorItem = error.toAdapterItem(descriptionResId, navController)
|
||||
|
||||
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
|
||||
adapter.submitList(errorList)
|
||||
@ -96,27 +98,21 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
|
||||
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
|
||||
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
|
||||
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
|
||||
}
|
||||
|
||||
internal fun getErrorItem(
|
||||
navController: NavController?,
|
||||
error: SyncedTabsView.ErrorType,
|
||||
@StringRes stringResId: Int
|
||||
): SyncedTabsAdapter.AdapterItem = when (error) {
|
||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
|
||||
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We have to do this weird daisy-chaining of callbacks because the listener is nullable and
|
||||
* when we get a null reference, we never get a new binding to the non-null listener.
|
||||
*/
|
||||
class ListenerDelegate(
|
||||
private val listener: (() -> SyncedTabsView.Listener?)
|
||||
) : SyncedTabsView.Listener {
|
||||
override fun onRefresh() {
|
||||
listener.invoke()?.onRefresh()
|
||||
}
|
||||
|
||||
override fun onTabClicked(tab: Tab) {
|
||||
listener.invoke()?.onTabClicked(tab)
|
||||
}
|
||||
}
|
||||
|
@ -7,29 +7,36 @@ package org.mozilla.fenix.sync
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.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.feature.syncedtabs.view.SyncedTabsView
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
import org.mozilla.fenix.R
|
||||
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) {
|
||||
|
||||
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) {
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
|
||||
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
|
||||
setErrorMargins()
|
||||
|
||||
@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
val lp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
|
38
app/src/main/java/org/mozilla/fenix/sync/ext/ErrorType.kt
Normal file
38
app/src/main/java/org/mozilla/fenix/sync/ext/ErrorType.kt
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -9,10 +9,12 @@ import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.session.Session
|
||||
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.prompt.ShareData
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
@ -30,6 +32,7 @@ interface TabTrayController {
|
||||
fun onNewTabTapped(private: Boolean)
|
||||
fun onTabTrayDismissed()
|
||||
fun onShareTabsClicked(private: Boolean)
|
||||
fun onSyncedTabClicked(syncTab: SyncTab)
|
||||
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||
fun onCloseAllTabsClicked(private: Boolean)
|
||||
fun handleBackPressed(): Boolean
|
||||
@ -59,6 +62,7 @@ interface TabTrayController {
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class DefaultTabTrayController(
|
||||
private val activity: HomeActivity,
|
||||
private val profiler: Profiler?,
|
||||
private val sessionManager: SessionManager,
|
||||
private val browsingModeManager: BrowsingModeManager,
|
||||
@ -117,6 +121,14 @@ class DefaultTabTrayController(
|
||||
navController.navigate(directions)
|
||||
}
|
||||
|
||||
override fun onSyncedTabClicked(syncTab: SyncTab) {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = syncTab.active().url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromTabTray
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||
val sessionsToClose = if (private) {
|
||||
|
@ -177,6 +177,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
|
||||
adapter,
|
||||
interactor = TabTrayFragmentInteractor(
|
||||
DefaultTabTrayController(
|
||||
activity = activity,
|
||||
profiler = activity.components.core.engine.profiler,
|
||||
sessionManager = activity.components.core.sessionManager,
|
||||
browsingModeManager = activity.browsingModeManager,
|
||||
@ -194,7 +195,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
|
||||
isPrivate = isPrivate,
|
||||
startingInLandscape = requireContext().resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE,
|
||||
lifecycleScope = viewLifecycleOwner.lifecycleScope
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
) { private ->
|
||||
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.browser.storage.sync.Tab as SyncTab
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TabTrayInteractor {
|
||||
@ -33,6 +34,11 @@ interface TabTrayInteractor {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -89,6 +95,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
|
||||
controller.onCloseAllTabsClicked(private)
|
||||
}
|
||||
|
||||
override fun onSyncedTabClicked(syncTab: SyncTab) {
|
||||
controller.onSyncedTabClicked(syncTab)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return controller.handleBackPressed()
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
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.LinearLayoutManager
|
||||
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.privateTabs
|
||||
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.feature.syncedtabs.SyncedTabsFeature
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
@ -58,9 +62,10 @@ class TabTrayView(
|
||||
private val interactor: TabTrayInteractor,
|
||||
isPrivate: Boolean,
|
||||
startingInLandscape: Boolean,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
private val filterTabs: (Boolean) -> Unit
|
||||
) : LayoutContainer, TabLayout.OnTabSelectedListener {
|
||||
val lifecycleScope = lifecycleOwner.lifecycleScope
|
||||
val fabView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||
|
||||
@ -79,13 +84,18 @@ class TabTrayView(
|
||||
private var tabsTouchHelper: TabsTouchHelper
|
||||
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
|
||||
|
||||
private val syncedTabsController = SyncedTabsController(view)
|
||||
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
|
||||
|
||||
private var hasLoaded = false
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
private val components = container.context.components
|
||||
|
||||
init {
|
||||
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||
components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||
|
||||
toggleFabText(isPrivate)
|
||||
|
||||
@ -102,7 +112,7 @@ class TabTrayView(
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
container.context.components.analytics.metrics.track(Event.TabsTrayClosed)
|
||||
components.analytics.metrics.track(Event.TabsTrayClosed)
|
||||
interactor.onTabTrayDismissed()
|
||||
}
|
||||
}
|
||||
@ -135,8 +145,20 @@ class TabTrayView(
|
||||
|
||||
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 {
|
||||
layoutManager = LinearLayoutManager(container.context).apply {
|
||||
reverseLayout = true
|
||||
@ -156,6 +178,9 @@ class TabTrayView(
|
||||
// Put the 'Add to collections' button after the tabs have loaded.
|
||||
concatAdapter.addAdapter(0, collectionsButtonAdapter)
|
||||
|
||||
// Put the Synced Tabs adapter at the end.
|
||||
concatAdapter.addAdapter(0, syncedTabsController.adapter)
|
||||
|
||||
if (hasAccessibilityEnabled) {
|
||||
tabsAdapter.notifyDataSetChanged()
|
||||
}
|
||||
@ -193,7 +218,7 @@ class TabTrayView(
|
||||
}
|
||||
|
||||
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?.show(it)
|
||||
?.also { pu ->
|
||||
@ -209,6 +234,10 @@ class TabTrayView(
|
||||
adjustNewTabButtonsForNormalMode()
|
||||
}
|
||||
|
||||
private fun handleTabClicked(tab: SyncTab) {
|
||||
interactor.onSyncedTabClicked(tab)
|
||||
}
|
||||
|
||||
private fun adjustNewTabButtonsForNormalMode() {
|
||||
view.tab_tray_new_tab.apply {
|
||||
isVisible = hasAccessibilityEnabled
|
||||
@ -234,7 +263,7 @@ class TabTrayView(
|
||||
Event.NewTabTapped
|
||||
}
|
||||
|
||||
container.context.components.analytics.metrics.track(eventToSend)
|
||||
components.analytics.metrics.track(eventToSend)
|
||||
}
|
||||
|
||||
fun expand() {
|
||||
@ -261,17 +290,14 @@ class TabTrayView(
|
||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||
|
||||
if (isPrivateModeSelected) {
|
||||
container.context.components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
|
||||
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
|
||||
} else {
|
||||
container.context.components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
|
||||
components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
||||
}
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
|
||||
var mode: Mode = Mode.Normal
|
||||
private set
|
||||
@ -513,7 +539,9 @@ class TabTrayView(
|
||||
|
||||
// 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`.
|
||||
val recyclerViewIndex = selectedBrowserTabIndex + collectionsButtonAdapter.itemCount
|
||||
val recyclerViewIndex = selectedBrowserTabIndex +
|
||||
collectionsButtonAdapter.itemCount +
|
||||
syncedTabsController.adapter.itemCount
|
||||
|
||||
layoutManager?.scrollToPosition(recyclerViewIndex)
|
||||
}
|
||||
|
9
app/src/main/res/anim/full_rotation.xml
Normal file
9
app/src/main/res/anim/full_rotation.xml
Normal 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" />
|
@ -19,6 +19,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="14sp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/tab_tray_item_text_normal_theme"
|
||||
tools:text="@string/synced_tabs_no_tabs"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
|
9
app/src/main/res/layout/view_synced_tabs_no_item.xml
Normal file
9
app/src/main/res/layout/view_synced_tabs_no_item.xml
Normal 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>
|
29
app/src/main/res/layout/view_synced_tabs_title.xml
Normal file
29
app/src/main/res/layout/view_synced_tabs_title.xml
Normal 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>
|
@ -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()) }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ 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 mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
@ -21,7 +22,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SyncedTabsAdapterTest {
|
||||
|
||||
private lateinit var listener: (Tab) -> Unit
|
||||
private lateinit var listener: SyncedTabsView.Listener
|
||||
private lateinit var adapter: SyncedTabsAdapter
|
||||
|
||||
private val oneTabDevice = SyncedDeviceTabs(
|
||||
@ -77,10 +78,12 @@ class SyncedTabsAdapterTest {
|
||||
fun `updateData() adds items for each device and tab`() {
|
||||
assertEquals(0, adapter.itemCount)
|
||||
|
||||
adapter.updateData(listOf(
|
||||
oneTabDevice,
|
||||
threeTabDevice
|
||||
))
|
||||
adapter.updateData(
|
||||
listOf(
|
||||
oneTabDevice,
|
||||
threeTabDevice
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(5, adapter.itemCount)
|
||||
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0))
|
||||
|
@ -4,16 +4,10 @@
|
||||
|
||||
package org.mozilla.fenix.sync
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class SyncedTabsLayoutTest {
|
||||
|
||||
@ -25,73 +19,4 @@ class SyncedTabsLayoutTest {
|
||||
assertFalse(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.SYNC_NEEDS_REAUTHENTICATION))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -7,15 +7,18 @@ package org.mozilla.fenix.sync
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import io.mockk.Called
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
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_title.view.*
|
||||
import mozilla.components.browser.storage.sync.Tab
|
||||
import mozilla.components.browser.storage.sync.TabEntry
|
||||
import mozilla.components.concept.sync.Device
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
@ -32,6 +35,10 @@ class SyncedTabsViewHolderTest {
|
||||
private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder
|
||||
private lateinit var deviceView: View
|
||||
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(
|
||||
history = listOf(
|
||||
@ -59,6 +66,12 @@ class SyncedTabsViewHolderTest {
|
||||
every { synced_tabs_group_name } returns deviceViewGroupName
|
||||
}
|
||||
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
|
||||
@ -71,11 +84,11 @@ class SyncedTabsViewHolderTest {
|
||||
|
||||
@Test
|
||||
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)
|
||||
|
||||
tabView.performClick()
|
||||
verify { interactor(tab) }
|
||||
verify { interactor.onTabClicked(tab) }
|
||||
}
|
||||
|
||||
@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 }
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -26,6 +26,8 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
@ -34,6 +36,7 @@ import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultTabTrayControllerTest {
|
||||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val profiler: Profiler? = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk()
|
||||
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||
@ -81,6 +84,7 @@ class DefaultTabTrayControllerTest {
|
||||
every { tabCollection.title } returns "Collection title"
|
||||
|
||||
controller = DefaultTabTrayController(
|
||||
activity = activity,
|
||||
profiler = profiler,
|
||||
sessionManager = sessionManager,
|
||||
browsingModeManager = browsingModeManager,
|
||||
@ -156,6 +160,15 @@ class DefaultTabTrayControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSyncedTabClicked() {
|
||||
controller.onSyncedTabClicked(mockk(relaxed = true))
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleBackPressed() {
|
||||
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
@ -53,6 +53,12 @@ class TabTrayFragmentInteractorTest {
|
||||
verify { controller.onCloseAllTabsClicked(true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSyncedTabClicked() {
|
||||
interactor.onSyncedTabClicked(mockk(relaxed = true))
|
||||
verify { controller.onSyncedTabClicked(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackPressed() {
|
||||
interactor.onBackPressed()
|
||||
|
Loading…
Reference in New Issue
Block a user