For #9294: Add option to clear current site data in quick settings dialog.

This commit is contained in:
aime Soriano Pastor 2021-12-07 09:29:31 -05:00 committed by mergify[bot]
parent e150f6118b
commit 9bfc94b793
12 changed files with 294 additions and 4 deletions

View File

@ -96,4 +96,9 @@ object FeatureFlags {
* Enables showing the homescreen onboarding card.
*/
const val showHomeOnboarding = false
/**
* Enables showing the option to clear site data.
*/
val showClearSiteData = Config.channel.isNightlyOrDebug
}

View File

@ -0,0 +1,117 @@
/* 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.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.QuicksettingsClearSiteDataBinding
import org.mozilla.fenix.ext.components
/**
* Contract declaring all possible user interactions with [ClearSiteDataView].
*/
interface ClearSiteDataViewInteractor {
/**
* Shows the confirmation dialog to clear site data for [baseDomain].
*/
fun onClearSiteDataClicked(baseDomain: String)
}
/**
* MVI View to access the dialog to clear site cookies and data.
*
* @param containerView [ViewGroup] in which this View will inflate itself.
* @param interactor [TrackingProtectionInteractor] which will have delegated to all user
* interactions.
*/
class ClearSiteDataView(
val context: Context,
private val ioScope: CoroutineScope,
val containerView: ViewGroup,
val containerDivider: View,
val interactor: ClearSiteDataViewInteractor
) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
lateinit var websiteUrl: String
val binding = QuicksettingsClearSiteDataBinding.inflate(
LayoutInflater.from(context),
containerView,
true
)
fun update(webInfoState: WebsiteInfoState) {
if (!FeatureFlags.showClearSiteData) {
setVisibility(false)
return
}
websiteUrl = webInfoState.websiteUrl
setVisibility(true)
binding.clearSiteData.setOnClickListener {
askToClear()
}
}
private fun setVisibility(visible: Boolean) {
binding.root.isVisible = visible
containerDivider.isVisible = visible
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun askToClear() {
ioScope.launch {
val publicSuffixList = context.components.publicSuffixList
val host = websiteUrl.toUri().host.orEmpty()
val domain = publicSuffixList.getPublicSuffixPlusOne(host).await()
domain?.let { baseDomain ->
launch(Dispatchers.Main) {
showConfirmationDialog(baseDomain)
}
}
}
}
private fun showConfirmationDialog(baseDomain: String) {
AlertDialog.Builder(context).apply {
setMessage(
HtmlCompat.fromHtml(
context.getString(
R.string.confirm_clear_site_data,
baseDomain
),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
)
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { it: DialogInterface, _ ->
it.cancel()
}
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { it: DialogInterface, _ ->
it.dismiss()
interactor.onClearSiteDataClicked(baseDomain)
}
create()
}.show()
}
}

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.feature.session.SessionUseCases.ReloadUrlUseCase
import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase
@ -74,6 +75,12 @@ interface QuickSettingsController {
* "Secured or Insecure Connection" section.
*/
fun handleConnectionDetailsClicked()
/**
* Clears site data for the current website. Called when a user clicks
* on the section to clear site data.
*/
fun handleClearSiteDataClicked(baseDomain: String)
}
/**
@ -111,6 +118,7 @@ class DefaultQuickSettingsController(
private val addNewTab: AddNewTabUseCase,
private val requestRuntimePermissions: OnNeedToRequestPermissions = { },
private val displayPermissions: () -> Unit,
private val engine: Engine = context.components.core.engine,
private val dismiss: () -> Unit
) : QuickSettingsController {
override fun handlePermissionsShown() {
@ -193,7 +201,11 @@ class DefaultQuickSettingsController(
sessionUseCases.reload.invoke(session.id)
}
quickSettingsStore.dispatch(TrackingProtectionAction.ToggleTrackingProtectionEnabled(isEnabled))
quickSettingsStore.dispatch(
TrackingProtectionAction.ToggleTrackingProtectionEnabled(
isEnabled
)
)
}
override fun handleDetailsClicked() {
@ -228,6 +240,17 @@ class DefaultQuickSettingsController(
navController().navigate(directions)
}
override fun handleClearSiteDataClicked(baseDomain: String) {
engine.clearData(
host = baseDomain,
data = Engine.BrowsingData.select(
Engine.BrowsingData.AUTH_SESSIONS,
Engine.BrowsingData.ALL_SITE_DATA,
),
)
navController().popBackStack()
}
/**
* Request a certain set of runtime Android permissions.
*

View File

@ -41,7 +41,6 @@ data class WebsiteInfoState(
val websiteSecurityUiValues: WebsiteSecurityUiValues,
val certificateName: String
) : State {
companion object {
/**
* Construct an initial [WebsiteInfoState]

View File

@ -15,7 +15,7 @@ package org.mozilla.fenix.settings.quicksettings
*/
class QuickSettingsInteractor(
private val controller: QuickSettingsController
) : WebsitePermissionInteractor, TrackingProtectionInteractor, WebSiteInfoInteractor {
) : WebsitePermissionInteractor, TrackingProtectionInteractor, WebSiteInfoInteractor, ClearSiteDataViewInteractor {
override fun onPermissionsShown() {
controller.handlePermissionsShown()
}
@ -39,4 +39,8 @@ class QuickSettingsInteractor(
override fun onConnectionDetailsClicked() {
controller.handleConnectionDetailsClicked()
}
override fun onClearSiteDataClicked(baseDomain: String) {
controller.handleClearSiteDataClicked(baseDomain)
}
}

View File

@ -49,6 +49,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
private lateinit var quickSettingsController: QuickSettingsController
private lateinit var websiteInfoView: WebsiteInfoView
private lateinit var websitePermissionsView: WebsitePermissionsView
private lateinit var clearSiteDataView: ClearSiteDataView
@VisibleForTesting
internal lateinit var trackingProtectionView: TrackingProtectionView
@ -116,6 +117,13 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
WebsitePermissionsView(binding.websitePermissionsLayout, interactor)
trackingProtectionView =
TrackingProtectionView(binding.trackingProtectionLayout, interactor, context.settings())
clearSiteDataView = ClearSiteDataView(
context = context,
ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO,
containerView = binding.clearSiteDataLayout,
containerDivider = binding.clearSiteDataDivider,
interactor = interactor
)
return rootView
}
@ -127,6 +135,7 @@ class QuickSettingsSheetDialogFragment : FenixDialogFragment() {
websiteInfoView.update(it.webInfoState)
websitePermissionsView.update(it.websitePermissionsState)
trackingProtectionView.update(it.trackingProtectionState)
clearSiteDataView.update(it.webInfoState)
}
}

