1
0
Fork 0

For #3106: Granular options for clearing user data

master
Colin Lee 2019-09-04 15:27:30 -05:00 committed by Emily Kager
parent eb26d951ab
commit 2b9efccfca
10 changed files with 340 additions and 77 deletions

View File

@ -329,6 +329,7 @@ dependencies {
implementation Deps.kotlin_stdlib
implementation Deps.kotlin_coroutines
testImplementation Deps.kotlin_coroutines_test
implementation Deps.androidx_appcompat
implementation Deps.androidx_constraintlayout
implementation Deps.androidx_coordinatorlayout

View File

@ -36,4 +36,10 @@ object FeatureFlags {
* https://github.com/mozilla-mobile/fenix/issues/4431
*/
const val mediaIntegration = true
/**
* Granular data deletion provides additional choices on the Delete Browsing Data
* setting screen for cookies, cached images and files, and site permissions.
*/
val granularDataDeletion = nightly or debug
}

View File

@ -0,0 +1,76 @@
/* 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
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.ext.components
import kotlin.coroutines.CoroutineContext
interface DeleteBrowsingDataController {
suspend fun deleteTabs()
suspend fun deleteBrowsingData()
suspend fun deleteCollections(collections: List<TabCollection>)
suspend fun deleteCookies()
suspend fun deleteCachedFiles()
suspend fun deleteSitePermissions()
}
class DefaultDeleteBrowsingDataController(
val context: Context,
val coroutineContext: CoroutineContext = Dispatchers.Main
) : DeleteBrowsingDataController {
override suspend fun deleteTabs() {
withContext(coroutineContext) {
context.components.useCases.tabsUseCases.removeAllTabs.invoke()
}
}
override suspend fun deleteBrowsingData() {
withContext(coroutineContext) {
context.components.core.engine.clearData(Engine.BrowsingData.all())
}
context.components.core.historyStorage.deleteEverything()
}
override suspend fun deleteCollections(collections: List<TabCollection>) {
while (context.components.core.tabCollectionStorage.getTabCollectionsCount() != collections.size) {
delay(DELAY_IN_MILLIS)
}
collections.forEach { context.components.core.tabCollectionStorage.removeCollection(it) }
}
override suspend fun deleteCookies() {
withContext(coroutineContext) {
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.COOKIES))
}
}
override suspend fun deleteCachedFiles() {
withContext(coroutineContext) {
context.components.core.engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES)
)
}
}
override suspend fun deleteSitePermissions() {
withContext(coroutineContext) {
context.components.core.engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS)
)
}
context.components.core.permissionStorage.deleteAllSitePermissions()
}
companion object {
private const val DELAY_IN_MILLIS = 500L
}
}

View File

@ -15,16 +15,17 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.paging.PagedList
import androidx.paging.toLiveData
import kotlinx.android.synthetic.main.fragment_delete_browsing_data.*
import kotlinx.android.synthetic.main.fragment_delete_browsing_data.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
@ -34,6 +35,7 @@ import org.mozilla.fenix.ext.requireComponents
class DeleteBrowsingDataFragment : Fragment() {
private lateinit var sessionObserver: SessionManager.Observer
private var tabCollections: List<TabCollection> = listOf()
private lateinit var controller: DeleteBrowsingDataController
override fun onCreateView(
inflater: LayoutInflater,
@ -45,6 +47,8 @@ class DeleteBrowsingDataFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controller = DefaultDeleteBrowsingDataController(context!!)
sessionObserver = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) = updateTabCount()
override fun onSessionRemoved(session: Session) = updateTabCount()
@ -60,9 +64,9 @@ class DeleteBrowsingDataFragment : Fragment() {
})
}
view.open_tabs_item?.onCheckListener = { _ -> updateDeleteButton() }
view.browsing_data_item?.onCheckListener = { _ -> updateDeleteButton() }
view.collections_item?.onCheckListener = { _ -> updateDeleteButton() }
getCheckboxes().forEach {
it.onCheckListener = { _ -> updateDeleteButton() }
}
view.delete_data?.setOnClickListener {
askToDelete()
}
@ -75,10 +79,11 @@ class DeleteBrowsingDataFragment : Fragment() {
supportActionBar?.show()
}
updateTabCount()
updateHistoryCount()
updateCollectionsCount()
updateDeleteButton()
getCheckboxes().forEach {
it.visibility = View.VISIBLE
}
updateItemCounts()
}
private fun askToDelete() {
@ -100,15 +105,20 @@ class DeleteBrowsingDataFragment : Fragment() {
}
private fun deleteSelected() {
val openTabsChecked = view!!.open_tabs_item!!.isChecked
val browsingDataChecked = view!!.browsing_data_item!!.isChecked
val collectionsChecked = view!!.collections_item!!.isChecked
startDeletion()
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
if (openTabsChecked) deleteTabs()
if (browsingDataChecked) deleteBrowsingData()
if (collectionsChecked) deleteCollections()
getCheckboxes().mapIndexed { i, v ->
if (v.isChecked) {
when (i) {
OPEN_TABS_INDEX -> controller.deleteTabs()
HISTORY_INDEX -> controller.deleteBrowsingData()
COLLECTIONS_INDEX -> controller.deleteCollections(tabCollections)
COOKIES_INDEX -> controller.deleteCookies()
CACHED_INDEX -> controller.deleteCachedFiles()
PERMS_INDEX -> controller.deleteSitePermissions()
}
}
}
launch(Dispatchers.Main) {
finishDeletion()
@ -131,28 +141,34 @@ class DeleteBrowsingDataFragment : Fragment() {
delete_browsing_data_wrapper.isClickable = true
delete_browsing_data_wrapper.alpha = ENABLED_ALPHA
listOf(open_tabs_item, browsing_data_item, collections_item).forEach {
getCheckboxes().forEach {
it.isChecked = false
}
updateTabCount()
updateHistoryCount()
updateCollectionsCount()
updateItemCounts()
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_SHORT)
.setText(resources.getString(R.string.preferences_delete_browsing_data_snackbar))
.show()
if (popAfter) viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
if (popAfter || FeatureFlags.granularDataDeletion) viewLifecycleOwner.lifecycleScope.launch(
Dispatchers.Main
) {
findNavController().popBackStack(R.id.homeFragment, false)
}
}
private fun updateItemCounts() {
updateTabCount()
updateHistoryCount()
updateCollectionsCount()
updateCookies()
updateCachedImagesAndFiles()
updateSitePermissions()
}
private fun updateDeleteButton() {
val openTabs = view!!.open_tabs_item!!.isChecked
val browsingData = view!!.browsing_data_item!!.isChecked
val collections = view!!.collections_item!!.isChecked
val enabled = openTabs || browsingData || collections
val enabled = getCheckboxes().any { it.isChecked }
view?.delete_data?.isEnabled = enabled
view?.delete_data?.alpha = if (enabled) ENABLED_ALPHA else DISABLED_ALPHA
@ -206,30 +222,52 @@ class DeleteBrowsingDataFragment : Fragment() {
}
}
private suspend fun deleteTabs() {
withContext(Dispatchers.Main) {
requireComponents.useCases.tabsUseCases.removeAllTabs.invoke()
}
private fun updateCookies() {
// NO OP until we have GeckoView methods to count cookies
}
private suspend fun deleteBrowsingData() {
withContext(Dispatchers.Main) {
requireComponents.core.engine.clearData(Engine.BrowsingData.all())
}
requireComponents.core.historyStorage.deleteEverything()
private fun updateCachedImagesAndFiles() {
// NO OP until we have GeckoView methods to count cached images and files
}
private suspend fun deleteCollections() {
while (requireComponents.core.tabCollectionStorage.getTabCollectionsCount() != tabCollections.size) {
delay(DELAY_IN_MILLIS)
}
private fun updateSitePermissions() {
val liveData =
requireComponents.core.permissionStorage.getSitePermissionsPaged().toLiveData(1)
liveData.observe(
this,
object : Observer<PagedList<SitePermissions>> {
override fun onChanged(list: PagedList<SitePermissions>?) {
view?.site_permissions_item?.isEnabled = !list.isNullOrEmpty()
liveData.removeObserver(this)
}
})
}
tabCollections.forEach { requireComponents.core.tabCollectionStorage.removeCollection(it) }
private fun getCheckboxes(): List<DeleteBrowsingDataItem> {
val fragmentView = view!!
val originalList = listOf(
fragmentView.open_tabs_item,
fragmentView.browsing_data_item,
fragmentView.collections_item
)
@Suppress("ConstantConditionIf")
val granularList = if (FeatureFlags.granularDataDeletion) listOf(
fragmentView.cookies_item,
fragmentView.cached_files_item,
fragmentView.site_permissions_item
) else emptyList()
return originalList + granularList
}
companion object {
private const val ENABLED_ALPHA = 1f
private const val DISABLED_ALPHA = 0.6f
private const val DELAY_IN_MILLIS = 500L
private const val OPEN_TABS_INDEX = 0
private const val HISTORY_INDEX = 1
private const val COLLECTIONS_INDEX = 2
private const val COOKIES_INDEX = 3
private const val CACHED_INDEX = 4
private const val PERMS_INDEX = 5
}
}

View File

@ -48,20 +48,15 @@ class DeleteBrowsingDataItem @JvmOverloads constructor(
}
context.withStyledAttributes(attrs, R.styleable.DeleteBrowsingDataItem, defStyleAttr, 0) {
val iconId = getResourceId(
R.styleable.DeleteBrowsingDataItem_deleteBrowsingDataItemIcon,
R.drawable.library_icon_reading_list_circle_background
)
val titleId = getResourceId(
R.styleable.DeleteBrowsingDataItem_deleteBrowsingDataItemTitle,
R.string.browser_menu_your_library
)
val subtitleId = getResourceId(
R.styleable.DeleteBrowsingDataItem_deleteBrowsingDataItemSubtitle,
R.string.browser_menu_your_library
R.string.empty_string
)
icon.background = resources.getDrawable(iconId, context.theme)
title.text = resources.getString(titleId)
subtitle.text = resources.getString(subtitleId)
}

View File

@ -9,17 +9,12 @@
android:layout_height="@dimen/library_item_height"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/library_item_icon_height"
android:layout_height="@dimen/library_item_icon_height"
android:layout_marginStart="@dimen/library_item_icon_margin_horizontal"
android:layout_marginTop="@dimen/library_item_icon_margin_vertical"
android:layout_marginEnd="@dimen/library_item_icon_margin_horizontal"
android:layout_marginBottom="@dimen/library_item_icon_margin_vertical"
android:background="@drawable/library_icon_reading_list_circle_background"
<CheckBox
android:id="@+id/checkbox"
android:clickable="false"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -32,9 +27,9 @@
android:textAppearance="@style/ListItemTextStyle"
android:clickable="false"
android:layout_marginTop="16dp"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintStart_toEndOf="@id/checkbox"
app:layout_constraintTop_toTopOf="parent"
tools:text="Open Tabs" />
<TextView
android:id="@+id/subtitle"
@ -43,17 +38,7 @@
android:layout_marginStart="@dimen/library_item_icon_margin_horizontal"
android:textAppearance="@style/SubtitleTextStyle"
android:clickable="false"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintTop_toBottomOf="@id/title" />
<CheckBox
android:id="@+id/checkbox"
android:clickable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintStart_toEndOf="@id/checkbox"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="2 Open Tabs" />
</merge>

View File

@ -36,7 +36,6 @@
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:deleteBrowsingDataItemIcon="@drawable/ic_tab_circle_background"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_tabs_title"
app:deleteBrowsingDataItemSubtitle="@string/preferences_delete_browsing_data_tabs_subtitle" />
<org.mozilla.fenix.settings.DeleteBrowsingDataItem
@ -46,7 +45,6 @@
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:deleteBrowsingDataItemIcon="@drawable/library_icon_history_circle_background"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_browsing_data_title"
app:deleteBrowsingDataItemSubtitle="@string/preferences_delete_browsing_data_browsing_data_subtitle" />
<org.mozilla.fenix.settings.DeleteBrowsingDataItem
@ -56,9 +54,37 @@
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:deleteBrowsingDataItemIcon="@drawable/ic_collections_circle_background"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_collections_title"
app:deleteBrowsingDataItemSubtitle="@string/preferences_delete_browsing_data_collections_subtitle" />
<org.mozilla.fenix.settings.DeleteBrowsingDataItem
android:id="@+id/cookies_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_cookies"
app:deleteBrowsingDataItemSubtitle="@string/preferences_delete_browsing_data_cookies_subtitle" />
<org.mozilla.fenix.settings.DeleteBrowsingDataItem
android:id="@+id/cached_files_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_cached_files"
app:deleteBrowsingDataItemSubtitle="@string/preferences_delete_browsing_data_cached_files_subtitle" />
<org.mozilla.fenix.settings.DeleteBrowsingDataItem
android:id="@+id/site_permissions_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
app:deleteBrowsingDataItemTitle="@string/preferences_delete_browsing_data_site_permissions" />
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_data"
style="@style/ThemeIndependentMaterialGreyButtonDestructive"

View File

@ -21,4 +21,7 @@
<!-- 1.0.1 strings -->
<!-- Bookmark deletion confirmation -->
<string name="bookmark_deletion_confirmation" translatable="false">Are you sure you want to delete this bookmark?</string>
<!--suppress CheckTagEmptyBody This is a default value for places where we don't want a string set-->
<string name="empty_string" translatable="false"></string>
</resources>

View File

@ -0,0 +1,132 @@
/* 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
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.TabCollection
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.TestApplication
import org.mozilla.fenix.ext.components
import org.robolectric.annotation.Config
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class)
class DefaultDeleteBrowsingDataControllerTest {
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private val context: Context = mockk(relaxed = true)
private lateinit var controller: DefaultDeleteBrowsingDataController
@Before
fun setup() {
Dispatchers.setMain(mainThreadSurrogate)
every { context.components.core.engine.clearData(any()) } just Runs
}
@After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
@Test
fun deleteTabs() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
every { context.components.useCases.tabsUseCases.removeAllTabs.invoke() } just Runs
controller.deleteTabs()
verify {
context.components.useCases.tabsUseCases.removeAllTabs.invoke()
}
}
@Test
fun deleteBrowsingData() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
every { context.components.core.historyStorage } returns mockk(relaxed = true)
controller.deleteBrowsingData()
verify {
context.components.core.engine.clearData(Engine.BrowsingData.all())
context.components.core.historyStorage
}
}
@Test
fun deleteCollections() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
val collections: List<TabCollection> = listOf(mockk(relaxed = true))
every { context.components.core.tabCollectionStorage.getTabCollectionsCount() } returns 1
controller.deleteCollections(collections)
verify {
context.components.core.tabCollectionStorage.removeCollection(collections[0])
}
}
@Test
fun deleteCookies() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
controller.deleteCookies()
verify {
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.COOKIES))
}
}
@Test
fun deleteCachedFiles() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
controller.deleteCachedFiles()
verify {
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES))
}
}
@Test
fun deleteSitePermissions() = runBlockingTest {
controller = DefaultDeleteBrowsingDataController(context, coroutineContext)
every { context.components.core.permissionStorage.deleteAllSitePermissions() } just Runs
launch(IO) {
controller.deleteSitePermissions()
}
verify {
context.components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS))
context.components.core.permissionStorage.deleteAllSitePermissions()
}
}
}

View File

@ -4,7 +4,7 @@
object Versions {
const val kotlin = "1.3.30"
const val coroutines = "1.3.0-RC2"
const val coroutines = "1.3.1"
const val android_gradle_plugin = "3.5.0"
const val newest_r8 = "ceaee94e172c6c057cc05e646f5324853fc5d4c5"
const val rxAndroid = "2.1.0"
@ -73,6 +73,7 @@ object Deps {
const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
const val kotlin_coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val kotlin_coroutines_test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
const val kotlin_coroutines_android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
const val allopen = "org.jetbrains.kotlin:kotlin-allopen:${Versions.kotlin}"