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 2dc8e682b..1f8d5ba96 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.view.ContextThemeWrapper import androidx.lifecycle.lifecycleScope @@ -22,13 +23,20 @@ import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.fragment_tracking_protection.view.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch -import mozilla.components.browser.session.Session -import mozilla.components.concept.engine.content.blocking.Tracker +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.session.TrackingProtectionUseCases +import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.observe import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider @@ -38,6 +46,8 @@ import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents +@ExperimentalCoroutinesApi +@Suppress("TooManyFunctions") class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private val args by navArgs() @@ -54,7 +64,8 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt ) } - private lateinit var trackingProtectionStore: TrackingProtectionStore + @VisibleForTesting + internal lateinit var trackingProtectionStore: TrackingProtectionStore private lateinit var trackingProtectionView: TrackingProtectionPanelView private lateinit var trackingProtectionInteractor: TrackingProtectionPanelInteractor private lateinit var trackingProtectionUseCases: TrackingProtectionUseCases @@ -69,17 +80,14 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt container: ViewGroup?, savedInstanceState: Bundle? ): View? { + val store = requireComponents.core.store val view = inflateRootView(container) - val session = requireComponents.core.sessionManager.findSessionById(args.sessionId) - - @Suppress("DEPRECATION") - // TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16944 - session?.register(sessionObserver, view = view) + val tab = store.state.findTabOrCustomTab(provideTabId()) trackingProtectionStore = StoreProvider.get(this) { TrackingProtectionStore( TrackingProtectionState( - session, + tab, args.url, args.trackingProtectionEnabled, listTrackers = listOf(), @@ -95,27 +103,14 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt ) trackingProtectionView = TrackingProtectionPanelView(view.fragment_tp, trackingProtectionInteractor) - session?.let { updateTrackers(it) } + tab?.let { updateTrackers(it) } return view } - private val sessionObserver = object : Session.Observer { - override fun onUrlChanged(session: Session, url: String) { - trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange(url)) - } - - override fun onTrackerBlocked(session: Session, tracker: Tracker, all: List) { - updateTrackers(session) - } - - override fun onTrackerLoaded(session: Session, tracker: Tracker, all: List) { - updateTrackers(session) - } - } - - private fun updateTrackers(session: Session) { + @VisibleForTesting + internal fun updateTrackers(tab: SessionState) { trackingProtectionUseCases.fetchTrackingLogs( - session.id, + tab.id, onSuccess = { trackingProtectionStore.dispatch(TrackingProtectionAction.TrackerLogChange(it)) }, @@ -127,7 +122,10 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val store = requireComponents.core.store + observeUrlChange(store) + observeTrackersChange(store) trackingProtectionStore.observe(view) { viewLifecycleOwner.lifecycleScope.launch { whenStarted { @@ -211,4 +209,35 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt } return true } + + @VisibleForTesting + internal fun observeUrlChange(store: BrowserStore) { + consumeFlow(store) { flow -> + flow.mapNotNull { state -> + state.findTabOrCustomTab(provideTabId()) + }.ifChanged { tab -> tab.content.url } + .collect { + trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange(it.content.url)) + } + } + } + + @VisibleForTesting + internal fun provideTabId(): String = args.sessionId + + @VisibleForTesting + 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) + } + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt index ea9be1214..e278076e0 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelView.kt @@ -18,8 +18,8 @@ import androidx.core.view.isVisible import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_tracking_protection_panel.* import kotlinx.android.synthetic.main.component_tracking_protection_panel.details_blocking_header -import kotlinx.android.synthetic.main.fragment_tracking_protection_blocking.* import kotlinx.android.synthetic.main.switch_with_description.view.* +import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event @@ -107,7 +107,7 @@ class TrackingProtectionPanelView( private fun setUIForNormalMode(state: TrackingProtectionState) { details_mode.visibility = View.GONE normal_mode.visibility = View.VISIBLE - protection_settings.isGone = state.session?.customTabConfig != null + protection_settings.isGone = state.tab is CustomTabSessionState not_blocking_header.isGone = bucketedTrackers.loadedIsEmpty() bindUrl(state.url) diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt index 8e957a522..394587b9f 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt @@ -5,7 +5,7 @@ package org.mozilla.fenix.trackingprotection import androidx.annotation.StringRes -import mozilla.components.browser.session.Session +import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.engine.content.blocking.TrackerLog import mozilla.components.lib.state.Action import mozilla.components.lib.state.State @@ -55,7 +55,7 @@ sealed class TrackingProtectionAction : Action { * accessibly focus after returning from details_moode */ data class TrackingProtectionState( - val session: Session?, + val tab: SessionState?, val url: String, val isTrackingProtectionEnabled: Boolean, val listTrackers: List, diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt new file mode 100644 index 000000000..5934d9aeb --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt @@ -0,0 +1,149 @@ +/* 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.trackingprotection + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import io.mockk.every +import io.mockk.mockk +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.ContentAction +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.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 TrackingProtectionPanelDialogFragmentTest { + + private val testDispatcher = TestCoroutineDispatcher() + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + private lateinit var lifecycleOwner: MockedLifecycleOwner + private lateinit var fragment: TrackingProtectionPanelDialogFragment + private lateinit var store: BrowserStore + + @Before + fun setup() { + fragment = spyk(TrackingProtectionPanelDialogFragment()) + 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 the url is updated THEN the url view is updated`() { + val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val tab = createTab("mozilla.org") + + every { fragment.trackingProtectionStore } returns trackingProtectionStore + every { fragment.provideTabId() } returns tab.id + + fragment.observeUrlChange(store) + addAndSelectTab(tab) + + verify(exactly = 1) { + trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("mozilla.org")) + } + + store.dispatch(ContentAction.UpdateUrlAction(tab.id, "wikipedia.org")).joinBlocking() + + verify(exactly = 1) { + trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("wikipedia.org")) + } + } + + @Test + fun `WHEN a tracker is loaded THEN trackers view is updated`() { + val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val tab = createTab("mozilla.org") + + every { fragment.trackingProtectionStore } returns trackingProtectionStore + 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 trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true) + val tab = createTab("mozilla.org") + + every { fragment.trackingProtectionStore } returns trackingProtectionStore + 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(tab) + } + } + 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/trackingprotection/TrackingProtectionPanelViewTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt index eb56f1a02..884a26529 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt @@ -29,7 +29,7 @@ class TrackingProtectionPanelViewTest { private lateinit var interactor: TrackingProtectionPanelInteractor private lateinit var view: TrackingProtectionPanelView private val baseState = TrackingProtectionState( - session = null, + tab = null, url = "", isTrackingProtectionEnabled = false, listTrackers = emptyList(), diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt index db2e66bc8..208396029 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt @@ -6,7 +6,7 @@ package org.mozilla.fenix.trackingprotection import io.mockk.mockk import kotlinx.coroutines.runBlocking -import mozilla.components.browser.session.Session +import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.engine.content.blocking.TrackerLog import org.junit.Assert.assertEquals import org.junit.Assert.assertNotSame @@ -14,7 +14,7 @@ import org.junit.Test class TrackingProtectionStoreTest { - val session: Session = mockk(relaxed = true) + val tab: SessionState = mockk(relaxed = true) @Test fun enterDetailsMode() = runBlocking { @@ -131,7 +131,7 @@ class TrackingProtectionStoreTest { } private fun defaultState(): TrackingProtectionState = TrackingProtectionState( - session = session, + tab = tab, url = "www.mozilla.org", isTrackingProtectionEnabled = true, listTrackers = listOf(), @@ -140,7 +140,7 @@ class TrackingProtectionStoreTest { ) private fun detailsState(): TrackingProtectionState = TrackingProtectionState( - session = session, + tab = tab, url = "www.mozilla.org", isTrackingProtectionEnabled = true, listTrackers = listOf(),