diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index fe3457e008..90a3660caf 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -11,19 +11,30 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.plus +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.R import org.mozilla.fenix.android.FenixDialogFragment import org.mozilla.fenix.databinding.FragmentQuickSettingsDialogSheetBinding import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.PhoneFeature /** @@ -38,7 +49,10 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { private lateinit var quickSettingsController: QuickSettingsController private lateinit var websiteInfoView: WebsiteInfoView private lateinit var websitePermissionsView: WebsitePermissionsView - private lateinit var trackingProtectionView: TrackingProtectionView + + @VisibleForTesting + internal lateinit var trackingProtectionView: TrackingProtectionView + private lateinit var interactor: QuickSettingsInteractor private var tryToRequestPermissions: Boolean = false @@ -101,7 +115,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { websitePermissionsView = WebsitePermissionsView(binding.websitePermissionsLayout, interactor) trackingProtectionView = - TrackingProtectionView(binding.trackingProtectionLayout, interactor) + TrackingProtectionView(binding.trackingProtectionLayout, interactor, context.settings()) return rootView } @@ -109,7 +123,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - + observeTrackersChange(requireComponents.core.store) consumeFrom(quickSettingsStore) { websiteInfoView.update(it.webInfoState) websitePermissionsView.update(it.websitePermissionsState) @@ -157,6 +171,42 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() { ) } + @VisibleForTesting + internal fun provideTabId(): String = args.sessionId + + @VisibleForTesting + @ExperimentalCoroutinesApi + internal fun observeTrackersChange(store: BrowserStore) { + consumeFlow(store) { flow -> + flow.mapNotNull { state -> + state.findTabOrCustomTab(provideTabId()) + }.ifAnyChanged { tab -> + arrayOf( + tab.trackingProtection.blockedTrackers, + tab.trackingProtection.loadedTrackers + ) + }.collect { + updateTrackers(it) + } + } + } + + @VisibleForTesting + internal fun updateTrackers(tab: SessionState) { + provideTrackingProtectionUseCases().fetchTrackingLogs( + tab.id, + onSuccess = { trackers -> + trackingProtectionView.updateDetailsSection(trackers.isNotEmpty()) + }, + onError = { + Logger.error("QuickSettingsSheetDialogFragment - fetchTrackingLogs onError", it) + } + ) + } + + @VisibleForTesting + internal fun provideTrackingProtectionUseCases() = requireComponents.useCases.trackingProtectionUseCases + private companion object { const val REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS = 4 } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt index 11528a0def..846f16f5cb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionView.kt @@ -6,9 +6,13 @@ package org.mozilla.fenix.settings.quicksettings import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.core.view.isVisible import org.mozilla.fenix.R import org.mozilla.fenix.databinding.QuicksettingsTrackingProtectionBinding +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.utils.Settings /** * Contract declaring all possible user interactions with [TrackingProtectionView]. @@ -35,26 +39,33 @@ interface TrackingProtectionInteractor { * * @param containerView [ViewGroup] in which this View will inflate itself. * @param interactor [TrackingProtectionInteractor] which will have delegated to all user + * @param settings [Settings] application settings. * interactions. */ class TrackingProtectionView( val containerView: ViewGroup, val interactor: TrackingProtectionInteractor, + val settings: Settings ) { private val context = containerView.context - private val binding = QuicksettingsTrackingProtectionBinding.inflate( + @VisibleForTesting + internal val binding = QuicksettingsTrackingProtectionBinding.inflate( LayoutInflater.from(containerView.context), containerView, true ) fun update(state: TrackingProtectionState) { bindTrackingProtectionInfo(state.isTrackingProtectionEnabled) - + binding.root.isVisible = settings.shouldUseTrackingProtection binding.trackingProtectionDetails.setOnClickListener { interactor.onDetailsClicked() } } + fun updateDetailsSection(show: Boolean) { + binding.trackingProtectionDetails.isVisible = show + } + private fun bindTrackingProtectionInfo(isTrackingProtectionEnabled: Boolean) { binding.trackingProtectionSwitch.trackingProtectionCategoryItemDescription.text = context.getString(if (isTrackingProtectionEnabled) R.string.etp_panel_on else R.string.etp_panel_off) diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt index 4f3e1b0627..ec2c53afc0 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -82,7 +82,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt ): View { val store = requireComponents.core.store val view = inflateRootView(container) - val tab = store.state.findTabOrCustomTab(provideTabId()) + val tab = store.state.findTabOrCustomTab(provideCurrentTabId()) trackingProtectionStore = StoreProvider.get(this) { TrackingProtectionStore( @@ -201,7 +201,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt internal fun observeUrlChange(store: BrowserStore) { consumeFlow(store) { flow -> flow.mapNotNull { state -> - state.findTabOrCustomTab(provideTabId()) + state.findTabOrCustomTab(provideCurrentTabId()) }.ifChanged { tab -> tab.content.url } .collect { trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange(it.content.url)) @@ -210,13 +210,13 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt } @VisibleForTesting - internal fun provideTabId(): String = args.sessionId + internal fun provideCurrentTabId(): String = args.sessionId @VisibleForTesting internal fun observeTrackersChange(store: BrowserStore) { consumeFlow(store) { flow -> flow.mapNotNull { state -> - state.findTabOrCustomTab(provideTabId()) + state.findTabOrCustomTab(provideCurrentTabId()) }.ifAnyChanged { tab -> arrayOf( tab.trackingProtection.blockedTrackers, diff --git a/app/src/main/res/layout/quicksettings_tracking_protection.xml b/app/src/main/res/layout/quicksettings_tracking_protection.xml index ed84b0b55a..24fd205c7a 100644 --- a/app/src/main/res/layout/quicksettings_tracking_protection.xml +++ b/app/src/main/res/layout/quicksettings_tracking_protection.xml @@ -29,6 +29,7 @@ android:gravity="end|center_vertical" android:layout_alignParentEnd="true" android:text="@string/enhanced_tracking_protection_details" + android:visibility="gone" app:drawableEndCompat="@drawable/ic_arrowhead_right" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt new file mode 100644 index 0000000000..a4fa119ef8 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragmentTest.kt @@ -0,0 +1,182 @@ +/* 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.settings.quicksettings + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import junit.framework.TestCase.assertNotSame +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.action.TrackingProtectionAction.TrackerBlockedAction +import mozilla.components.browser.state.action.TrackingProtectionAction.TrackerLoadedAction +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.content.blocking.TrackerLog +import mozilla.components.feature.session.TrackingProtectionUseCases +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class QuickSettingsSheetDialogFragmentTest { + + private val testDispatcher = TestCoroutineDispatcher() + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + private lateinit var lifecycleOwner: MockedLifecycleOwner + private lateinit var fragment: QuickSettingsSheetDialogFragment + private lateinit var store: BrowserStore + + @Before + fun setup() { + fragment = spyk(QuickSettingsSheetDialogFragment()) + lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED) + + store = BrowserStore() + every { fragment.view } returns mockk(relaxed = true) + every { fragment.lifecycle } returns lifecycleOwner.lifecycle + every { fragment.activity } returns mockk(relaxed = true) + } + + @After + fun cleanUp() { + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun `WHEN a tracker is loaded THEN trackers view is updated`() { + val tab = createTab("mozilla.org") + + every { fragment.provideTabId() } returns tab.id + every { fragment.updateTrackers(any()) } returns Unit + + fragment.observeTrackersChange(store) + + addAndSelectTab(tab) + + verify(exactly = 1) { + fragment.updateTrackers(tab) + } + + store.dispatch(TrackerLoadedAction(tab.id, mockk())).joinBlocking() + + val updatedTab = store.state.findTab(tab.id)!! + + assertNotSame(updatedTab, tab) + + verify(exactly = 1) { + fragment.updateTrackers(updatedTab) + } + } + + @Test + fun `WHEN a tracker is blocked THEN trackers view is updated`() { + val tab = createTab("mozilla.org") + + every { fragment.provideTabId() } returns tab.id + every { fragment.updateTrackers(any()) } returns Unit + + fragment.observeTrackersChange(store) + + addAndSelectTab(tab) + + verify(exactly = 1) { + fragment.updateTrackers(tab) + } + + store.dispatch(TrackerBlockedAction(tab.id, mockk())).joinBlocking() + + val updatedTab = store.state.findTab(tab.id)!! + + assertNotSame(updatedTab, tab) + + verify(exactly = 1) { + fragment.updateTrackers(updatedTab) + } + } + + @Test + fun `GIVEN no trackers WHEN calling updateTrackers THEN hide the details section`() { + val tab = createTab("mozilla.org") + val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true) + val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true) + + val onComplete = slot<(List) -> Unit>() + + every { fragment.trackingProtectionView } returns trackingProtectionView + + every { + trackingProtectionUseCases.fetchTrackingLogs.invoke( + any(), + capture(onComplete), + any() + ) + }.answers { onComplete.captured.invoke(emptyList()) } + + every { fragment.provideTrackingProtectionUseCases() } returns trackingProtectionUseCases + + fragment.updateTrackers(tab) + + verify { + trackingProtectionView.updateDetailsSection(false) + } + } + + @Test + fun `GIVEN trackers WHEN calling updateTrackers THEN show the details section`() { + val tab = createTab("mozilla.org") + val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true) + val trackingProtectionView: TrackingProtectionView = mockk(relaxed = true) + + val onComplete = slot<(List) -> Unit>() + + every { fragment.trackingProtectionView } returns trackingProtectionView + + every { + trackingProtectionUseCases.fetchTrackingLogs.invoke( + any(), + capture(onComplete), + any() + ) + }.answers { onComplete.captured.invoke(listOf(TrackerLog(""))) } + + every { fragment.provideTrackingProtectionUseCases() } returns trackingProtectionUseCases + + fragment.updateTrackers(tab) + + verify { + trackingProtectionView.updateDetailsSection(true) + } + } + + private fun addAndSelectTab(tab: TabSessionState) { + store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() + store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking() + } + + internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry(this).apply { + currentState = initialState + } + + override fun getLifecycle(): Lifecycle = lifecycleRegistry + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt new file mode 100644 index 0000000000..8eca33cf04 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/TrackingProtectionViewTest.kt @@ -0,0 +1,95 @@ +/* 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.settings.quicksettings + +import android.widget.FrameLayout +import androidx.core.view.isVisible +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.spyk +import mozilla.components.browser.state.state.createTab +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.databinding.QuicksettingsTrackingProtectionBinding +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.trackingprotection.TrackingProtectionState +import org.mozilla.fenix.utils.Settings + +@RunWith(FenixRobolectricTestRunner::class) +class TrackingProtectionViewTest { + + private lateinit var view: TrackingProtectionView + private lateinit var binding: QuicksettingsTrackingProtectionBinding + private lateinit var interactor: TrackingProtectionInteractor + + @MockK(relaxed = true) + private lateinit var settings: Settings + + @Before + fun setup() { + MockKAnnotations.init(this) + interactor = mockk(relaxed = true) + view = spyk(TrackingProtectionView(FrameLayout(testContext), interactor, settings)) + binding = view.binding + } + + @Test + fun `WHEN updating THEN bind checkbox`() { + val websiteUrl = "https://mozilla.org" + val state = TrackingProtectionState( + tab = createTab(url = websiteUrl), + url = websiteUrl, + isTrackingProtectionEnabled = true, + listTrackers = listOf(), + mode = TrackingProtectionState.Mode.Normal, + lastAccessedCategory = "" + ) + + every { settings.shouldUseTrackingProtection } returns true + + view.update(state) + + assertTrue(binding.root.isVisible) + assertTrue(binding.trackingProtectionSwitch.switchWidget.isChecked) + } + + @Test + fun `GIVEN TP is globally off WHEN updating THEN hide the TP section`() { + val websiteUrl = "https://mozilla.org" + val state = TrackingProtectionState( + tab = createTab(url = websiteUrl), + url = websiteUrl, + isTrackingProtectionEnabled = true, + listTrackers = listOf(), + mode = TrackingProtectionState.Mode.Normal, + lastAccessedCategory = "" + ) + + every { settings.shouldUseTrackingProtection } returns false + + view.update(state) + + assertFalse(binding.root.isVisible) + } + + @Test + fun `WHEN updateDetailsSection is called THEN update the visibility of the section`() { + every { settings.shouldUseTrackingProtection } returns false + + view.updateDetailsSection(false) + + assertFalse(binding.trackingProtectionDetails.isVisible) + + view.updateDetailsSection(true) + + assertTrue(binding.trackingProtectionDetails.isVisible) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt index 5934d9aeb5..49f9b453e5 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt @@ -65,7 +65,7 @@ class TrackingProtectionPanelDialogFragmentTest { val tab = createTab("mozilla.org") every { fragment.trackingProtectionStore } returns trackingProtectionStore - every { fragment.provideTabId() } returns tab.id + every { fragment.provideCurrentTabId() } returns tab.id fragment.observeUrlChange(store) addAndSelectTab(tab) @@ -87,7 +87,7 @@ class TrackingProtectionPanelDialogFragmentTest { val tab = createTab("mozilla.org") every { fragment.trackingProtectionStore } returns trackingProtectionStore - every { fragment.provideTabId() } returns tab.id + every { fragment.provideCurrentTabId() } returns tab.id every { fragment.updateTrackers(any()) } returns Unit fragment.observeTrackersChange(store) @@ -114,7 +114,7 @@ class TrackingProtectionPanelDialogFragmentTest { val tab = createTab("mozilla.org") every { fragment.trackingProtectionStore } returns trackingProtectionStore - every { fragment.provideTabId() } returns tab.id + every { fragment.provideCurrentTabId() } returns tab.id every { fragment.updateTrackers(any()) } returns Unit fragment.observeTrackersChange(store)