For #19886 - Add a back navigation to the Global Quick Settings from the Tracking Protection dialog

This commit is contained in:
Gabriel Luong 2021-07-14 15:19:11 -04:00 committed by Arturo Mejia
parent 34d99f89ac
commit d1b9744069
15 changed files with 213 additions and 71 deletions

View File

@ -28,8 +28,6 @@ interface ConnectionDetailsController {
/**
* Default behavior of [ConnectionDetailsController].
*
* @param dismiss callback allowing to request this entire Fragment to be dismissed.
*/
class DefaultConnectionDetailsController(
private val context: Context,
@ -37,9 +35,9 @@ class DefaultConnectionDetailsController(
private val navController: () -> NavController,
internal var sitePermissions: SitePermissions?,
private val gravity: Int,
private val getCurrentTab: () -> SessionState?,
private val dismiss: () -> Unit
private val getCurrentTab: () -> SessionState?
) : ConnectionDetailsController {
override fun handleBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->

View File

@ -34,15 +34,16 @@ class ConnectionPanelDialogFragment : FenixDialogFragment() {
savedInstanceState: Bundle?
): View {
val rootView = inflateRootView(container)
val controller = DefaultConnectionDetailsController(
context = requireContext(),
fragment = this,
navController = { findNavController() },
sitePermissions = args.sitePermissions,
gravity = args.gravity,
getCurrentTab = ::getCurrentTab,
dismiss = ::dismiss
getCurrentTab = ::getCurrentTab
)
val interactor = ConnectionDetailsInteractor(controller)
connectionView = WebsiteInfoView(
container = rootView.connectionDetailsInfoLayout,
@ -65,7 +66,7 @@ class ConnectionPanelDialogFragment : FenixDialogFragment() {
)
}
internal fun getCurrentTab(): SessionState? {
private fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findTabOrCustomTab(args.sessionId)
}
}

View File

@ -205,7 +205,8 @@ class DefaultQuickSettingsController(
sessionId = sessionId,
url = state.url,
trackingProtectionEnabled = state.isTrackingProtectionEnabled,
gravity = context.components.settings.toolbarPosition.androidGravity
gravity = context.components.settings.toolbarPosition.androidGravity,
sitePermissions = sitePermissions
)
navController().navigate(directions)
}

View File

@ -53,7 +53,7 @@ class QuickSettingsFragmentStore(
* @param permissions [SitePermissions]? list of website permissions and their status.
* @param settings [Settings] application settings.
* @param certificateName [String] the certificate name of the current web page.
* @param sessionId [String] TODO
* @param sessionId [String] The current session ID.
* @param isTrackingProtectionEnabled [Boolean] Current status of tracking protection
* for this session.
*/
@ -127,7 +127,7 @@ class QuickSettingsFragmentStore(
* [TrackingProtectionView].
*
* @param context [Context] used for various Android interactions.
* @param sessionId [String] TODO
* @param sessionId [String] The current session ID.
* @param websiteUrl [String] the URL of the current web page.
* @param isTrackingProtectionEnabled [Boolean] Current status of tracking protection
* for this session.

View File

@ -19,6 +19,7 @@ import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -96,8 +97,14 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
)
}
trackingProtectionInteractor = TrackingProtectionPanelInteractor(
trackingProtectionStore,
::openTrackingProtectionSettings
context = requireContext(),
fragment = this,
store = trackingProtectionStore,
navController = { findNavController() },
openTrackingProtectionSettings = ::openTrackingProtectionSettings,
sitePermissions = args.sitePermissions,
gravity = args.gravity,
getCurrentTab = ::getCurrentTab
)
trackingProtectionView =
TrackingProtectionPanelView(view.fragment_tp, trackingProtectionInteractor)
@ -219,4 +226,8 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt
}
}
}
private fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findTabOrCustomTab(args.sessionId)
}
}

View File

