For #12287: Add Synced Tabs to Tabs Tray
parent
2e62dd5c87
commit
f614c0b18d
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
@ -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>
|
@ -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()) }
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue