For #21900 - Delete files from Synced Tabs XML implementation

upstream-sync
Noah Bond 2 years ago committed by mergify[bot]
parent bf70616d93
commit 584b2cd83c

@ -1,109 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.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 newListener: SyncedTabsView.Listener
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
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), 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 = syncedTabs.toAdapterList()
submitList(allDeviceTabs)
}
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
when (oldItem) {
is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.NoTabs ->
newItem is AdapterItem.NoTabs && oldItem.device.id == newItem.device.id
is AdapterItem.Tab,
is AdapterItem.Error,
is AdapterItem.Title ->
oldItem == newItem
}
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
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
) : AdapterItem()
}
}

@ -1,77 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
/**
* Adds an [ItemDecoration] to the device name of each Synced Tab group.
*/
class SyncedTabsTitleDecoration(
context: Context,
private val style: Style = Style(
height = 1.dpToPx(context.resources.displayMetrics),
color = run {
val a = context.obtainStyledAttributes(intArrayOf(R.attr.toolbarDivider))
val color = a.getDrawable(0)!!
a.recycle()
color
}
)
) : ItemDecoration() {
/**
* A class for holding various customizations.
*/
data class Style(val height: Int, val color: Drawable)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val viewHolder = parent.getChildViewHolder(view)
val position = viewHolder.bindingAdapterPosition
val viewType = viewHolder.itemViewType
// Only add offsets on the device title that is not the first.
if (viewType == DeviceViewHolder.LAYOUT_ID && position != 0) {
outRect.set(0, style.height, 0, 0)
return
}
outRect.setEmpty()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val view = parent.getChildAt(i)
val viewHolder = parent.getChildViewHolder(view)
val position = viewHolder.bindingAdapterPosition
val viewType = viewHolder.itemViewType
// Only draw on the device title that is not the first.
if (viewType == DeviceViewHolder.LAYOUT_ID && position != 0) {
style.color.setBounds(
view.left,
view.top - style.height,
view.right,
view.top
)
style.color.draw(c)
}
}
}
}

@ -1,126 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.SyncTabsErrorRowBinding
import org.mozilla.fenix.databinding.SyncTabsListItemBinding
import org.mozilla.fenix.databinding.ViewSyncedTabsGroupBinding
import org.mozilla.fenix.databinding.ViewSyncedTabsTitleBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl
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: SyncedTabsView.Listener)
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindTab(item as AdapterItem.Tab)
itemView.setOnClickListener {
interactor.onTabClicked(item.tab)
}
}
private fun bindTab(tab: AdapterItem.Tab) {
val active = tab.tab.active()
val binding = SyncTabsListItemBinding.bind(itemView)
binding.syncedTabItemTitle.text = active.title
binding.syncedTabItemUrl.text = active.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
}
companion object {
const val LAYOUT_ID = R.layout.sync_tabs_list_item
}
}
class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
val errorItem = item as AdapterItem.Error
val binding = SyncTabsErrorRowBinding.bind(itemView)
binding.syncTabsErrorDescription.text =
itemView.context.getString(errorItem.descriptionResId)
binding.syncTabsErrorCtaButton.visibility = GONE
errorItem.navController?.let { navController ->
binding.syncTabsErrorCtaButton.visibility = VISIBLE
binding.syncTabsErrorCtaButton.setOnClickListener {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
}
}
}
companion object {
const val LAYOUT_ID = R.layout.sync_tabs_error_row
}
}
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
@VisibleForTesting
internal val binding = ViewSyncedTabsGroupBinding.bind(itemView)
override fun <T : AdapterItem> bind(item: T, interactor: SyncedTabsView.Listener) {
bindHeader(item as AdapterItem.Device)
}
private fun bindHeader(device: AdapterItem.Device) {
binding.syncedTabsGroupName.text = device.device.displayName
}
companion object {
const val LAYOUT_ID = R.layout.view_synced_tabs_group
}
}
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) {
val binding = ViewSyncedTabsTitleBinding.bind(itemView)
binding.refreshIcon.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
}
}
}

@ -1,40 +0,0 @@
/* 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)
}

@ -1,22 +0,0 @@
/* 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
/**
* Converts a list of [SyncedDeviceTabs] into a list of [AdapterItem].
*/
fun List<SyncedDeviceTabs>.toAdapterList() = asSequence().flatMap { (device, tabs) ->
val deviceTabs = if (tabs.isEmpty()) {
sequenceOf(AdapterItem.NoTabs(device))
} else {
tabs.asSequence().map { AdapterItem.Tab(it) }
}
sequenceOf(AdapterItem.Device(device)) + deviceTabs
}.toList()