@ -4,14 +4,31 @@
package org.mozilla.fenix.trackingprotection
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.permission.SitePermissions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.runIfFragmentIsAttached
/**
* Interactor for the tracking protection panel
* Provides implementations for the TrackingProtectionPanelViewInteractor
*/
@Suppress("LongParameterList")
class TrackingProtectionPanelInteractor(
private val context: Context,
private val fragment: Fragment,
private val store: TrackingProtectionStore,
private val openTrackingProtectionSettings: () -> Unit
private val navController: () -> NavController,
private val openTrackingProtectionSettings: () -> Unit,
internal var sitePermissions: SitePermissions?,
private val gravity: Int,
private val getCurrentTab: () -> SessionState?
) : TrackingProtectionPanelViewInteractor {
override fun openDetails(category: TrackingProtectionCategory, categoryBlocked: Boolean) {
store.dispatch(TrackingProtectionAction.EnterDetailsMode(category, categoryBlocked))
}
@ -21,6 +38,30 @@ class TrackingProtectionPanelInteractor(
}
override fun onBackPressed() {
getCurrentTab()?.let { tab ->
context.components.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
fragment.runIfFragmentIsAttached {
navController().popBackStack()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionGlobalQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = gravity,
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled
)
navController().navigate(directions)
}
}
}
}
override fun onExitDetailMode() {
store.dispatch(TrackingProtectionAction.ExitDetailsMode)
}
}

View File

