[fenix] For https://github.com/mozilla-mobile/fenix/issues/1745 - Allow trackers from Allow List (https://github.com/mozilla-mobile/fenix/pull/2310)
parent
acd1aa19c3
commit
4e1fb4fff4
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
Loading…
Reference in New Issue