@ -1,128 +0,0 @@
/* 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.tabstray.syncedtabs
import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.findFragment
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.databinding.ComponentSyncTabsTrayLayoutBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.sync.SyncedTabsTitleDecoration
import org.mozilla.fenix.sync.ext.toAdapterItem
import org.mozilla.fenix.sync.ext.toStringRes
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.utils.view.LifecycleViewProvider
class SyncedTabsTrayLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr),
SyncedTabsView,
Observable<SyncedTabsView.Listener> by ObserverRegistry() {
private val lifecycleProvider = LifecycleViewProvider(this)
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private var _binding: ComponentSyncTabsTrayLayoutBinding? = null
private val binding get() = _binding!!
private val syncedTabsFeature by lazy {
SyncedTabsFeature(
context = context,
storage = context.components.backgroundServices.syncedTabsStorage,
accountManager = context.components.backgroundServices.accountManager,
view = this,
lifecycleOwner = lifecycleProvider,
onTabClicked = {
// We can ignore this callback here because we're not connecting the adapter
// back to the feature. This works fine in other features, but passing the listener
// to other components in this case is annoying.
}
)
}
private val syncButtonBinding by lazy {
SyncButtonBinding(tabsTrayStore) {
listener?.onRefresh()
}
}
lateinit var tabsTrayStore: TabsTrayStore
override var listener: SyncedTabsView.Listener? = null
override fun onFinishInflate() {
_binding = ComponentSyncTabsTrayLayoutBinding.bind(this)
binding.syncedTabsList.addItemDecoration(SyncedTabsTitleDecoration(context))
super.onFinishInflate()
}
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
coroutineScope.launch {
(binding.syncedTabsList.adapter as SyncedTabsAdapter).updateData(syncedTabs)
}
}
override fun onError(error: SyncedTabsView.ErrorType) {
coroutineScope.launch {
// We may still be displaying a "loading" spinner, hide it.
stopLoading()
val navController: NavController? = try {
findFragment<TabsTrayFragment>().findNavController()
} catch (exception: IllegalStateException) {
null
}
val descriptionResId = error.toStringRes()
val errorItem = error.toAdapterItem(descriptionResId, navController)
val errorList: List<SyncedTabsAdapter.AdapterItem> = listOf(errorItem)
(binding.syncedTabsList.adapter as SyncedTabsAdapter).submitList(errorList)
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
syncedTabsFeature.start()
syncButtonBinding.start()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
_binding = null
syncedTabsFeature.stop()
syncButtonBinding.stop()
coroutineScope.cancel()
}
override fun stopLoading() {
tabsTrayStore.dispatch(TabsTrayAction.SyncCompleted)
}
/**
* Do nothing; the UI is handled with FloatingActionButtonBinding.
*/
override fun startLoading() = Unit
}

@ -1,22 +0,0 @@
/* 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.tabstray.syncedtabs
import mozilla.components.browser.storage.sync.Tab
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.tabstray.NavigationInteractor
/**
* A wrapper class that handles tab clicks from a Synced Tabs list.
*/
class TabClickDelegate(
private val interactor: NavigationInteractor
) : SyncedTabsView.Listener {
override fun onTabClicked(tab: Tab) {
interactor.onSyncedTabClicked(tab)
}
override fun onRefresh() = Unit
}

@ -1,12 +0,0 @@
<?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/. -->
<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" />

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsTrayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/synced_tabs_tray_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/synced_tabs_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/tab_tray_list_bottom_padding"
tools:listitem="@layout/sync_tabs_list_item"/>
</org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsTrayLayout>

@ -1,35 +0,0 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/empty_session_control_background"
android:layout_margin="@dimen/synced_tabs_error_margin"
android:padding="16dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/sync_tabs_error_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="viewStart"
android:textColor="@color/fx_mobile_text_color_primary"
tools:text="@string/synced_tabs_no_tabs"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/sync_tabs_error_cta_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
style="@style/PositiveButton"
app:icon="@drawable/ic_sign_in"
android:visibility="gone"
android:text="@string/synced_tabs_sign_in_button"
android:layout_marginTop="8dp"/>
</LinearLayout>

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/synced_tab_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="@color/fx_mobile_text_color_primary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Tab Title" />
<TextView
android:id="@+id/synced_tab_item_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="2dp"
android:singleLine="true"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp"
tools:text="https://example.com/"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/synced_tab_item_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/synced_tabs_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingTop="16dp"
android:paddingBottom="7dp">
<TextView
android:id="@+id/synced_tabs_group_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAppearance="@style/Header14TextStyle"
android:textColor="@color/fx_mobile_text_color_primary"
android:textSize="12sp"
android:letterSpacing="0.04"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Header" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,21 +0,0 @@
<?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="wrap_content"
android:paddingBottom="7dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="7dp"
android:singleLine="true"
android:text="@string/synced_tabs_no_open_tabs"
android:textAlignment="viewStart"
android:textColor="?secondaryText"
android:textSize="12sp" />
</FrameLayout>

