parent
79fcb97ed2
commit
84a7430e57
|
@ -9,14 +9,45 @@ import mozilla.components.browser.errorpages.ErrorPages
|
||||||
import mozilla.components.browser.errorpages.ErrorType
|
import mozilla.components.browser.errorpages.ErrorType
|
||||||
import mozilla.components.concept.engine.EngineSession
|
import mozilla.components.concept.engine.EngineSession
|
||||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionDomains
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||||
override fun onLoadRequest(session: EngineSession, uri: String): RequestInterceptor.InterceptionResponse? {
|
override fun onLoadRequest(session: EngineSession, uri: String): RequestInterceptor.InterceptionResponse? {
|
||||||
|
adjustTrackingProtection(uri, context)
|
||||||
// Accounts uses interception to check for a "success URL" in the sign-in flow to finalize authentication.
|
// Accounts uses interception to check for a "success URL" in the sign-in flow to finalize authentication.
|
||||||
return context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri)
|
return context.components.services.accountsAuthFeature.interceptor.onLoadRequest(session, uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun adjustTrackingProtection(url: String, context: Context) {
|
||||||
|
val host = try {
|
||||||
|
URL(url).host
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
val trackingProtectionException = ExceptionDomains.load(context).contains(host)
|
||||||
|
val trackingProtectionEnabled = Settings.getInstance(context).shouldUseTrackingProtection
|
||||||
|
val core = context.components.core
|
||||||
|
if (trackingProtectionException || !trackingProtectionEnabled) {
|
||||||
|
with(core.sessionManager) {
|
||||||
|
selectedSession?.let {
|
||||||
|
getEngineSession(it)?.disableTrackingProtection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val policy = core.createTrackingProtectionPolicy(normalMode = true)
|
||||||
|
core.engine.settings.trackingProtectionPolicy = policy
|
||||||
|
with(core.sessionManager) {
|
||||||
|
selectedSession?.let {
|
||||||
|
getEngineSession(it)?.enableTrackingProtection(policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onErrorRequest(
|
override fun onErrorRequest(
|
||||||
session: EngineSession,
|
session: EngineSession,
|
||||||
errorType: ErrorType,
|
errorType: ErrorType,
|
||||||
|
@ -27,8 +58,10 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||||
val htmlResource = getPageForRiskLevel(riskLevel)
|
val htmlResource = getPageForRiskLevel(riskLevel)
|
||||||
val cssResource = getStyleForRiskLevel(riskLevel)
|
val cssResource = getStyleForRiskLevel(riskLevel)
|
||||||
|
|
||||||
return RequestInterceptor.ErrorResponse(ErrorPages
|
return RequestInterceptor.ErrorResponse(
|
||||||
.createErrorPage(context, errorType, uri = uri, htmlResource = htmlResource, cssResource = cssResource))
|
ErrorPages
|
||||||
|
.createErrorPage(context, errorType, uri = uri, htmlResource = htmlResource, cssResource = cssResource)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPageForRiskLevel(riskLevel: RiskLevel): Int {
|
private fun getPageForRiskLevel(riskLevel: RiskLevel): Int {
|
||||||
|
|
|
@ -667,7 +667,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope,
|
||||||
val quickSettingsSheet = QuickSettingsSheetDialogFragment.newInstance(
|
val quickSettingsSheet = QuickSettingsSheetDialogFragment.newInstance(
|
||||||
url = session.url,
|
url = session.url,
|
||||||
isSecured = session.securityInfo.secure,
|
isSecured = session.securityInfo.secure,
|
||||||
isTrackingProtectionOn = Settings.getInstance(context!!).shouldUseTrackingProtection,
|
isTrackingProtectionOn = session.trackerBlockingEnabled,
|
||||||
sitePermissions = sitePermissions,
|
sitePermissions = sitePermissions,
|
||||||
gravity = getAppropriateLayoutGravity()
|
gravity = getAppropriateLayoutGravity()
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.mozilla.fenix.exceptions
|
||||||
|
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functionality to manage custom domains for allow-list.
|
||||||
|
*/
|
||||||
|
object ExceptionDomains {
|
||||||
|
private const val PREFERENCE_NAME = "exceptions"
|
||||||
|
private const val KEY_DOMAINS = "exceptions_domains"
|
||||||
|
private const val SEPARATOR = "@<;>@"
|
||||||
|
|
||||||
|
private var exceptions: List<String>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the previously added/saved custom domains from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @return list of custom domains
|
||||||
|
*/
|
||||||
|
fun load(context: Context): List<String> {
|
||||||
|
if (exceptions == null) {
|
||||||
|
exceptions = (preferences(context)
|
||||||
|
.getString(KEY_DOMAINS, "") ?: "")
|
||||||
|
.split(SEPARATOR)
|
||||||
|
.filter { !it.isEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return exceptions ?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the provided domains to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains list of domains
|
||||||
|
*/
|
||||||
|
fun save(context: Context, domains: List<String>) {
|
||||||
|
exceptions = domains
|
||||||
|
|
||||||
|
preferences(context)
|
||||||
|
.edit()
|
||||||
|
.putString(KEY_DOMAINS, domains.joinToString(separator = SEPARATOR))
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the provided domain to preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domain the domain to add
|
||||||
|
*/
|
||||||
|
fun add(context: Context, domain: String) {
|
||||||
|
val domains = mutableListOf<String>()
|
||||||
|
domains.addAll(load(context))
|
||||||
|
domains.add(domain)
|
||||||
|
|
||||||
|
save(context, domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the provided domain from preferences.
|
||||||
|
*
|
||||||
|
* @param context the application context
|
||||||
|
* @param domains the domain to remove
|
||||||
|
*/
|
||||||
|
fun remove(context: Context, domains: List<String>) {
|
||||||
|
save(context, load(context) - domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preferences(context: Context): SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* 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.exceptions
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.reactivex.Observer
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExceptionsAdapter(
|
||||||
|
private val actionEmitter: Observer<ExceptionsAction>
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
private var exceptionsList: ExceptionsList = ExceptionsList(emptyList())
|
||||||
|
private lateinit var job: Job
|
||||||
|
|
||||||
|
fun updateData(items: List<ExceptionsItem>) {
|
||||||
|
this.exceptionsList = ExceptionsList(items)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
|
|
||||||
|
return when (viewType) {
|
||||||
|
ExceptionsDeleteButtonViewHolder.LAYOUT_ID -> ExceptionsDeleteButtonViewHolder(view, actionEmitter)
|
||||||
|
ExceptionsHeaderViewHolder.LAYOUT_ID -> ExceptionsHeaderViewHolder(view)
|
||||||
|
ExceptionsListItemViewHolder.LAYOUT_ID -> ExceptionsListItemViewHolder(view, actionEmitter, job)
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is ExceptionsListItemViewHolder -> (exceptionsList.items[position] as AdapterItem.Item).also {
|
||||||
|
holder.bind(it.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
|
job = Job()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
super.onDetachedFromRecyclerView(recyclerView)
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* 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.exceptions
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.mozilla.fenix.mvi.Action
|
||||||
|
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||||
|
import org.mozilla.fenix.mvi.Change
|
||||||
|
import org.mozilla.fenix.mvi.UIComponent
|
||||||
|
import org.mozilla.fenix.mvi.ViewState
|
||||||
|
import org.mozilla.fenix.test.Mockable
|
||||||
|
|
||||||
|
data class ExceptionsItem(val url: String)
|
||||||
|
|
||||||
|
@Mockable
|
||||||
|
class ExceptionsComponent(
|
||||||
|
private val container: ViewGroup,
|
||||||
|
bus: ActionBusFactory,
|
||||||
|
override var initialState: ExceptionsState = ExceptionsState(emptyList())
|
||||||
|
) :
|
||||||
|
UIComponent<ExceptionsState, ExceptionsAction, ExceptionsChange>(
|
||||||
|
bus.getManagedEmitter(ExceptionsAction::class.java),
|
||||||
|
bus.getSafeManagedObservable(ExceptionsChange::class.java)
|
||||||
|
) {
|
||||||
|
|
||||||
|
override val reducer: (ExceptionsState, ExceptionsChange) -> ExceptionsState = { state, change ->
|
||||||
|
when (change) {
|
||||||
|
is ExceptionsChange.Change -> state.copy(items = change.list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initView() = ExceptionsUIView(container, actionEmitter, changesObservable)
|
||||||
|
|
||||||
|
init {
|
||||||
|
render(reducer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExceptionsState(val items: List<ExceptionsItem>) : ViewState
|
||||||
|
|
||||||
|
sealed class ExceptionsAction : Action {
|
||||||
|
sealed class Delete : ExceptionsAction() {
|
||||||
|
object All : Delete()
|
||||||
|
data class One(val item: ExceptionsItem) : Delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ExceptionsChange : Change {
|
||||||
|
data class Change(val list: List<ExceptionsItem>) : ExceptionsChange()
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package org.mozilla.fenix.exceptions
|
||||||
|
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import kotlinx.android.synthetic.main.fragment_exceptions.view.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||||
|
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
||||||
|
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class ExceptionsFragment : Fragment(), CoroutineScope {
|
||||||
|
private var job = Job()
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = job + Dispatchers.Main
|
||||||
|
private lateinit var exceptionsComponent: ExceptionsComponent
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
job = Job()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
activity?.title = getString(R.string.preference_exceptions)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_exceptions, container, false)
|
||||||
|
exceptionsComponent = ExceptionsComponent(
|
||||||
|
view.exceptions_layout,
|
||||||
|
ActionBusFactory.get(this),
|
||||||
|
ExceptionsState(loadAndMapExceptions())
|
||||||
|
)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
getAutoDisposeObservable<ExceptionsAction>()
|
||||||
|
.subscribe {
|
||||||
|
when (it) {
|
||||||
|
is ExceptionsAction.Delete.All -> launch(Dispatchers.IO) {
|
||||||
|
val domains = ExceptionDomains.load(context!!)
|
||||||
|
ExceptionDomains.remove(context!!, domains)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
view?.let { view: View -> Navigation.findNavController(view).navigateUp() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ExceptionsAction.Delete.One -> launch(Dispatchers.IO) {
|
||||||
|
ExceptionDomains.remove(context!!, listOf(it.item.url))
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAndMapExceptions(): List<ExceptionsItem> {
|
||||||
|
return ExceptionDomains.load(context!!)
|
||||||
|
.map { item ->
|
||||||
|
ExceptionsItem(
|
||||||
|
item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun reloadData() {
|
||||||
|
val items = loadAndMapExceptions()
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
view?.let { view: View -> Navigation.findNavController(view).navigateUp() }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
getManagedEmitter<ExceptionsChange>().onNext(ExceptionsChange.Change(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* 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.exceptions
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.Observer
|
||||||
|
import io.reactivex.functions.Consumer
|
||||||
|
import kotlinx.android.synthetic.main.component_exceptions.view.*
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.mvi.UIView
|
||||||
|
|
||||||
|
class ExceptionsUIView(
|
||||||
|
container: ViewGroup,
|
||||||
|
actionEmitter: Observer<ExceptionsAction>,
|
||||||
|
changesObservable: Observable<ExceptionsChange>
|
||||||
|
) :
|
||||||
|
UIView<ExceptionsState, ExceptionsAction, ExceptionsChange>(container, actionEmitter, changesObservable) {
|
||||||
|
|
||||||
|
override val view: LinearLayout = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_exceptions, container, true)
|
||||||
|
.findViewById(R.id.exceptions_wrapper)
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.exceptions_list.apply {
|
||||||
|
adapter = ExceptionsAdapter(actionEmitter)
|
||||||
|
layoutManager = LinearLayoutManager(container.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateView() = Consumer<ExceptionsState> {
|
||||||
|
(view.exceptions_list.adapter as ExceptionsAdapter).updateData(it.items)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* 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.exceptions.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.reactivex.Observer
|
||||||
|
import kotlinx.android.synthetic.main.delete_exceptions_button.view.*
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionsAction
|
||||||
|
|
||||||
|
class ExceptionsDeleteButtonViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val actionEmitter: Observer<ExceptionsAction>
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
private val deleteButton = view.removeAllExceptions
|
||||||
|
|
||||||
|
init {
|
||||||
|
deleteButton.setOnClickListener {
|
||||||
|
actionEmitter.onNext(ExceptionsAction.Delete.All)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.delete_exceptions_button
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* 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.exceptions.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
class ExceptionsHeaderViewHolder(
|
||||||
|
view: View
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.exceptions_description
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/* 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.exceptions.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.reactivex.Observer
|
||||||
|
import kotlinx.android.synthetic.main.exception_item.view.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.icons.IconRequest
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionsAction
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionsItem
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class ExceptionsListItemViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val actionEmitter: Observer<ExceptionsAction>,
|
||||||
|
val job: Job
|
||||||
|
) : RecyclerView.ViewHolder(view), CoroutineScope {
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.IO + job
|
||||||
|
|
||||||
|
private val favicon = view.favicon_image
|
||||||
|
private val url = view.domainView
|
||||||
|
private val deleteButton = view.delete_exception
|
||||||
|
|
||||||
|
private var item: ExceptionsItem? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
deleteButton.setOnClickListener {
|
||||||
|
item?.let {
|
||||||
|
actionEmitter.onNext(ExceptionsAction.Delete.One(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(item: ExceptionsItem) {
|
||||||
|
this.item = item
|
||||||
|
url.text = item.url
|
||||||
|
updateFavIcon(item.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFavIcon(url: String) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val bitmap = favicon.context.components.utils.icons
|
||||||
|
.loadIcon(IconRequest(url)).await().bitmap
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
favicon.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.exception_item
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,43 +6,66 @@ package org.mozilla.fenix.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.navigation.Navigation
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.SwitchPreference
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionDomains
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
class TrackingProtectionFragment : PreferenceFragmentCompat() {
|
class TrackingProtectionFragment : PreferenceFragmentCompat() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
(activity as AppCompatActivity).title = getString(R.string.preferences_tracking_protection)
|
|
||||||
(activity as AppCompatActivity).supportActionBar?.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.tracking_protection_preferences, rootKey)
|
setPreferencesFromResource(R.xml.tracking_protection_preferences, rootKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
activity?.title = getString(R.string.preferences_tracking_protection)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
|
|
||||||
// Tracking Protection Switch
|
// Tracking Protection Switch
|
||||||
val trackingProtectionKey =
|
val trackingProtectionKey =
|
||||||
context!!.getPreferenceKey(R.string.pref_key_tracking_protection)
|
context!!.getPreferenceKey(R.string.pref_key_tracking_protection)
|
||||||
val preferenceTP = findPreference<Preference>(trackingProtectionKey)
|
val preferenceTP = findPreference<SwitchPreference>(trackingProtectionKey)
|
||||||
preferenceTP?.onPreferenceChangeListener =
|
preferenceTP?.onPreferenceChangeListener =
|
||||||
Preference.OnPreferenceChangeListener { _, newValue ->
|
Preference.OnPreferenceChangeListener { _, newValue ->
|
||||||
|
Settings.getInstance(context!!).setTrackingProtection(newValue = newValue as Boolean)
|
||||||
with(requireComponents.core) {
|
with(requireComponents.core) {
|
||||||
val policy =
|
val policy =
|
||||||
createTrackingProtectionPolicy(newValue as Boolean)
|
createTrackingProtectionPolicy(newValue)
|
||||||
engine.settings.trackingProtectionPolicy = policy
|
engine.settings.trackingProtectionPolicy = policy
|
||||||
|
|
||||||
with(sessionManager) {
|
with(sessionManager) {
|
||||||
sessions.forEach { getEngineSession(it)?.enableTrackingProtection(policy) }
|
sessions.forEach {
|
||||||
|
if (newValue)
|
||||||
|
getEngineSession(it)?.enableTrackingProtection(policy) else
|
||||||
|
getEngineSession(it)?.disableTrackingProtection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requireContext().components.useCases.sessionUseCases.reload.invoke()
|
requireContext().components.useCases.sessionUseCases.reload.invoke()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context?.let {
|
||||||
|
val exceptionsEmpty = ExceptionDomains.load(it).isEmpty()
|
||||||
|
val exceptions =
|
||||||
|
it.getPreferenceKey(R.string.pref_key_tracking_protection_exceptions)
|
||||||
|
val preferenceExceptions = findPreference<Preference>(exceptions)
|
||||||
|
preferenceExceptions.shouldDisableView = true
|
||||||
|
preferenceExceptions.isEnabled = !exceptionsEmpty
|
||||||
|
preferenceExceptions?.onPreferenceClickListener = getClickListenerForExceptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClickListenerForExceptions(): Preference.OnPreferenceClickListener {
|
||||||
|
return Preference.OnPreferenceClickListener {
|
||||||
|
val directions = TrackingProtectionFragmentDirections.actionTrackingProtectionFragmentToExceptionsFragment()
|
||||||
|
Navigation.findNavController(view!!).navigate(directions)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,15 @@ import mozilla.components.feature.sitepermissions.SitePermissions
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.BrowserFragment
|
import org.mozilla.fenix.browser.BrowserFragment
|
||||||
|
import org.mozilla.fenix.exceptions.ExceptionDomains
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||||
import org.mozilla.fenix.settings.PhoneFeature
|
import org.mozilla.fenix.settings.PhoneFeature
|
||||||
import org.mozilla.fenix.utils.Settings
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
private const val KEY_URL = "KEY_URL"
|
private const val KEY_URL = "KEY_URL"
|
||||||
|
@ -174,19 +176,17 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
|
||||||
private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) =
|
private fun arePermissionsGranted(requestCode: Int, grantResults: IntArray) =
|
||||||
requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED }
|
requestCode == REQUEST_CODE_QUICK_SETTINGS_PERMISSIONS && grantResults.all { it == PERMISSION_GRANTED }
|
||||||
|
|
||||||
private fun toggleTrackingProtection(trackingEnabled: Boolean, context: Context) {
|
private fun toggleTrackingProtection(context: Context, url: String) {
|
||||||
with(requireComponents.core) {
|
val host = try {
|
||||||
val policy =
|
URL(url).host
|
||||||
createTrackingProtectionPolicy(trackingEnabled)
|
} catch (e: MalformedURLException) {
|
||||||
Settings.getInstance(context).setTrackingProtection(trackingEnabled)
|
url
|
||||||
engine.settings.trackingProtectionPolicy = policy
|
|
||||||
|
|
||||||
with(sessionManager) {
|
|
||||||
sessions.forEach {
|
|
||||||
getEngineSession(it)?.enableTrackingProtection(
|
|
||||||
policy
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
if (!ExceptionDomains.load(context).contains(host)) {
|
||||||
|
ExceptionDomains.add(context, host)
|
||||||
|
} else {
|
||||||
|
ExceptionDomains.remove(context, listOf(host))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment(), CoroutineSco
|
||||||
}
|
}
|
||||||
is QuickSettingsAction.ToggleTrackingProtection -> {
|
is QuickSettingsAction.ToggleTrackingProtection -> {
|
||||||
val trackingEnabled = it.trackingProtection
|
val trackingEnabled = it.trackingProtection
|
||||||
context?.let { toggleTrackingProtection(trackingEnabled, it) }
|
context?.let { context: Context -> toggleTrackingProtection(context, url) }
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
getManagedEmitter<QuickSettingsChange>().onNext(
|
getManagedEmitter<QuickSettingsChange>().onNext(
|
||||||
QuickSettingsChange.Change(
|
QuickSettingsChange.Change(
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?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"
|
||||||
|
android:id="@+id/exceptions_wrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/exceptions_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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/. -->
|
||||||
|
<Button android:id="@+id/removeAllExceptions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:backgroundTint="?attr/neutral"
|
||||||
|
android:paddingStart="24dp"
|
||||||
|
android:paddingEnd="24dp"
|
||||||
|
android:text="@string/preferences_tracking_protection_exceptions_turn_on_for_all"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="?attr/accentHighContrast"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?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/. -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/favicon_image"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/domainView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/delete_exception"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/favicon_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="mozilla.org" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/delete_exception"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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/. -->
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/exceptions_description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:text="@string/preferences_tracking_protection_exceptions_description"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:textSize="16sp" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?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:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/exceptions_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context="org.mozilla.fenix.exceptions.ExceptionsFragment">
|
||||||
|
</LinearLayout>
|
|
@ -290,5 +290,13 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/trackingProtectionFragment"
|
android:id="@+id/trackingProtectionFragment"
|
||||||
android:name="org.mozilla.fenix.settings.TrackingProtectionFragment"
|
android:name="org.mozilla.fenix.settings.TrackingProtectionFragment"
|
||||||
android:label="TrackingProtectionFragment" />
|
android:label="TrackingProtectionFragment" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_trackingProtectionFragment_to_exceptionsFragment"
|
||||||
|
app:destination="@id/exceptionsFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/exceptionsFragment"
|
||||||
|
android:name="org.mozilla.fenix.exceptions.ExceptionsFragment"
|
||||||
|
android:label="ExceptionsFragment" />
|
||||||
</navigation>
|
</navigation>
|
|
@ -167,6 +167,10 @@
|
||||||
<string name="preferences_tracking_protection_description">Block content and scripts that track you online</string>
|
<string name="preferences_tracking_protection_description">Block content and scripts that track you online</string>
|
||||||
<!-- Preference for tracking protection exceptions -->
|
<!-- Preference for tracking protection exceptions -->
|
||||||
<string name="preferences_tracking_protection_exceptions">Exceptions</string>
|
<string name="preferences_tracking_protection_exceptions">Exceptions</string>
|
||||||
|
<!-- Preference description for tracking protection exceptions -->
|
||||||
|
<string name="preferences_tracking_protection_exceptions_description">Tracking Protection is off for these websites</string>
|
||||||
|
<!-- Button in Exceptions Preference to turn on tracking protection for all sites (remove all exceptions) -->
|
||||||
|
<string name="preferences_tracking_protection_exceptions_turn_on_for_all">Turn on for all sites</string>
|
||||||
|
|
||||||
<!-- Preference switch for Telemetry -->
|
<!-- Preference switch for Telemetry -->
|
||||||
<string name="preferences_telemetry">Telemetry</string>
|
<string name="preferences_telemetry">Telemetry</string>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:icon="@drawable/ic_tracking_protection"
|
android:icon="@drawable/ic_tracking_protection"
|
||||||
|
@ -11,7 +10,6 @@
|
||||||
android:summary="@string/preferences_tracking_protection_description"
|
android:summary="@string/preferences_tracking_protection_description"
|
||||||
android:title="@string/preferences_tracking_protection" />
|
android:title="@string/preferences_tracking_protection" />
|
||||||
<Preference
|
<Preference
|
||||||
app:isPreferenceVisible="false"
|
|
||||||
android:icon="@drawable/ic_internet"
|
android:icon="@drawable/ic_internet"
|
||||||
android:key="@string/pref_key_tracking_protection_exceptions"
|
android:key="@string/pref_key_tracking_protection_exceptions"
|
||||||
android:title="@string/preferences_tracking_protection_exceptions" />
|
android:title="@string/preferences_tracking_protection_exceptions" />
|
||||||
|
|
Loading…
Reference in New Issue