@ -9,7 +9,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@ -17,18 +16,16 @@ import androidx.core.view.isGone
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 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
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CROSS_SITE_TRACKING_COOKIES
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.CRYPTOMINERS
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.FINGERPRINTERS
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.REDIRECT_TRACKERS
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.SOCIAL_MEDIA_TRACKERS
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.TRACKING_CONTENT
import org.mozilla.fenix.trackingprotection.TrackingProtectionCategory.REDIRECT_TRACKERS
/**
* Interface for the TrackingProtectionPanelViewInteractor. This interface is implemented by objects that want
@ -45,6 +42,11 @@ interface TrackingProtectionPanelViewInteractor {
*/
fun onBackPressed()
/**
* Called whenever back button is pressed in Detail mode.
*/
fun onExitDetailMode()
/**
* Called whenever an active tracking protection category is tapped
* @param category The Tracking Protection Category to view details about
@ -76,9 +78,15 @@ class TrackingProtectionPanelView(
protection_settings.setOnClickListener {
interactor.selectTrackingProtectionSettings()
}
details_back.setOnClickListener {
interactor.onExitDetailMode()
}
navigate_back.setOnClickListener {
interactor.onBackPressed()
}
setCategoryClickListeners()
}
@ -100,12 +108,11 @@ class TrackingProtectionPanelView(
private fun setUIForNormalMode(state: TrackingProtectionState) {
details_mode.visibility = View.GONE
normal_mode.visibility = View.VISIBLE
protection_settings.isGone = state.tab is CustomTabSessionState
not_blocking_header.isGone = bucketedTrackers.loadedIsEmpty()
bindUrl(state.url)
blocking_header.isGone = bucketedTrackers.blockedIsEmpty()
updateCategoryVisibility()
focusAccessibilityLastUsedCategory(state.lastAccessedCategory)
}
@ -213,10 +220,6 @@ class TrackingProtectionPanelView(
interactor.openDetails(category, categoryBlocked = !isLoaded(v))
}
private fun bindUrl(url: String) {
this.url.text = url.toUri().hostWithoutCommonPrefixes
}
fun onBackPressed(): Boolean {
return when (mode) {
is TrackingProtectionState.Mode.Details -> {

View File

@ -45,14 +45,14 @@ sealed class TrackingProtectionAction : Action {
/**
* The state for the Tracking Protection Panel
* @property tab TODO
* @property tab Current session to display
* @property url Current URL to display
* @property isTrackingProtectionEnabled Current status of tracking protection for this session
* (ie is an exception)
* @property listTrackers Current Tracker Log list of blocked and loaded tracker categories
* @property mode Current Mode of TrackingProtection
* @property lastAccessedCategory Remembers the last accessed details category, used to move
* accessibly focus after returning from details_mode
* accessibly focus after returning from details_mode
*/
data class TrackingProtectionState(
val tab: SessionState?,

View File

@ -19,14 +19,29 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/url"
style="@style/QuickSettingsText"
android:layout_width="wrap_content"
<ImageView
android:id="@+id/navigate_back"
android:layout_width="@dimen/tracking_protection_item_height"
android:layout_height="@dimen/tracking_protection_item_height"
android:scaleType="center"
android:contentDescription="@string/etp_back_button_content_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="https://wikipedia.org" />
app:srcCompat="@drawable/mozac_ic_back"
app:tint="?attr/primaryText" />
<TextView
android:id="@+id/details"
style="@style/QuickSettingsLargeText"
android:layout_width="wrap_content"
android:layout_height="@dimen/tracking_protection_item_height"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:textColor="?attr/primaryText"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintStart_toEndOf="@+id/navigate_back"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/enhanced_tracking_protection_details" />
<TextView
android:id="@+id/blocking_header"
@ -39,7 +54,7 @@
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/url"
app:layout_constraintTop_toBottomOf="@id/details"
tools:targetApi="p" />
<TextView
@ -210,6 +225,7 @@
android:layout_marginEnd="19dp"
android:textColor="?attr/primaryText"
android:textSize="16sp"
app:fontFamily="@font/metropolis_semibold"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />

View File

@ -21,8 +21,6 @@
app:switchIcon="@drawable/ic_tracking_protection"
app:switchTitle="@string/preference_enhanced_tracking_protection" />
<!-- TODO: Match drawablePadding with Design Spec. Using @style/QuickSettingsText.Icon as a placeholder -->
<!-- TODO: Match layout_height with Design Spec. Using @dimen/quicksettings_item_height as a placeholder -->
<TextView
android:id="@+id/trackingProtectionBlockedItems"
style="@style/QuickSettingsText.Icon"

View File

@ -812,6 +812,10 @@
android:name="gravity"
android:defaultValue="80"
app:argType="integer" />
<argument
android:name="sitePermissions"
app:argType="mozilla.components.concept.engine.permission.SitePermissions"
app:nullable="true" />
</dialog>
<dialog
android:id="@+id/connectionPanelDialogFragment"

View File

@ -25,7 +25,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ExperimentalCoroutinesApi
@ -43,9 +42,6 @@ class DefaultConnectionDetailsControllerTest {
@MockK(relaxed = true)
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var dismiss: () -> Unit
private lateinit var controller: DefaultConnectionDetailsController
private lateinit var tab: TabSessionState
@ -64,8 +60,7 @@ class DefaultConnectionDetailsControllerTest {
navController = { navController },
sitePermissions = sitePermissions,
gravity = gravity,
getCurrentTab = { tab },
dismiss = dismiss
getCurrentTab = { tab }
)
every { fragment.context } returns context
@ -87,7 +82,7 @@ class DefaultConnectionDetailsControllerTest {
verify {
navController.popBackStack()
navController.navigateBlockingForAsyncNavGraph(any<NavDirections>())
navController.navigate(any<NavDirections>())
}
}
}

View File

@ -6,15 +6,15 @@ package org.mozilla.fenix.settings.quicksettings
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
@ -22,9 +22,9 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.test.mock
@ -43,7 +43,6 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
@ -376,7 +375,7 @@ class DefaultQuickSettingsControllerTest {
verify {
navController.popBackStack()
navController.navigateBlockingForAsyncNavGraph(any<NavDirections>())
navController.navigate(any<NavDirections>())
}
}
@ -400,7 +399,7 @@ class DefaultQuickSettingsControllerTest {
verify {
navController.popBackStack()
navController.navigateBlockingForAsyncNavGraph(any<NavDirections>())
navController.navigate(any<NavDirections>())
}
}
}

View File