@ -1,33 +0,0 @@
<?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:importantForAccessibility="no"
android:orientation="horizontal">
<TextView
style="@style/Header16TextStyle"
android:layout_width="0dp"
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="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="4dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/resync_button_content_description"
app:srcCompat="@drawable/mozac_ic_refresh"
app:tint="?primaryText" />
</LinearLayout>

@ -1,119 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.widget.FrameLayout
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 mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
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.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsAdapterTest {
private lateinit var listener: SyncedTabsView.Listener
private lateinit var adapter: SyncedTabsAdapter
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 threeTabDevice = 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
)
)
)
@Before
fun setup() {
listener = mockk(relaxed = true)
adapter = SyncedTabsAdapter(listener)
}
@Test
fun `updateData() adds items for each device and tab`() {
assertEquals(0, adapter.itemCount)
adapter.updateData(
listOf(
oneTabDevice,
threeTabDevice
)
)
assertEquals(5, adapter.itemCount)
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(0))
assertEquals(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, adapter.getItemViewType(1))
assertEquals(SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, adapter.getItemViewType(2))
assertEquals(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, adapter.getItemViewType(3))
assertEquals(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, adapter.getItemViewType(4))
}
@Test
fun `adapter can create and bind viewholders for SyncedDeviceTabs`() {
every { testContext.components.publicSuffixList } returns PublicSuffixList(testContext)
val parent = FrameLayout(testContext)
adapter.updateData(listOf(oneTabDevice))
val deviceHolder = adapter.createViewHolder(parent, SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID)
val tabHolder = adapter.createViewHolder(parent, SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID)
// Should not throw
adapter.bindViewHolder(deviceHolder, 0)
adapter.bindViewHolder(tabHolder, 1)
}
}

@ -1,101 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsTitleDecoration.Style
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder
class SyncedTabsTitleDecorationTest {
private val recyclerView: RecyclerView = mockk(relaxed = true)
private val canvas: Canvas = mockk(relaxed = true)
private val viewHolder: RecyclerView.ViewHolder = mockk(relaxed = true)
private val state: RecyclerView.State = mockk(relaxed = true)
private val view: View = mockk(relaxed = true)
// Mocking these classes so we don't have to use the (slow) Android test runner.
private val rect: Rect = mockk(relaxed = true)
private val colorDrawable: Drawable = mockk(relaxed = true)
private val style = Style(5, colorDrawable)
@Before
fun setup() {
every { recyclerView.getChildViewHolder(any()) }.returns(viewHolder)
every { recyclerView.childCount }.returns(1)
every { recyclerView.getChildAt(any()) }.returns(view)
every { view.left }.returns(5)
every { view.top }.returns(5)
every { view.right }.returns(5)
every { view.bottom }.returns(5)
}
@Test
fun `WHEN device title and not first item THEN add offset to the layout rect`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { DeviceViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }.answers { 1 }
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
verify { rect.set(0, 5, 0, 0) }
}
@Test
fun `WHEN not device title and first position THEN do not add offsets`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { ErrorViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }
.answers { 1 }
.andThenAnswer { 0 }
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
decoration.getItemOffsets(rect, mockk(), recyclerView, state)
verify(exactly = 2) { rect.setEmpty() }
}
@Test
fun `WHEN device title and not first THEN draw`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { DeviceViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }.answers { 1 }
decoration.onDraw(canvas, recyclerView, state)
verify { colorDrawable.setBounds(5, 0, 5, 5) }
verify { colorDrawable.draw(canvas) }
}
@Test
fun `WHEN not device title and not first THEN do not draw`() {
val decoration = SyncedTabsTitleDecoration(mockk(), style)
every { viewHolder.itemViewType }.answers { ErrorViewHolder.LAYOUT_ID }
every { viewHolder.bindingAdapterPosition }
.answers { 1 }
.andThenAnswer { 0 }
decoration.onDraw(canvas, recyclerView, state)
verify { colorDrawable wasNot Called }
}
}

