/* 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.deletebrowsingdata import android.content.DialogInterface import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.databinding.FragmentDeleteBrowsingDataBinding import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.utils.Settings @SuppressWarnings("TooManyFunctions", "LargeClass") class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_data) { private lateinit var controller: DeleteBrowsingDataController private var scope: CoroutineScope? = null private lateinit var settings: Settings private var _binding: FragmentDeleteBrowsingDataBinding? = null private val binding get() = _binding!! override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val tabsUseCases = requireComponents.useCases.tabsUseCases val downloadUseCases = requireComponents.useCases.downloadUseCases _binding = FragmentDeleteBrowsingDataBinding.bind(view) controller = DefaultDeleteBrowsingDataController( tabsUseCases.removeAllTabs, downloadUseCases.removeAllDownloads, requireComponents.core.historyStorage, requireComponents.core.permissionStorage, requireComponents.core.store, requireComponents.core.icons, requireComponents.core.engine ) settings = requireContext().settings() getCheckboxes().iterator().forEach { it.onCheckListener = { _ -> updateDeleteButton() updatePreference(it) } } getCheckboxes().iterator().forEach { it.isChecked = when (it.id) { R.id.open_tabs_item -> settings.deleteOpenTabs R.id.browsing_data_item -> settings.deleteBrowsingHistory R.id.cookies_item -> settings.deleteCookies R.id.cached_files_item -> settings.deleteCache R.id.site_permissions_item -> settings.deleteSitePermissions R.id.downloads_item -> settings.deleteDownloads else -> true } } binding.deleteData.setOnClickListener { askToDelete() } updateDeleteButton() } private fun updatePreference(it: DeleteBrowsingDataItem) { when (it.id) { R.id.open_tabs_item -> settings.deleteOpenTabs = it.isChecked R.id.browsing_data_item -> settings.deleteBrowsingHistory = it.isChecked R.id.cookies_item -> settings.deleteCookies = it.isChecked R.id.cached_files_item -> settings.deleteCache = it.isChecked R.id.site_permissions_item -> settings.deleteSitePermissions = it.isChecked R.id.downloads_item -> settings.deleteDownloads = it.isChecked else -> return } } override fun onStart() { super.onStart() scope = requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.tabs.size } .ifChanged() .collect { openTabs -> updateTabCount(openTabs) } } } override fun onResume() { super.onResume() showToolbar(getString(R.string.preferences_delete_browsing_data)) getCheckboxes().forEach { it.visibility = View.VISIBLE } updateItemCounts() } private fun updateDeleteButton() { val enabled = getCheckboxes().any { it.isChecked } binding.deleteData.isEnabled = enabled binding.deleteData.alpha = if (enabled) ENABLED_ALPHA else DISABLED_ALPHA } private fun askToDelete() { context?.let { AlertDialog.Builder(it).apply { setMessage( it.getString( R.string.delete_browsing_data_prompt_message_3, it.getString(R.string.app_name) ) ) setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { it: DialogInterface, _ -> it.cancel() } setPositiveButton(R.string.delete_browsing_data_prompt_allow) { it: DialogInterface, _ -> it.dismiss() deleteSelected() } create() }.show() } } private fun deleteSelected() { startDeletion() lifecycleScope.launch(IO) { getCheckboxes().mapIndexed { i, v -> if (v.isChecked) { when (i) { OPEN_TABS_INDEX -> controller.deleteTabs() HISTORY_INDEX -> controller.deleteBrowsingData() COOKIES_INDEX -> controller.deleteCookies() CACHED_INDEX -> controller.deleteCachedFiles() PERMS_INDEX -> controller.deleteSitePermissions() DOWNLOADS_INDEX -> controller.deleteDownloads() } } } withContext(Main) { finishDeletion() requireComponents.analytics.metrics.track(Event.ClearedPrivateData) } } } private fun startDeletion() { binding.progressBar.visibility = View.VISIBLE binding.deleteBrowsingDataWrapper.isEnabled = false binding.deleteBrowsingDataWrapper.isClickable = false binding.deleteBrowsingDataWrapper.alpha = DISABLED_ALPHA } private fun finishDeletion() { val popAfter = binding.openTabsItem.isChecked binding.progressBar.visibility = View.GONE binding.deleteBrowsingDataWrapper.isEnabled = true binding.deleteBrowsingDataWrapper.isClickable = true binding.deleteBrowsingDataWrapper.alpha = ENABLED_ALPHA updateItemCounts() FenixSnackbar.make( view = requireView(), duration = FenixSnackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = true ) .setText(resources.getString(R.string.preferences_delete_browsing_data_snackbar)) .show() if (popAfter) { viewLifecycleOwner.lifecycleScope.launch(Main) { findNavController().apply { // If the user deletes all open tabs we need to make sure we remove // the BrowserFragment from the backstack. popBackStack(R.id.homeFragment, false) navigate(DeleteBrowsingDataFragmentDirections.actionGlobalSettingsFragment()) } } } } override fun onPause() { super.onPause() binding.progressBar.visibility = View.GONE } override fun onStop() { super.onStop() scope?.cancel() } override fun onDestroyView() { super.onDestroyView() _binding = null } private fun updateItemCounts() { updateTabCount() updateHistoryCount() updateCookies() updateCachedImagesAndFiles() updateSitePermissions() } private fun updateTabCount(openTabs: Int = requireComponents.core.store.state.tabs.size) { binding.openTabsItem.apply { subtitleView.text = resources.getString( R.string.preferences_delete_browsing_data_tabs_subtitle, openTabs ) } } private fun updateHistoryCount() { binding.browsingDataItem.subtitleView.text = "" viewLifecycleOwner.lifecycleScope.launch(IO) { val historyCount = requireComponents.core.historyStorage.getVisited().size launch(Main) { binding.browsingDataItem.apply { subtitleView.text = resources.getString( R.string.preferences_delete_browsing_data_browsing_data_subtitle, historyCount ) } } } } private fun updateCookies() { // NO OP until we have GeckoView methods to count cookies } private fun updateCachedImagesAndFiles() { // NO OP until we have GeckoView methods to count cached images and files } private fun updateSitePermissions() { // NO OP until we have GeckoView methods for cookies and cached files, for consistency } private fun getCheckboxes(): List { return listOf( binding.openTabsItem, binding.browsingDataItem, binding.cookiesItem, binding.cachedFilesItem, binding.sitePermissionsItem, binding.downloadsItem ) } companion object { private const val ENABLED_ALPHA = 1f private const val DISABLED_ALPHA = 0.6f private const val OPEN_TABS_INDEX = 0 private const val HISTORY_INDEX = 1 private const val COOKIES_INDEX = 2 private const val CACHED_INDEX = 3 private const val PERMS_INDEX = 4 private const val DOWNLOADS_INDEX = 5 } }