diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt index d45c9e8c7..69ace066f 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -8,16 +8,15 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import mozilla.components.concept.sync.Device as SyncDevice -import mozilla.components.browser.storage.sync.Tab as SyncTab +import mozilla.components.browser.storage.sync.SyncedDeviceTabs import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder +import mozilla.components.browser.storage.sync.Tab as SyncTab +import mozilla.components.concept.sync.Device as SyncDevice class SyncedTabsAdapter( private val listener: (SyncTab) -> Unit -) : ListAdapter( - DiffCallback -) { +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false) @@ -30,23 +29,35 @@ class SyncedTabsAdapter( } override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { - val item = when (holder) { - is DeviceViewHolder -> getItem(position) as AdapterItem.Device - is TabViewHolder -> getItem(position) as AdapterItem.Tab - } - holder.bind(item, listener) + holder.bind(getItem(position), listener) } - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID - is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + override fun getItemViewType(position: Int) = when (getItem(position)) { + is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID + is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + } + + fun updateData(syncedTabs: List) { + val allDeviceTabs = mutableListOf() + + syncedTabs.forEach { (device, tabs) -> + if (tabs.isNotEmpty()) { + allDeviceTabs.add(AdapterItem.Device(device)) + tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) } + } } + + submitList(allDeviceTabs) } private object DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - areContentsTheSame(oldItem, newItem) + when (oldItem) { + is AdapterItem.Device -> + newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id + is AdapterItem.Tab -> + oldItem == newItem + } @Suppress("DiffUtilEquals") override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index c5bbf4d4e..0889ccb50 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -10,10 +10,10 @@ import android.view.View import android.widget.FrameLayout import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* -import kotlinx.coroutines.launch 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.view.SyncedTabsView import org.mozilla.fenix.R @@ -43,15 +43,7 @@ class SyncedTabsLayout @JvmOverloads constructor( // We may still be displaying a "loading" spinner, hide it. stopLoading() - val stringResId = when (error) { - SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device - SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account - SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth - SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs - } - - sync_tabs_status.text = context.getText(stringResId) + sync_tabs_status.text = context.getText(stringResourceForError(error)) synced_tabs_list.visibility = View.GONE sync_tabs_status.visibility = View.VISIBLE @@ -65,19 +57,7 @@ class SyncedTabsLayout @JvmOverloads constructor( synced_tabs_list.visibility = View.VISIBLE sync_tabs_status.visibility = View.GONE - val allDeviceTabs = emptyList().toMutableList() - - syncedTabs.forEach { (device, tabs) -> - if (tabs.isEmpty()) { - return@forEach - } - - val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) } - - allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs - } - - adapter.submitList(allDeviceTabs) + adapter.updateData(syncedTabs) } } @@ -110,5 +90,13 @@ 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_connect_to_sync_account + SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth + SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs + } } } diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsAdapterTest.kt new file mode 100644 index 000000000..e22bbb181 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsAdapterTest.kt @@ -0,0 +1,105 @@ +/* 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.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.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class SyncedTabsAdapterTest { + + private lateinit var listener: (Tab) -> Unit + 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`() { + 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) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt index 7b69743b9..9805c4445 100644 --- a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsLayoutTest.kt @@ -5,11 +5,14 @@ package org.mozilla.fenix.sync import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType -import org.junit.Assert.assertTrue +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test +import org.mozilla.fenix.R class SyncedTabsLayoutTest { + @Test fun `pull to refresh state`() { assertTrue(SyncedTabsLayout.pullToRefreshEnableState(ErrorType.MULTIPLE_DEVICES_UNAVAILABLE)) @@ -18,4 +21,28 @@ 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_connect_to_sync_account, + 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) + ) + } }