@ -1,147 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.sync
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
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.lib.publicsuffixlist.PublicSuffixList
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.databinding.SyncTabsListItemBinding
import org.mozilla.fenix.databinding.ViewSyncedTabsGroupBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class SyncedTabsViewHolderTest {
private lateinit var tabViewHolder: SyncedTabsViewHolder.TabViewHolder
private lateinit var tabView: View
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 lateinit var syncTabsListItemBinding: SyncTabsListItemBinding
private val tab = Tab(
history = listOf(
mockk(),
TabEntry(
title = "Firefox",
url = "https://mozilla.org/mobile",
iconUrl = "https://mozilla.org/favicon.ico"
),
mockk()
),
active = 1,
lastUsed = 0L
)
@Before
fun setup() {
val inflater = LayoutInflater.from(testContext)
tabView = inflater.inflate(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, null)
tabViewHolder = SyncedTabsViewHolder.TabViewHolder(tabView)
syncTabsListItemBinding = SyncTabsListItemBinding.bind(tabView)
val viewSyncedTabsGroupBinding = ViewSyncedTabsGroupBinding.inflate(inflater)
deviceView = mockk()
deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(spyk(viewSyncedTabsGroupBinding.root))
deviceViewGroupName = spyk(viewSyncedTabsGroupBinding.syncedTabsGroupName)
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
fun `TabViewHolder binds active tab`() {
every { testContext.components.publicSuffixList } returns PublicSuffixList(testContext)
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), mockk())
assertEquals("Firefox", syncTabsListItemBinding.syncedTabItemTitle.text)
assertEquals("mozilla.org", syncTabsListItemBinding.syncedTabItemUrl.text)
}
@Test
fun `TabViewHolder calls interactor on click`() {
every { testContext.components.publicSuffixList } returns PublicSuffixList(testContext)
val interactor = mockk<SyncedTabsView.Listener>(relaxed = true)
tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor)
tabView.performClick()
verify { interactor.onTabClicked(tab) }
}
@Test
fun `DeviceViewHolder binds desktop device`() {
val device = mockk<Device> {
every { displayName } returns "Charcoal"
every { deviceType } returns DeviceType.DESKTOP
}
deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk())
assertEquals("Charcoal", deviceViewHolder.binding.syncedTabsGroupName.text)
}
@Test
fun `DeviceViewHolder binds mobile device`() {
val device = mockk<Device> {
every { displayName } returns "Emerald"
every { deviceType } returns DeviceType.MOBILE
}
deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk())
assertEquals("Emerald", deviceViewHolder.binding.syncedTabsGroupName.text)
}
@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 }
}
}

@ -1,76 +0,0 @@
/* 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 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)
}
}

@ -4,7 +4,7 @@
package org.mozilla.fenix.tabstray.browser
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
import androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags
@ -28,8 +28,8 @@ class TabsTouchHelperTest {
@Test
fun `movement flags remain unchanged if onSwipeToDelete is true`() {
val recyclerView = RecyclerView(testContext)
val layout = FrameLayout(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk())
val layout = ComposeView(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk(), mockk())
val callback = TouchCallback(mockk(), { true }, { false }, featureName)
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))
@ -47,8 +47,8 @@ class TabsTouchHelperTest {
@Test
fun `movement flags remain unchanged if onSwipeToDelete is false`() {
val recyclerView = RecyclerView(testContext)
val layout = FrameLayout(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk())
val layout = ComposeView(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk(), mockk())
val callback = TouchCallback(mockk(), { false }, { false }, featureName)
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))

@ -1,36 +0,0 @@
/* 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.tabstray.syncedtabs
import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.storage.sync.Tab
import org.junit.Test
import org.mozilla.fenix.tabstray.NavigationInteractor
class TabClickDelegateTest {
private val interactor = mockk<NavigationInteractor>(relaxed = true)
private val tab = mockk<Tab>()
@Test
fun `WHEN tab is clicked THEN invoke the interactor`() {
val delegate = TabClickDelegate(interactor)
delegate.onTabClicked(tab)
verify { interactor.onSyncedTabClicked(tab) }
}
@Test
fun `WHEN refresh is invoked THEN do nothing`() {
val delegate = TabClickDelegate(interactor)
delegate.onRefresh()
verify { interactor wasNot Called }
}
}
Loading…
Cancel
Save