1
0
Fork 0

Clean up tracking protection fragment (#6532)

* Clean up exceptions fragment

* Clean up tracking protection fragment

* Move saved logins to list adapter
master
Tiger Oakes 2019-11-22 07:38:13 -08:00 committed by Roger Yang
parent 8a330d413c
commit a5e9439c6e
10 changed files with 141 additions and 201 deletions

View File

@ -313,8 +313,8 @@ class CollectionCreationView(
}
}
fun onKey(keyCode: Int, event: KeyEvent?): Boolean {
return if (event?.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
fun onKey(keyCode: Int, event: KeyEvent): Boolean {
return if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
interactor.onBackPressed(step)
true
} else {

View File

@ -6,49 +6,43 @@ package org.mozilla.fenix.exceptions
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder
private sealed class AdapterItem {
sealed class AdapterItem {
object DeleteButton : AdapterItem()
object Header : AdapterItem()
data class Item(val item: ExceptionsItem) : AdapterItem()
}
private class ExceptionsList(val exceptions: List<ExceptionsItem>) {
val items: List<AdapterItem>
init {
val items = mutableListOf<AdapterItem>()
items.add(AdapterItem.Header)
for (exception in exceptions) {
items.add(AdapterItem.Item(exception))
}
items.add(AdapterItem.DeleteButton)
this.items = items
}
}
/**
* Adapter for a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class ExceptionsAdapter(
private val interactor: ExceptionsInteractor
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var exceptionsList: ExceptionsList = ExceptionsList(emptyList())
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
fun updateData(items: List<ExceptionsItem>) {
this.exceptionsList = ExceptionsList(items)
notifyDataSetChanged()
/**
* Change the list of items that are displayed.
* Header and footer items are added to the list as well.
*/
fun updateData(exceptions: List<ExceptionsItem>) {
val adapterItems = mutableListOf<AdapterItem>()
adapterItems.add(AdapterItem.Header)
exceptions.mapTo(adapterItems) { AdapterItem.Item(it) }
adapterItems.add(AdapterItem.DeleteButton)
submitList(adapterItems)
}
override fun getItemCount(): Int = exceptionsList.items.size
override fun getItemViewType(position: Int): Int {
return when (exceptionsList.items[position]) {
is AdapterItem.DeleteButton -> ExceptionsDeleteButtonViewHolder.LAYOUT_ID
is AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> ExceptionsListItemViewHolder.LAYOUT_ID
}
override fun getItemViewType(position: Int) = when (getItem(position)) {
AdapterItem.DeleteButton -> ExceptionsDeleteButtonViewHolder.LAYOUT_ID
AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID
is AdapterItem.Item -> ExceptionsListItemViewHolder.LAYOUT_ID
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -66,10 +60,18 @@ class ExceptionsAdapter(
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ExceptionsListItemViewHolder -> (exceptionsList.items[position] as AdapterItem.Item).also {
holder.bind(it.item)
}
if (holder is ExceptionsListItemViewHolder) {
val adapterItem = getItem(position) as AdapterItem.Item
holder.bind(adapterItem.item)
}
}
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
areContentsTheSame(oldItem, newItem)
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
oldItem == newItem
}
}

View File

@ -22,6 +22,10 @@ import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.settings.SupportUtils
/**
* Displays a list of sites that are exempted from Tracking Protection,
* along with controls to remove the exception.
*/
class ExceptionsFragment : Fragment() {
private lateinit var exceptionsStore: ExceptionsFragmentStore
private lateinit var exceptionsView: ExceptionsView
@ -89,11 +93,8 @@ class ExceptionsFragment : Fragment() {
private fun reloadExceptions() {
trackingProtectionUseCases.fetchExceptions { resultList ->
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(resultList.map {
ExceptionsItem(
it
)
}))
val exceptionsList = resultList.map { ExceptionsItem(it) }
exceptionsStore.dispatch(ExceptionsFragmentAction.Change(exceptionsList))
}
}
}

View File

@ -8,9 +8,9 @@ import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.UnderlineSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.view.*
@ -41,21 +41,20 @@ interface ExceptionsViewInteractor {
* View that contains and configures the Exceptions List
*/
class ExceptionsView(
private val container: ViewGroup,
override val containerView: ViewGroup,
val interactor: ExceptionsInteractor
) : LayoutContainer {
val view: FrameLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_exceptions, container, true)
val view: FrameLayout = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_exceptions, containerView, true)
.findViewById(R.id.exceptions_wrapper)
override val containerView: View?
get() = container
private val exceptionsAdapter = ExceptionsAdapter(interactor)
init {
view.exceptions_list.apply {
adapter = ExceptionsAdapter(interactor)
layoutManager = LinearLayoutManager(container.context)
adapter = exceptionsAdapter
layoutManager = LinearLayoutManager(containerView.context)
}
val learnMoreText = view.exceptions_learn_more.text.toString()
val textWithLink = SpannableString(learnMoreText).apply {
@ -69,9 +68,8 @@ class ExceptionsView(
}
fun update(state: ExceptionsFragmentState) {
view.exceptions_empty_view.visibility =
if (state.items.isEmpty()) View.VISIBLE else View.GONE
view.exceptions_list.visibility = if (state.items.isEmpty()) View.GONE else View.VISIBLE
(view.exceptions_list.adapter as ExceptionsAdapter).updateData(state.items)
view.exceptions_empty_view.isVisible = state.items.isEmpty()
view.exceptions_list.isVisible = state.items.isNotEmpty()
exceptionsAdapter.updateData(state.items)
}
}

View File

@ -13,6 +13,9 @@ import org.mozilla.fenix.exceptions.ExceptionsItem
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
/**
* View holder for a single website that is exempted from Tracking Protection.
*/
class ExceptionsListItemViewHolder(
view: View,
private val interactor: ExceptionsInteractor

View File

@ -6,51 +6,31 @@ package org.mozilla.fenix.logins
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
private sealed class AdapterItem {
data class Item(val item: SavedLoginsItem) : AdapterItem()
}
private class SavedLoginsList(savedLogins: List<SavedLoginsItem>) {
val items: List<AdapterItem> = savedLogins.map { AdapterItem.Item(it) }
}
class SavedLoginsAdapter(
private val interactor: SavedLoginsInteractor
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var savedLoginsList: SavedLoginsList = SavedLoginsList(emptyList())
) : ListAdapter<SavedLoginsItem, SavedLoginsListItemViewHolder>(DiffCallback) {
fun updateData(items: List<SavedLoginsItem>) {
this.savedLoginsList = SavedLoginsList(items)
notifyDataSetChanged()
}
override fun getItemCount(): Int = savedLoginsList.items.size
override fun getItemViewType(position: Int): Int {
return when (savedLoginsList.items[position]) {
is AdapterItem.Item -> SavedLoginsListItemViewHolder.LAYOUT_ID
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedLoginsListItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
SavedLoginsListItemViewHolder.LAYOUT_ID -> SavedLoginsListItemViewHolder(
view,
interactor
)
else -> throw IllegalStateException()
}
return SavedLoginsListItemViewHolder(view, interactor)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is SavedLoginsListItemViewHolder -> (savedLoginsList.items[position] as AdapterItem.Item).also {
holder.bind(it.item)
}
}
override fun onBindViewHolder(holder: SavedLoginsListItemViewHolder, position: Int) {
holder.bind(getItem(position))
}
private object DiffCallback : DiffUtil.ItemCallback<SavedLoginsItem>() {
override fun areItemsTheSame(oldItem: SavedLoginsItem, newItem: SavedLoginsItem) =
oldItem.url == newItem.url
override fun areContentsTheSame(oldItem: SavedLoginsItem, newItem: SavedLoginsItem) =
oldItem == newItem
}
}

View File

@ -14,12 +14,12 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_saved_logins.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
@ -56,7 +56,7 @@ class SavedLoginsFragment : Fragment() {
}
savedLoginsInteractor = SavedLoginsInteractor(::itemClicked)
savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor)
loadAndMapLogins()
lifecycleScope.launch(Main) { loadAndMapLogins() }
return view
}
@ -69,8 +69,10 @@ class SavedLoginsFragment : Fragment() {
}
}
/**
* If we pause this fragment, we want to pop users back to reauth
*/
override fun onPause() {
// If we pause this fragment, we want to pop users back to reauth
if (findNavController().currentDestination?.id != R.id.savedLoginSiteInfoFragment) {
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
findNavController().popBackStack(R.id.loginsFragment, false)
@ -85,22 +87,16 @@ class SavedLoginsFragment : Fragment() {
findNavController().navigate(directions)
}
private fun loadAndMapLogins() {
lifecycleScope.launch(IO) {
val syncedLogins = async {
context!!.components.core.passwordsStorage.withUnlocked {
it.list().await().map { item ->
SavedLoginsItem(
item.hostname,
item.username,
item.password
)
}
private suspend fun loadAndMapLogins() {
val syncedLogins = withContext(IO) {
requireContext().components.core.passwordsStorage.withUnlocked {
it.list().await().map { item ->
SavedLoginsItem(item.hostname, item.username, item.password)
}
}.await()
launch(Dispatchers.Main) {
savedLoginsStore.dispatch(SavedLoginsFragmentAction.UpdateLogins(syncedLogins))
}
}
withContext(Main) {
savedLoginsStore.dispatch(SavedLoginsFragmentAction.UpdateLogins(syncedLogins))
}
}
}

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.logins
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isVisible
@ -29,26 +28,25 @@ interface SavedLoginsViewInteractor {
* View that contains and configures the Saved Logins List
*/
class SavedLoginsView(
private val container: ViewGroup,
override val containerView: ViewGroup,
val interactor: SavedLoginsInteractor
) : LayoutContainer {
val view: FrameLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_saved_logins, container, true)
val view: FrameLayout = LayoutInflater.from(containerView.context)
.inflate(R.layout.component_saved_logins, containerView, true)
.findViewById(R.id.saved_logins_wrapper)
override val containerView: View?
get() = container
private val loginsAdapter = SavedLoginsAdapter(interactor)
init {
view.saved_logins_list.apply {
adapter = SavedLoginsAdapter(interactor)
layoutManager = LinearLayoutManager(container.context)
adapter = loginsAdapter
layoutManager = LinearLayoutManager(containerView.context)
}
}
fun update(state: SavedLoginsFragmentState) {
view.saved_logins_list.isVisible = state.items.isNotEmpty()
(view.saved_logins_list.adapter as SavedLoginsAdapter).updateData(state.items)
loginsAdapter.submitList(state.items)
}
}

View File

@ -5,9 +5,6 @@
package org.mozilla.fenix.trackingprotection
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.TrackingCategory.MOZILLA_SOCIAL
import mozilla.components.concept.engine.content.blocking.Tracker
import mozilla.components.concept.engine.content.blocking.TrackerLog
import org.mozilla.fenix.ext.tryGetHostFromUrl
@ -27,7 +24,7 @@ class TrackerBuckets {
private var trackers = emptyList<TrackerLog>()
data class BucketedTrackerLog(var blockedBucketMap: BucketMap, var loadedBucketMap: BucketMap)
data class BucketedTrackerLog(val blockedBucketMap: BucketMap, val loadedBucketMap: BucketMap)
var buckets: BucketedTrackerLog = BucketedTrackerLog(emptyMap(), emptyMap())
private set
@ -60,78 +57,63 @@ class TrackerBuckets {
if (blocked) buckets.blockedBucketMap[key].orEmpty() else buckets.loadedBucketMap[key].orEmpty()
companion object {
@Suppress("ComplexMethod")
private fun putTrackersInBuckets(
list: List<TrackerLog>
): BucketedTrackerLog {
val blockedMap =
EnumMap<TrackingProtectionCategory, List<String>>(TrackingProtectionCategory::class.java)
val loadedMap =
EnumMap<TrackingProtectionCategory, List<String>>(TrackingProtectionCategory::class.java)
val blockedMap = createMap()
val loadedMap = createMap()
for (item in list) {
if (item.cookiesHasBeenBlocked) {
blockedMap[CROSS_SITE_TRACKING_COOKIES] =
blockedMap[CROSS_SITE_TRACKING_COOKIES].orEmpty() + item.url.tryGetHostFromUrl()
blockedMap.addTrackerHost(CROSS_SITE_TRACKING_COOKIES, item)
}
// Blocked categories
bucketBlockedCategories(item, blockedMap)
for (category in item.blockedCategories) {
blockedMap.addTrackerHost(category, item)
}
// Loaded categories
bucketLoadedCategories(item, loadedMap)
for (category in item.loadedCategories) {
loadedMap.addTrackerHost(category, item)
}
}
return BucketedTrackerLog(blockedMap, loadedMap)
}
private fun bucketLoadedCategories(
item: TrackerLog,
loadedMap: EnumMap<TrackingProtectionCategory, List<String>>
/**
* Create an empty mutable map of [TrackingProtectionCategory] to hostnames.
*/
private fun createMap() =
EnumMap<TrackingProtectionCategory, MutableList<String>>(TrackingProtectionCategory::class.java)
/**
* Add the hostname of the [TrackerLog.url] into the map for the given category
* from Android Components. The category is transformed into a corresponding Fenix bucket,
* and the item is discarded if the category doesn't have a match.
*/
private fun MutableMap<TrackingProtectionCategory, MutableList<String>>.addTrackerHost(
category: TrackingCategory,
tracker: TrackerLog
) {
item.loadedCategories.forEach { category ->
if (CRYPTOMINING == category) {
loadedMap[CRYPTOMINERS] = loadedMap[CRYPTOMINERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (FINGERPRINTING == category) {
loadedMap[FINGERPRINTERS] = loadedMap[FINGERPRINTERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (MOZILLA_SOCIAL == category) {
loadedMap[SOCIAL_MEDIA_TRACKERS] =
loadedMap[SOCIAL_MEDIA_TRACKERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (TrackingCategory.SCRIPTS_AND_SUB_RESOURCES == category) {
loadedMap[TRACKING_CONTENT] = loadedMap[TRACKING_CONTENT].orEmpty() +
item.url.tryGetHostFromUrl()
}
val key = when (category) {
TrackingCategory.CRYPTOMINING -> CRYPTOMINERS
TrackingCategory.FINGERPRINTING -> FINGERPRINTERS
TrackingCategory.MOZILLA_SOCIAL -> SOCIAL_MEDIA_TRACKERS
TrackingCategory.SCRIPTS_AND_SUB_RESOURCES -> TRACKING_CONTENT
else -> return
}
addTrackerHost(key, tracker)
}
private fun bucketBlockedCategories(
item: TrackerLog,
blockedMap: EnumMap<TrackingProtectionCategory, List<String>>
/**
* Add the hostname of the [TrackerLog.url] into the map for the given [TrackingProtectionCategory].
*/
private fun MutableMap<TrackingProtectionCategory, MutableList<String>>.addTrackerHost(
key: TrackingProtectionCategory,
tracker: TrackerLog
) {
item.blockedCategories.forEach { category ->
if (CRYPTOMINING == category) {
blockedMap[CRYPTOMINERS] = blockedMap[CRYPTOMINERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (FINGERPRINTING == category) {
blockedMap[FINGERPRINTERS] = blockedMap[FINGERPRINTERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (MOZILLA_SOCIAL == category) {
blockedMap[SOCIAL_MEDIA_TRACKERS] =
blockedMap[SOCIAL_MEDIA_TRACKERS].orEmpty() +
item.url.tryGetHostFromUrl()
}
if (TrackingCategory.SCRIPTS_AND_SUB_RESOURCES == category) {
blockedMap[TRACKING_CONTENT] = blockedMap[TRACKING_CONTENT].orEmpty() +
item.url.tryGetHostFromUrl()
}
}
getOrPut(key) { mutableListOf() }.add(tracker.url.tryGetHostFromUrl())
}
}
}

View File

@ -18,6 +18,7 @@ import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
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.*
@ -39,28 +40,7 @@ import org.mozilla.fenix.ext.requireComponents
class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHandler {
private val safeArguments get() = requireNotNull(arguments)
private val sessionId: String by lazy {
TrackingProtectionPanelDialogFragmentArgs.fromBundle(
safeArguments
).sessionId
}
private val url: String by lazy {
TrackingProtectionPanelDialogFragmentArgs.fromBundle(safeArguments).url
}
private val trackingProtectionEnabled: Boolean by lazy {
TrackingProtectionPanelDialogFragmentArgs.fromBundle(safeArguments)
.trackingProtectionEnabled
}
private val promptGravity: Int by lazy {
TrackingProtectionPanelDialogFragmentArgs.fromBundle(
safeArguments
).gravity
}
private val args by navArgs<TrackingProtectionPanelDialogFragmentArgs>()
private fun inflateRootView(container: ViewGroup? = null): View {
val contextThemeWrapper = ContextThemeWrapper(
@ -84,16 +64,16 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHan
savedInstanceState: Bundle?
): View? {
val view = inflateRootView(container)
val session = requireComponents.core.sessionManager.findSessionById(sessionId)
val session = requireComponents.core.sessionManager.findSessionById(args.sessionId)
session?.register(sessionObserver, view = view)
trackingProtectionStore = StoreProvider.get(this) {
TrackingProtectionStore(
TrackingProtectionState(
session,
url,
trackingProtectionEnabled,
listOf(),
TrackingProtectionState.Mode.Normal
args.url,
args.trackingProtectionEnabled,
listTrackers = listOf(),
mode = TrackingProtectionState.Mode.Normal
)
)
}
@ -127,7 +107,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHan
private fun updateTrackers() {
context?.let { context ->
val session =
context.components.core.sessionManager.findSessionById(sessionId) ?: return
context.components.core.sessionManager.findSessionById(args.sessionId) ?: return
val useCase = TrackingProtectionUseCases(
sessionManager = context.components.core.sessionManager,
engine = context.components.core.engine
@ -174,7 +154,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHan
sessionManager = context.components.core.sessionManager,
engine = context.components.core.engine
)
val session = context.components.core.sessionManager.findSessionById(sessionId)
val session = context.components.core.sessionManager.findSessionById(args.sessionId)
session?.let {
if (isEnabled) {
useCase.removeException(it)
@ -192,7 +172,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHan
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (promptGravity == Gravity.BOTTOM) {
return if (args.gravity == Gravity.BOTTOM) {
object : BottomSheetDialog(requireContext(), this.theme) {
override fun onBackPressed() {
this@TrackingProtectionPanelDialogFragment.onBackPressed()
@ -224,7 +204,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), BackHan
)
window?.apply {
setGravity(promptGravity)
setGravity(args.gravity)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// This must be called after addContentView, or it won't fully fill to the edge.
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)