View File

@ -41,7 +41,7 @@
android:id="@+id/trackingProtectionLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@+id/clearSiteDataLayout" />
<View
android:id="@+id/trackingProtectionDivider"
@ -52,6 +52,21 @@
android:background="?neutralFaded"
app:layout_constraintBottom_toTopOf="@id/trackingProtectionLayout" />
<FrameLayout
android:id="@+id/clearSiteDataLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />
<View
android:id="@+id/clearSiteDataDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="?neutralFaded"
app:layout_constraintBottom_toTopOf="@id/clearSiteDataLayout" />
<androidx.constraintlayout.widget.Group
android:id="@+id/websitePermissionsGroup"
android:layout_width="wrap_content"
@ -59,5 +74,6 @@
android:visibility="gone"
app:constraint_referenced_ids="websitePermissionsLayout,webSitePermissionsDivider"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/website_clear_site_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/clearSiteData"
style="@style/QuickSettingsLargeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/quicksettings_item_height"
android:paddingHorizontal="8dp"
android:layout_marginStart="16dp"
android:text="@string/clear_site_data" />
</LinearLayout>

View File

@ -1790,6 +1790,10 @@
<string name="quick_settings_sheet_secure_connection_2">Connection is secure</string>
<!-- Label that indicates a site is using a insecure connection -->
<string name="quick_settings_sheet_insecure_connection_2">Connection is not secure</string>
<!-- Label to clear site data -->
<string name="clear_site_data">Clear cookies and site data</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete all data for current site -->
<string name="confirm_clear_site_data"><![CDATA[Are you sure that you want to clear all the cookies and data for the site <b>%s</b>?]]></string>
<!-- Confirmation message for a dialog confirming if the user wants to delete all the permissions for all sites-->
<string name="confirm_clear_permissions_on_all_sites">Are you sure that you want to clear all the permissions on all sites?</string>
<!-- Confirmation message for a dialog confirming if the user wants to delete all the permissions for a site-->

View File

@ -0,0 +1,59 @@
/* 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.view.View
import android.widget.FrameLayout
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.databinding.QuicksettingsClearSiteDataBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class ClearSiteDataViewTest {
private lateinit var view: ClearSiteDataView
private lateinit var binding: QuicksettingsClearSiteDataBinding
private lateinit var interactor: ClearSiteDataViewInteractor
private val coroutinesScope = TestCoroutineScope()
@Before
fun setup() {
MockKAnnotations.init(this)
interactor = mockk(relaxed = true)
view = spyk(
ClearSiteDataView(
testContext,
coroutinesScope,
FrameLayout(testContext),
View(testContext),
interactor
)
)
binding = view.binding
}
@Test
fun `clear site`() {
val state = WebsiteInfoState(
websiteUrl = "https://developers.mozilla.org",
websiteTitle = "Mozilla",
websiteSecurityUiValues = WebsiteSecurityUiValues.SECURE,
certificateName = "Certificate"
)
view.update(state)
binding.clearSiteData.callOnClick()
verify { view.askToClear() }
}
}

View File

@ -21,6 +21,7 @@ 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.concept.engine.Engine
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION
import mozilla.components.feature.session.SessionUseCases
@ -71,6 +72,9 @@ class DefaultQuickSettingsControllerTest {
@MockK(relaxed = true)
private lateinit var permissionStorage: PermissionStorage
@MockK(relaxed = true)
private lateinit var engine: Engine
@MockK(relaxed = true)
private lateinit var reload: SessionUseCases.ReloadUrlUseCase
@ -104,6 +108,7 @@ class DefaultQuickSettingsControllerTest {
reload = reload,
addNewTab = addNewTab,
requestRuntimePermissions = requestPermissions,
engine = engine,
displayPermissions = {},
dismiss = {}
)
@ -384,6 +389,21 @@ class DefaultQuickSettingsControllerTest {
}
}
@Test
fun `WHEN handleClearSiteData THEN call clearSite`() {
controller.handleClearSiteDataClicked("mozilla.org")
verify {
engine.clearData(
host = "mozilla.org",
data = Engine.BrowsingData.select(
Engine.BrowsingData.AUTH_SESSIONS,
Engine.BrowsingData.ALL_SITE_DATA,
)
)
}
}
private fun createController(
requestPermissions: (Array<String>) -> Unit = { _ -> },
displayPermissions: () -> Unit = { },

View File

@ -82,4 +82,13 @@ class QuickSettingsInteractorTest {
controller.handleConnectionDetailsClicked()
}
}
@Test
fun `WHEN calling onClearSiteDataClicked THEN delegate to the controller`() {
interactor.onClearSiteDataClicked("baseDomain")
verify {
controller.handleClearSiteDataClicked("baseDomain")
}
}
}