For #18515 - Added Icon and sync functionality

Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>
upstream-sync
codrut.topliceanu 3 years ago committed by Jonathan Almeida
parent d961d7ba38
commit 9219a1b35b

@ -0,0 +1,93 @@
/* 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
import androidx.appcompat.content.res.AppCompatResources
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
class FloatingActionButtonBinding(
private val store: TabsTrayStore,
private val actionButton: ExtendedFloatingActionButton,
private val browserTrayInteractor: BrowserTrayInteractor,
private val syncedTabsInteractor: SyncedTabsInteractor
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
setFab(store.state.selectedPage, store.state.syncing)
scope = store.flowScoped { flow ->
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setFab(state.selectedPage, state.syncing)
}
}
}
override fun stop() {
scope?.cancel()
}
private fun setFab(selectedPage: Page, syncing: Boolean) {
when (selectedPage) {
Page.NormalTabs -> {
actionButton.apply {
shrink()
show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_new)
setOnClickListener {
browserTrayInteractor.onFabClicked(false)
}
}
}
Page.PrivateTabs -> {
actionButton.apply {
text = context.getText(R.string.tab_drawer_fab_content)
extend()
show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_new)
setOnClickListener {
browserTrayInteractor.onFabClicked(true)
}
}
}
Page.SyncedTabs -> {
actionButton.apply {
text = if (syncing) context.getText(R.string.sync_syncing_in_progress)
else context.getText(R.string.tab_drawer_fab_sync)
extend()
show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_fab_sync)
setOnClickListener {
// Notify the store observers (one of which is the SyncedTabsFeature), that
// a sync was requested.
if (!syncing) {
store.dispatch(TabsTrayAction.SyncNow)
syncedTabsInteractor.onRefresh()
}
}
}
}
}
}
}

@ -18,11 +18,12 @@ import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_PRIVATE_TA
*/
class TabLayoutMediator(
private val tabLayout: TabLayout,
private val interactor: TabsTrayInteractor,
private val store: BrowserStore
interactor: TabsTrayInteractor,
private val browserStore: BrowserStore,
trayStore: TabsTrayStore
) : LifecycleAwareFeature {
private val observer = TabLayoutObserver(interactor)
private val observer = TabLayoutObserver(interactor, trayStore)
/**
* Start observing the [TabLayout] and select the current tab for initial state.
@ -39,7 +40,7 @@ class TabLayoutMediator(
@VisibleForTesting
internal fun selectActivePage() {
val selectedTab = store.state.selectedTab ?: return
val selectedTab = browserStore.state.selectedTab ?: return
val selectedPagerPosition = if (selectedTab.content.private) {
POSITION_PRIVATE_TABS
@ -55,7 +56,8 @@ class TabLayoutMediator(
* An observer for the [TabLayout] used for the Tabs Tray.
*/
internal class TabLayoutObserver(
private val interactor: TabsTrayInteractor
private val interactor: TabsTrayInteractor,
private val trayStore: TabsTrayStore
) : TabLayout.OnTabSelectedListener {
private var initialScroll = true
@ -70,8 +72,16 @@ internal class TabLayoutObserver(
}
interactor.setCurrentTrayPosition(tab.position, animate)
trayStore.dispatch(TabsTrayAction.PageSelected(tab.toPage()))
}
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabReselected(tab: TabLayout.Tab) = Unit
}
fun TabLayout.Tab.toPage() = when (this.position) {
0 -> Page.NormalTabs
1 -> Page.PrivateTabs
else -> Page.SyncedTabs
}