@ -4,19 +4,89 @@
package org.mozilla.fenix.trackingprotection
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import org.junit.Test
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.TrackingProtectionUseCases
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.ext.components
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class TrackingProtectionPanelInteractorTest {
private lateinit var context: Context
@MockK(relaxed = true)
private lateinit var navController: NavController
@MockK(relaxed = true)
private lateinit var fragment: Fragment
@MockK(relaxed = true)
private lateinit var sitePermissions: SitePermissions
@MockK(relaxed = true)
private lateinit var store: TrackingProtectionStore
private lateinit var interactor: TrackingProtectionPanelInteractor
private lateinit var tab: TabSessionState
private var openSettings = false
private var gravity = 54
@Before
fun setup() {
MockKAnnotations.init(this)
context = spyk(testContext)
tab = createTab("https://mozilla.org")
interactor = TrackingProtectionPanelInteractor(
context = context,
fragment = fragment,
store = store,
navController = { navController },
openTrackingProtectionSettings = { openSettings = true },
sitePermissions = sitePermissions,
gravity = gravity,
getCurrentTab = { tab }
)
val trackingProtectionUseCases: TrackingProtectionUseCases = mockk(relaxed = true)
every { fragment.context } returns context
every { context.components.useCases.trackingProtectionUseCases } returns trackingProtectionUseCases
val onComplete = slot<(Boolean) -> Unit>()
every {
trackingProtectionUseCases.containsException.invoke(
any(),
capture(onComplete)
)
}.answers { onComplete.captured.invoke(true) }
}
@Test
fun openDetails() {
val store: TrackingProtectionStore = mockk(relaxed = true)
val interactor = TrackingProtectionPanelInteractor(store, {}, {})
interactor.openDetails(TrackingProtectionCategory.FINGERPRINTERS, true)
fun `WHEN openDetails is called THEN store should dispatch EnterDetailsMode action with the right category`() {
interactor.openDetails(TrackingProtectionCategory.FINGERPRINTERS, true, {}, {})
verify {
store.dispatch(
TrackingProtectionAction.EnterDetailsMode(
@ -25,14 +95,9 @@ class TrackingProtectionPanelInteractorTest {
)
)
}
}
@Test
fun openDetailsForRedirectTrackers() {
val store: TrackingProtectionStore = mockk(relaxed = true)
val interactor =
TrackingProtectionPanelInteractor(store, {}, {})
interactor.openDetails(TrackingProtectionCategory.REDIRECT_TRACKERS, true)
verify {
store.dispatch(
TrackingProtectionAction.EnterDetailsMode(
@ -44,23 +109,27 @@ class TrackingProtectionPanelInteractorTest {
}
@Test
fun selectTrackingProtectionSettings() {
var openSettings = false
val interactor = TrackingProtectionPanelInteractor(
mockk(),
{ },
{ openSettings = true }
)
fun `WHEN selectTrackingProtectionSettings is called THEN openTrackingProtectionSettings should be invoked`() {
interactor.selectTrackingProtectionSettings()
assertEquals(true, openSettings)
}
@Test
fun onBackPressed() {
val store: TrackingProtectionStore = mockk(relaxed = true)
val interactor =
TrackingProtectionPanelInteractor(store, {}, {})
fun `WHEN onBackPressed is called THEN call popBackStack and navigate`() {
interactor.onBackPressed()
verify {
navController.popBackStack()
navController.navigate(any<NavDirections>())
}
}
@Test
fun `WHEN onExitDetailMode is called THEN store should dispatch ExitDetailsMode action`() {
interactor.onExitDetailMode()
verify { store.dispatch(TrackingProtectionAction.ExitDetailsMode) }
}
}

View File

@ -87,8 +87,14 @@ class TrackingProtectionPanelViewTest {
}
@Test
fun testDetailsBack() {
fun testExistDetailModed() {
view.details_back.performClick()
verify { interactor.onExitDetailMode() }
}
@Test
fun testDetailsBack() {
view.navigate_back.performClick()
verify { interactor.onBackPressed() }
}