2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-19 09:25:34 +00:00
This commit is contained in:
Roger Yang 2021-04-22 12:48:10 -04:00 committed by GitHub
parent d94dcd6bd3
commit fffa196da9
6 changed files with 454 additions and 9 deletions

View File

@ -0,0 +1,104 @@
/* 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 android.view.View
import android.widget.ImageButton
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
import org.mozilla.fenix.utils.Settings
/**
* Do not show accessible new tab button when accessibility service is disabled
*
* This binding is coupled with [FloatingActionButtonBinding].
* When [FloatingActionButtonBinding] is visible this should not be visible
*/
class AccessibleNewTabButtonBinding(
private val store: TabsTrayStore,
private val settings: Settings,
private val newTabButton: ImageButton,
private val browserTrayInteractor: BrowserTrayInteractor,
private val syncedTabsInteractor: SyncedTabsInteractor
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
override fun start() {
if (!settings.accessibilityServicesEnabled) {
newTabButton.visibility = View.GONE
return
}
scope = store.flowScoped { flow ->
flow.map { it }
.ifAnyChanged { state ->
arrayOf(
state.selectedPage,
state.syncing
)
}
.collect { state ->
setAccessibleNewTabButton(state.selectedPage, state.syncing)
}
}
}
override fun stop() {
scope?.cancel()
}
private fun setAccessibleNewTabButton(selectedPage: Page, syncing: Boolean) {
when (selectedPage) {
Page.NormalTabs -> {
newTabButton.apply {
visibility = View.VISIBLE
setImageResource(R.drawable.ic_new)
setOnClickListener {
browserTrayInteractor.onFabClicked(false)
}
}
}
Page.PrivateTabs -> {
newTabButton.apply {
visibility = View.VISIBLE
setImageResource(R.drawable.ic_new)
setOnClickListener {
browserTrayInteractor.onFabClicked(true)
}
}
}
Page.SyncedTabs -> {
newTabButton.apply {
visibility =
when (syncing) {
true -> View.GONE
false -> View.VISIBLE
}
setImageResource(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()
}
}
}
}
}
}
}

View File