@ -5,8 +5,15 @@
package org.mozilla.fenix.tabstray
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.service.fxa.sync.SyncReason
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
interface TabsTrayController {
@ -15,15 +22,48 @@ interface TabsTrayController {
* Called when user clicks the new tab button.
*/
fun onNewTabTapped(isPrivate: Boolean)
/**
* Starts user account tab syncing.
* */
fun onSyncStarted()
}
class DefaultTabsTrayController(
private val store: TabsTrayStore,
private val browsingModeManager: BrowsingModeManager,
private val navController: NavController
private val navController: NavController,
private val profiler: Profiler?,
private val dismissTabTray: () -> Unit,
private val metrics: MetricController,
private val ioScope: CoroutineScope,
private val accountManager: FxaAccountManager
) : TabsTrayController {
override fun onNewTabTapped(isPrivate: Boolean) {
val startTime = profiler?.getProfilerTime()
browsingModeManager.mode = BrowsingMode.fromBoolean(isPrivate)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
dismissTabTray()
profiler?.addMarker(
"DefaultTabTrayController.onNewTabTapped",
startTime
)
}
override fun onSyncStarted() {
ioScope.launch {
metrics.track(Event.SyncAccountSyncNow)
// Trigger a sync.
accountManager.syncNow(SyncReason.User)
// Poll for device events & update devices.
accountManager.authenticatedAccount()
?.deviceConstellation()?.run {
refreshDevices()
pollForCommands()
}
}.invokeOnCompletion {
store.dispatch(TabsTrayAction.SyncCompleted)
}
}
}

@ -12,25 +12,24 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior
import kotlinx.android.synthetic.main.component_tabstray2.*
import kotlinx.android.synthetic.main.component_tabstray2.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter2.*
import kotlinx.android.synthetic.main.component_tabstray2.tab_layout
import kotlinx.android.synthetic.main.component_tabstray2.tabsTray
import kotlinx.android.synthetic.main.component_tabstray2.view.tab_wrapper
import kotlinx.android.synthetic.main.component_tabstray_fab.view.new_tab_button
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.plus
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
@ -42,10 +41,10 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private lateinit var browserTrayInteractor: BrowserTrayInteractor
private lateinit var tabsTrayController: DefaultTabsTrayController
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
private var hasAccessibilityEnabled: Boolean = false
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
private val tabCounterBinding = ViewBoundFeatureWrapper<TabCounterBinding>()
private val floatingActionButtonBinding = ViewBoundFeatureWrapper<FloatingActionButtonBinding>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -78,14 +77,19 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = activity as HomeActivity
hasAccessibilityEnabled = activity.settings().accessibilityServicesEnabled
tabsTrayController = DefaultTabsTrayController(
store = tabsTrayStore,
browsingModeManager = activity.browsingModeManager,
navController = findNavController()
navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss,
profiler = requireComponents.core.engine.profiler,
accountManager = requireComponents.backgroundServices.accountManager,
metrics = requireComponents.analytics.metrics,
ioScope = lifecycleScope + Dispatchers.IO
)
val browserTrayInteractor = DefaultBrowserTrayInteractor(
browserTrayInteractor = DefaultBrowserTrayInteractor(
tabsTrayStore,
this@TabsTrayFragment,
tabsTrayController,
@ -106,7 +110,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
val syncedTabsTrayInteractor = SyncedTabsInteractor(
requireComponents.analytics.metrics,
requireActivity() as HomeActivity,
this
this,
controller = tabsTrayController
)
setupMenu(view, navigationInteractor)
@ -122,7 +127,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
feature = TabLayoutMediator(
tabLayout = tab_layout,
interactor = this,
store = requireComponents.core.store
browserStore = requireComponents.core.store,
trayStore = tabsTrayStore
), owner = this,
view = view
)
@ -135,11 +141,21 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
owner = this,
view = view
)
floatingActionButtonBinding.set(
feature = FloatingActionButtonBinding(
store = tabsTrayStore,
actionButton = new_tab_button,
browserTrayInteractor = browserTrayInteractor,
syncedTabsInteractor = syncedTabsTrayInteractor
),
owner = this,
view = view
)
}
override fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) {
tabsTray.setCurrentItem(position, smoothScroll)
setupNewTabButtons(tabsTray.currentItem)
}
override fun navigateToBrowser() {
@ -209,42 +225,4 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
findNavController().navigate(directions)
dismissAllowingStateLoss()
}
private fun setupNewTabButtons(currentPage: Int) {
fabView?.let { fabView ->
when (currentPage) {
NORMAL -> {
fabView.new_tab_button.shrink()
fabView.new_tab_button.show()
fabView.new_tab_button.setOnClickListener {
browserTrayInteractor.onFabClicked(false)
}
}
PRIVATE -> {
fabView.new_tab_button.text =
requireContext().resources.getText(R.string.tab_drawer_fab_content)
fabView.new_tab_button.extend()
fabView.new_tab_button.show()
fabView.new_tab_button.setOnClickListener {
browserTrayInteractor.onFabClicked(true)
}
}
SYNC -> {
fabView.new_tab_button.text =
requireContext().resources.getText(R.string.preferences_sync_now)
fabView.new_tab_button.extend()
fabView.new_tab_button.show()
fabView.new_tab_button.setOnClickListener {
}
}
}
}
}
companion object {
// TabsTray Pages
const val NORMAL = 0
const val PRIVATE = 1
const val SYNC = 2
}
}

@ -10,14 +10,18 @@ import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.tabstray.TabsTrayController
import org.mozilla.fenix.tabstray.TabsTrayInteractor
internal class SyncedTabsInteractor(
class SyncedTabsInteractor(
private val metrics: MetricController,
private val activity: HomeActivity,
private val trayInteractor: TabsTrayInteractor
private val trayInteractor: TabsTrayInteractor,
private val controller: TabsTrayController
) : SyncedTabsView.Listener {
override fun onRefresh() = Unit
override fun onRefresh() {
controller.onSyncStarted()
}
override fun onTabClicked(tab: Tab) {
metrics.track(Event.SyncedTabOpened)
activity.openToBrowserAndLoad(

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<group
android:pivotX="7"
android:pivotY="7"
android:scaleX="0.8"
android:scaleY="0.8">
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M12,1C12,0.4477 12.4477,0 13,0C13.5523,0 14,0.4477 14,1V5C14,5.5523 13.5523,6 13,6H9C8.4477,6 8,5.5523 8,5C8,4.4477 8.4477,4 9,4H10.967C10.0382,2.7401 8.5652,1.9976 7,2C4.7088,2.0026 2.7146,3.5672 2.167,5.792C2.0256,6.319 1.4879,6.6353 0.9586,6.503C0.4293,6.3707 0.1038,5.8385 0.227,5.307C0.8267,2.8397 2.7245,0.896 5.1768,0.2376C7.629,-0.4208 10.2451,0.3109 12,2.146V1ZM11.833,8.208C11.9668,7.6724 12.5093,7.3465 13.045,7.48C13.3024,7.5442 13.5238,7.7081 13.6603,7.9356C13.7969,8.1632 13.8374,8.4356 13.773,8.693C13.1733,11.1603 11.2755,13.104 8.8232,13.7624C6.371,14.4208 3.7548,13.6891 2,11.854V13C2,13.5523 1.5523,14 1,14C0.4477,14 0,13.5523 0,13V9C0,8.4477 0.4477,8 1,8H5C5.5523,8 6,8.4477 6,9C6,9.5523 5.5523,10 5,10H3.033C3.9618,11.2598 5.4348,12.0025 7,12C9.2912,11.9974 11.2854,10.4328 11.833,8.208Z" />
</group>
</vector>

@ -598,6 +598,8 @@
<string name="add_private_tab">Add private tab</string>
<!-- Text for the new tab button to indicate adding a new private tab in the tab -->
<string name="tab_drawer_fab_content">Private</string>
<!-- Text for the new tab button to indicate syncing command on the synced tabs page -->
<string name="tab_drawer_fab_sync">Sync</string>
<!-- Text shown as the title of the open tab tray -->
<string name="tab_tray_title">Open Tabs</string>
<!-- Text shown in the menu for saving tabs to a collection -->

@ -23,10 +23,10 @@ class TabLayoutMediatorTest {
@Test
fun `page to normal tab position when selected tab is also normal`() {
val store = createState("123")
val store = createStore("123")
val tabLayout: TabLayout = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk())
every { tabLayout.getTabAt(POSITION_NORMAL_TABS) }.answers { tab }
@ -37,10 +37,10 @@ class TabLayoutMediatorTest {
@Test
fun `page to private tab position when selected tab is also private`() {
val store = createState("456")
val store = createStore("456")
val tabLayout: TabLayout = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk())
every { tabLayout.getTabAt(POSITION_PRIVATE_TABS) }.answers { tab }
@ -51,9 +51,9 @@ class TabLayoutMediatorTest {
@Test
fun `lifecycle methods adds and removes observer`() {
val store = createState("456")
val store = createStore("456")
val tabLayout: TabLayout = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk())
mediator.start()
@ -64,7 +64,7 @@ class TabLayoutMediatorTest {
verify { tabLayout.removeOnTabSelectedListener(any()) }
}
private fun createState(selectedId: String) = BrowserStore(
private fun createStore(selectedId: String) = BrowserStore(
initialState = BrowserState(
tabs = listOf(
TabSessionState(

@ -8,25 +8,43 @@ import com.google.android.material.tabs.TabLayout
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class TabLayoutObserverTest {
private val interactor = mockk<TabsTrayInteractor>(relaxed = true)
private lateinit var store: TabsTrayStore
private val middleware = CaptureActionsMiddleware<TabsTrayState, TabsTrayAction>()
@Before
fun setup() {
store = TabsTrayStore(middlewares = listOf(middleware))
}
@Test
fun `WHEN tab is selected THEN notify the interactor`() {
val observer = TabLayoutObserver(interactor)
val observer = TabLayoutObserver(interactor, store)
val tab = mockk<TabLayout.Tab>()
every { tab.position } returns 1
observer.onTabSelected(tab)
store.waitUntilIdle()
verify { interactor.setCurrentTrayPosition(1, false) }
middleware.assertLastAction(TabsTrayAction.PageSelected::class) {
assertTrue(it.page == Page.PrivateTabs)
}
}
@Test
fun `WHEN observer is first started THEN do not smooth scroll`() {
val observer = TabLayoutObserver(interactor)
val store = TabsTrayStore()
val observer = TabLayoutObserver(interactor, store)
val tab = mockk<TabLayout.Tab>()
every { tab.position } returns 1

@ -32,7 +32,8 @@ class DefaultBrowserTrayInteractorTest {
@Test
fun `WHEN pager position is synced tabs THEN return a list layout manager`() {
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk())
val interactor =
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk(), mockk())
val result = interactor.getLayoutManagerForPosition(
mockk(),
@ -46,7 +47,8 @@ class DefaultBrowserTrayInteractorTest {
fun `WHEN setting is grid view THEN return grid layout manager`() {
val context = mockk<Context>()
val settings = mockk<Settings>()
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
val interactor =
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
every { context.numberOfGridColumns }.answers { 4 }
every { settings.gridTabView }.answers { true }
@ -63,7 +65,8 @@ class DefaultBrowserTrayInteractorTest {
fun `WHEN setting is list view THEN return list layout manager`() {
val context = mockk<Context>()
val settings = mockk<Settings>()
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
val interactor =
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
every { context.numberOfGridColumns }.answers { 4 }
every { settings.gridTabView }.answers { false }

Loading…
Cancel
Save