@ -4,7 +4,6 @@
package org.mozilla.fenix.tabstray package org.mozilla.fenix.tabstray
import androidx.appcompat.content.res.AppCompatResources
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -17,9 +16,17 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
import org.mozilla.fenix.utils.Settings
/**
* Do not show fab when accessibility service is enabled
*
* This binding is coupled with [AccessibleNewTabButtonBinding].
* When [AccessibleNewTabButtonBinding] is visible this should not be visible
*/
class FloatingActionButtonBinding( class FloatingActionButtonBinding(
private val store: TabsTrayStore, private val store: TabsTrayStore,
private val settings: Settings,
private val actionButton: ExtendedFloatingActionButton, private val actionButton: ExtendedFloatingActionButton,
private val browserTrayInteractor: BrowserTrayInteractor, private val browserTrayInteractor: BrowserTrayInteractor,
private val syncedTabsInteractor: SyncedTabsInteractor private val syncedTabsInteractor: SyncedTabsInteractor
@ -29,7 +36,11 @@ class FloatingActionButtonBinding(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun start() { override fun start() {
setFab(store.state.selectedPage, store.state.syncing) if (settings.accessibilityServicesEnabled) {
actionButton.hide()
return
}
scope = store.flowScoped { flow -> scope = store.flowScoped { flow ->
flow.map { it } flow.map { it }
.ifAnyChanged { state -> .ifAnyChanged { state ->
@ -54,7 +65,7 @@ class FloatingActionButtonBinding(
actionButton.apply { actionButton.apply {
shrink() shrink()
show() show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_new) setIconResource(R.drawable.ic_new)
setOnClickListener { setOnClickListener {
browserTrayInteractor.onFabClicked(false) browserTrayInteractor.onFabClicked(false)
} }
@ -62,10 +73,10 @@ class FloatingActionButtonBinding(
} }
Page.PrivateTabs -> { Page.PrivateTabs -> {
actionButton.apply { actionButton.apply {
text = context.getText(R.string.tab_drawer_fab_content) setText(R.string.tab_drawer_fab_content)
extend() extend()
show() show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_new) setIconResource(R.drawable.ic_new)
setOnClickListener { setOnClickListener {
browserTrayInteractor.onFabClicked(true) browserTrayInteractor.onFabClicked(true)
} }
@ -73,11 +84,15 @@ class FloatingActionButtonBinding(
} }
Page.SyncedTabs -> { Page.SyncedTabs -> {
actionButton.apply { actionButton.apply {
text = if (syncing) context.getText(R.string.sync_syncing_in_progress) setText(
else context.getText(R.string.tab_drawer_fab_sync) when (syncing) {
true -> R.string.sync_syncing_in_progress
false -> R.string.tab_drawer_fab_sync
}
)
extend() extend()
show() show()
icon = AppCompatResources.getDrawable(context, R.drawable.ic_fab_sync) setIconResource(R.drawable.ic_fab_sync)
setOnClickListener { setOnClickListener {
// Notify the store observers (one of which is the SyncedTabsFeature), that // Notify the store observers (one of which is the SyncedTabsFeature), that
// a sync was requested. // a sync was requested.

View File

@ -12,6 +12,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -68,6 +69,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>() private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
private val tabCounterBinding = ViewBoundFeatureWrapper<TabCounterBinding>() private val tabCounterBinding = ViewBoundFeatureWrapper<TabCounterBinding>()
private val floatingActionButtonBinding = ViewBoundFeatureWrapper<FloatingActionButtonBinding>() private val floatingActionButtonBinding = ViewBoundFeatureWrapper<FloatingActionButtonBinding>()
private val newTabButtonBinding = ViewBoundFeatureWrapper<AccessibleNewTabButtonBinding>()
private val selectionBannerBinding = ViewBoundFeatureWrapper<SelectionBannerBinding>() private val selectionBannerBinding = ViewBoundFeatureWrapper<SelectionBannerBinding>()
private val selectionHandleBinding = ViewBoundFeatureWrapper<SelectionHandleBinding>() private val selectionHandleBinding = ViewBoundFeatureWrapper<SelectionHandleBinding>()
private val tabsTrayCtaBinding = ViewBoundFeatureWrapper<TabsTrayInfoBannerBinding>() private val tabsTrayCtaBinding = ViewBoundFeatureWrapper<TabsTrayInfoBannerBinding>()
@ -211,6 +213,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
floatingActionButtonBinding.set( floatingActionButtonBinding.set(
feature = FloatingActionButtonBinding( feature = FloatingActionButtonBinding(
store = tabsTrayStore, store = tabsTrayStore,
settings = requireComponents.settings,
actionButton = new_tab_button, actionButton = new_tab_button,
browserTrayInteractor = browserTrayInteractor, browserTrayInteractor = browserTrayInteractor,
syncedTabsInteractor = syncedTabsTrayInteractor syncedTabsInteractor = syncedTabsTrayInteractor
@ -219,6 +222,18 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
view = view view = view
) )
newTabButtonBinding.set(
feature = AccessibleNewTabButtonBinding(
store = tabsTrayStore,
settings = requireComponents.settings,
newTabButton = tab_tray_new_tab,
browserTrayInteractor = browserTrayInteractor,
syncedTabsInteractor = syncedTabsTrayInteractor
),
owner = this,
view = view
)
selectionBannerBinding.set( selectionBannerBinding.set(
feature = SelectionBannerBinding( feature = SelectionBannerBinding(
context = requireContext(), context = requireContext(),
@ -312,7 +327,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}, },
operation = { }, operation = { },
elevation = ELEVATION, elevation = ELEVATION,
anchorView = new_tab_button anchorView = if (new_tab_button.isVisible) new_tab_button else null
) )
} }

View File

@ -17,6 +17,7 @@
android:elevation="99dp" android:elevation="99dp"
android:text="@string/tab_drawer_fab_content" android:text="@string/tab_drawer_fab_content"
android:textColor="@color/photonWhite" android:textColor="@color/photonWhite"
android:visibility="gone"
app:elevation="99dp" app:elevation="99dp"
app:borderWidth="0dp" app:borderWidth="0dp"
app:icon="@drawable/ic_new" app:icon="@drawable/ic_new"

View File

@ -0,0 +1,140 @@
/* 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 android.view.View
import android.widget.ImageButton
import androidx.appcompat.content.res.AppCompatResources
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
import org.mozilla.fenix.utils.Settings
class AccessibleNewTabButtonBindingTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
private val settings: Settings = mockk(relaxed = true)
private val newTabButton: ImageButton = mockk(relaxed = true)
private val browserTrayInteractor: BrowserTrayInteractor = mockk(relaxed = true)
private val syncedTabsInteractor: SyncedTabsInteractor = mockk(relaxed = true)
@Before
fun setup() {
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
}
@Test
fun `WHEN tab selected page is normal tab THEN new tab button is visible`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
val newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns true
newTabButtonBinding.start()
verify(exactly = 1) { newTabButton.visibility = View.VISIBLE }
}
@Test
fun `WHEN tab selected page is private tab THEN new tab button is visible`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.PrivateTabs))
val newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns true
newTabButtonBinding.start()
verify(exactly = 1) { newTabButton.visibility = View.VISIBLE }
}
@Test
fun `WHEN tab selected page is sync tab THEN new tab button is visible`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.SyncedTabs))
val newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns true
newTabButtonBinding.start()
verify(exactly = 1) { newTabButton.visibility = View.VISIBLE }
}
@Test
fun `WHEN accessibility is disabled THEN new tab button is not visible`() {
var tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
var newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns false
newTabButtonBinding.start()
verify(exactly = 1) { newTabButton.visibility = View.GONE }
tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.PrivateTabs))
newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
newTabButtonBinding.start()
verify(exactly = 2) { newTabButton.visibility = View.GONE }
tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.SyncedTabs))
newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
newTabButtonBinding.start()
verify(exactly = 3) { newTabButton.visibility = View.GONE }
}
@Test
fun `WHEN selected page is updated THEN button is updated`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
val newTabButtonBinding = AccessibleNewTabButtonBinding(
tabsTrayStore, settings, newTabButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns true
newTabButtonBinding.start()
verify(exactly = 1) { newTabButton.setImageResource(R.drawable.ic_new) }
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(Page.PrivateTabs.ordinal)))
tabsTrayStore.waitUntilIdle()
verify(exactly = 2) { newTabButton.setImageResource(R.drawable.ic_new) }
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(Page.SyncedTabs.ordinal)))
tabsTrayStore.waitUntilIdle()
verify(exactly = 1) { newTabButton.setImageResource(R.drawable.ic_fab_sync) }
tabsTrayStore.dispatch(TabsTrayAction.SyncNow)
tabsTrayStore.waitUntilIdle()
verify(exactly = 1) { newTabButton.visibility = View.GONE }
}
}

View File

@ -0,0 +1,170 @@
/* 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 io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
import org.mozilla.fenix.utils.Settings
class FloatingActionButtonBindingTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
private val settings: Settings = mockk(relaxed = true)
private val actionButton: ExtendedFloatingActionButton = mockk(relaxed = true)
private val browserTrayInteractor: BrowserTrayInteractor = mockk(relaxed = true)
private val syncedTabsInteractor: SyncedTabsInteractor = mockk(relaxed = true)
@Before
fun setup() {
mockkStatic(AppCompatResources::class)
every { AppCompatResources.getDrawable(any(), any()) } returns mockk(relaxed = true)
}
@Test
fun `WHEN tab selected page is normal tab THEN shrink and show is called`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
val fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns false
fabBinding.start()
verify(exactly = 1) { actionButton.shrink() }
verify(exactly = 1) { actionButton.show() }
verify(exactly = 0) { actionButton.extend() }
verify(exactly = 0) { actionButton.hide() }
}
@Test
fun `WHEN tab selected page is private tab THEN extend and show is called`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.PrivateTabs))
val fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns false
fabBinding.start()
verify(exactly = 1) { actionButton.extend() }
verify(exactly = 1) { actionButton.show() }
verify(exactly = 0) { actionButton.shrink() }
verify(exactly = 0) { actionButton.hide() }
}
@Test
fun `WHEN tab selected page is sync tab THEN extend and show is called`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.SyncedTabs))
val fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns false
fabBinding.start()
verify(exactly = 1) { actionButton.extend() }
verify(exactly = 1) { actionButton.show() }
verify(exactly = 0) { actionButton.shrink() }
verify(exactly = 0) { actionButton.hide() }
}
@Test
fun `WHEN accessibility is enabled THEN show is not called`() {
var tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
var fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns true
fabBinding.start()
verify(exactly = 0) { actionButton.show() }
verify(exactly = 1) { actionButton.hide() }
tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.PrivateTabs))
fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
fabBinding.start()
verify(exactly = 0) { actionButton.show() }
verify(exactly = 2) { actionButton.hide() }
tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.SyncedTabs))
fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
fabBinding.start()
verify(exactly = 0) { actionButton.show() }
verify(exactly = 3) { actionButton.hide() }
}
@Test
fun `WHEN selected page is updated THEN button is updated`() {
val tabsTrayStore = TabsTrayStore(TabsTrayState(selectedPage = Page.NormalTabs))
val fabBinding = FloatingActionButtonBinding(
tabsTrayStore, settings, actionButton, browserTrayInteractor, syncedTabsInteractor
)
every { settings.accessibilityServicesEnabled } returns false
fabBinding.start()
verify(exactly = 1) { actionButton.shrink() }
verify(exactly = 1) { actionButton.show() }
verify(exactly = 0) { actionButton.extend() }
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setIconResource(R.drawable.ic_new) }
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(Page.PrivateTabs.ordinal)))
tabsTrayStore.waitUntilIdle()
verify(exactly = 1) { actionButton.shrink() }
verify(exactly = 2) { actionButton.show() }
verify(exactly = 1) { actionButton.extend() }
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setText(R.string.tab_drawer_fab_content) }
verify(exactly = 2) { actionButton.setIconResource(R.drawable.ic_new) }
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(Page.SyncedTabs.ordinal)))
tabsTrayStore.waitUntilIdle()
verify(exactly = 1) { actionButton.shrink() }
verify(exactly = 3) { actionButton.show() }
verify(exactly = 2) { actionButton.extend() }
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setText(R.string.tab_drawer_fab_sync) }
verify(exactly = 1) { actionButton.setIconResource(R.drawable.ic_fab_sync) }
tabsTrayStore.dispatch(TabsTrayAction.SyncNow)
tabsTrayStore.waitUntilIdle()
verify(exactly = 1) { actionButton.shrink() }
verify(exactly = 4) { actionButton.show() }
verify(exactly = 3) { actionButton.extend() }
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setText(R.string.sync_syncing_in_progress) }
verify(exactly = 2) { actionButton.setIconResource(R.drawable.ic_fab_sync) }
}
}