AdBlock for WebView
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"
|
||||
tools:ignore="ForegroundServicesPolicy" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
@@ -19,17 +21,19 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||
tools:ignore="RequestInstallPackagesPolicy" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
tools:ignore="AllFilesAccessPolicy,ScopedStorage" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -338,6 +342,9 @@
|
||||
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||
android:exported="false"
|
||||
android:label="@string/prefetch_content" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.browser.AdListUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
|
||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AdListUpdateService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var updater: AdBlock.Updater
|
||||
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
updater.updateList()
|
||||
}
|
||||
|
||||
override fun IntentJobContext.onError(error: Throwable) = Unit
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
@@ -29,6 +30,9 @@ abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), Bro
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var adBlock: AdBlock
|
||||
|
||||
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -24,7 +24,7 @@ class BrowserActivity : BaseBrowserActivity() {
|
||||
|
||||
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this, adBlock)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
proxyProvider.applyWebViewConfig()
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
open class BrowserClient(
|
||||
private val callback: BrowserCallback
|
||||
private val callback: BrowserCallback,
|
||||
private val adBlock: AdBlock,
|
||||
) : WebViewClient() {
|
||||
|
||||
/**
|
||||
@@ -31,4 +37,29 @@ open class BrowserClient(
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
callback.onHistoryChanged()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
url: String?
|
||||
): WebResourceResponse? = if (url.isNullOrEmpty() || adBlock.shouldLoadUrl(url, view?.url)) {
|
||||
super.shouldInterceptRequest(view, url)
|
||||
} else {
|
||||
emptyResponse()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): WebResourceResponse? = if (request == null || adBlock.shouldLoadUrl(request.url.toString(), view?.url)) {
|
||||
view?.originalUrl
|
||||
super.shouldInterceptRequest(view, request)
|
||||
} else {
|
||||
emptyResponse()
|
||||
}
|
||||
|
||||
private fun emptyResponse(): WebResourceResponse =
|
||||
WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream(byteArrayOf()))
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
|
||||
finishAfterTransition()
|
||||
return
|
||||
}
|
||||
cfClient = CloudFlareClient(cookieJar, this, url)
|
||||
cfClient = CloudFlareClient(cookieJar, this, adBlock, url)
|
||||
viewBinding.webView.webViewClient = cfClient
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import org.koitharu.kotatsu.browser.BrowserClient
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.webview.adblock.AdBlock
|
||||
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
|
||||
|
||||
private const val LOOP_COUNTER = 3
|
||||
@@ -11,8 +12,9 @@ private const val LOOP_COUNTER = 3
|
||||
class CloudFlareClient(
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
adBlock: AdBlock,
|
||||
private val targetUrl: String,
|
||||
) : BrowserClient(callback) {
|
||||
) : BrowserClient(callback, adBlock) {
|
||||
|
||||
private val oldClearance = getClearance()
|
||||
private var counter = 0
|
||||
|
||||
@@ -16,8 +16,12 @@ object CommonHeaders {
|
||||
const val CACHE_CONTROL = "Cache-Control"
|
||||
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
|
||||
const val RETRY_AFTER = "Retry-After"
|
||||
const val LAST_MODIFIED = "Last-Modified"
|
||||
const val IF_MODIFIED_SINCE = "If-Modified-Since"
|
||||
const val MANGA_SOURCE = "X-Manga-Source"
|
||||
|
||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||
get() = CacheControl.Builder().noStore().build()
|
||||
|
||||
const val DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.koitharu.kotatsu.core.network.webview.adblock
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.sink
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.requireBody
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class AdBlock @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private var rules: RulesList? = null
|
||||
|
||||
@WorkerThread
|
||||
fun shouldLoadUrl(url: String, baseUrl: String?): Boolean {
|
||||
return shouldLoadUrl(
|
||||
url.lowercase().toHttpUrlOrNull() ?: return true,
|
||||
baseUrl?.lowercase()?.toHttpUrlOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun shouldLoadUrl(url: HttpUrl, baseUrl: HttpUrl?): Boolean {
|
||||
if (!settings.isAdBlockEnabled) {
|
||||
return true
|
||||
}
|
||||
return synchronized(this) {
|
||||
rules ?: parseRules().also { rules = it }
|
||||
}?.let {
|
||||
val rule = it[url, baseUrl]
|
||||
if (rule != null) {
|
||||
Log.i(TAG, "Blocked $url by $rule")
|
||||
}
|
||||
rule == null
|
||||
} ?: true
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun parseRules() = runCatchingCancellable {
|
||||
listFile(context).useLines { lines ->
|
||||
val rules = RulesList()
|
||||
lines.forEach { line -> rules.add(line) }
|
||||
rules.trimToSize()
|
||||
rules
|
||||
}
|
||||
}.onFailure { e ->
|
||||
e.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
|
||||
class Updater @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@BaseHttpClient private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
|
||||
suspend fun updateList() {
|
||||
val file = listFile(context)
|
||||
val dateFormat = SimpleDateFormat(CommonHeaders.DATE_FORMAT, Locale.ENGLISH)
|
||||
val requestBuilder = Request.Builder()
|
||||
.url(EASYLIST_URL)
|
||||
.get()
|
||||
if (file.exists() && file.isNotEmpty()) {
|
||||
val lastModified = file.lastModified()
|
||||
requestBuilder.header(CommonHeaders.IF_MODIFIED_SINCE, dateFormat.format(Date(lastModified)))
|
||||
}
|
||||
okHttpClient.newCall(
|
||||
requestBuilder.build(),
|
||||
).await().use { response ->
|
||||
if (response.code == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
return
|
||||
}
|
||||
val lastModified = response.header(CommonHeaders.LAST_MODIFIED)?.let {
|
||||
runCatching {
|
||||
dateFormat.parse(it)
|
||||
}.getOrNull()
|
||||
}?.time ?: System.currentTimeMillis()
|
||||
response.requireBody().source().use { source ->
|
||||
file.sink().use { sink ->
|
||||
source.readAll(sink)
|
||||
}
|
||||
file.setLastModified(lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
fun listFile(context: Context): File {
|
||||
val root = File(context.externalCacheDir ?: context.cacheDir, LIST_DIR)
|
||||
root.mkdir()
|
||||
return File(root, LIST_FILENAME)
|
||||
}
|
||||
|
||||
private const val LIST_FILENAME = "easylist.txt"
|
||||
private const val LIST_DIR = "adblock"
|
||||
private const val EASYLIST_URL = "https://easylist.to/easylist/easylist.txt"
|
||||
private const val TAG = "AdBlock"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.koitharu.kotatsu.core.network.webview.adblock
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
|
||||
class CSSRuleBuilder {
|
||||
|
||||
private val selectors = ArraySet<String>()
|
||||
|
||||
fun add(selector: String) {
|
||||
selectors.add(selector)
|
||||
}
|
||||
|
||||
fun build() = buildString {
|
||||
append("<style> {")
|
||||
for (selector in selectors) {
|
||||
append(selector)
|
||||
append(";")
|
||||
}
|
||||
append("}!important</style>")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.core.network.webview.adblock
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
sealed interface Rule {
|
||||
|
||||
operator fun invoke(url: HttpUrl, baseUrl: HttpUrl?): Boolean
|
||||
|
||||
data class Domain(private val domain: String) : Rule {
|
||||
|
||||
override fun invoke(url: HttpUrl, baseUrl: HttpUrl?): Boolean = (url.topPrivateDomain() ?: url.host) == domain
|
||||
}
|
||||
|
||||
data class ExactUrl(private val url: HttpUrl) : Rule {
|
||||
|
||||
override operator fun invoke(url: HttpUrl, baseUrl: HttpUrl?): Boolean = url == this.url
|
||||
}
|
||||
|
||||
data class Path(private val path: String, private val contains: Boolean) : Rule {
|
||||
|
||||
override fun invoke(url: HttpUrl, baseUrl: HttpUrl?): Boolean {
|
||||
val fullPath = url.host + "/" + url.encodedPath
|
||||
return if (contains) {
|
||||
fullPath.contains(path)
|
||||
} else {
|
||||
fullPath.endsWith(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WithModifiers(
|
||||
private val baseRule: Rule,
|
||||
private val script: Boolean?,
|
||||
private val thirdParty: Boolean?,
|
||||
private val domains: Set<String>?,
|
||||
private val domainsNot: Set<String>?,
|
||||
) : Rule {
|
||||
|
||||
override fun invoke(url: HttpUrl, baseUrl: HttpUrl?): Boolean {
|
||||
if (!baseRule.invoke(url, baseUrl)) {
|
||||
return false
|
||||
}
|
||||
if (baseUrl == null) {
|
||||
return true
|
||||
}
|
||||
thirdParty?.let {
|
||||
val isThirdPartyRequest =
|
||||
(url.topPrivateDomain() ?: url.host) != (baseUrl.topPrivateDomain() ?: baseUrl.host)
|
||||
if (isThirdPartyRequest != it) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// TODO check other modifiers
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.koitharu.kotatsu.core.network.webview.adblock
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
/**
|
||||
* Very simple implementation of adblock list parser
|
||||
* Not all features are supported
|
||||
*/
|
||||
class RulesList {
|
||||
|
||||
private val blockRules = ArrayList<Rule>()
|
||||
private val allowRules = ArrayList<Rule>()
|
||||
|
||||
operator fun get(url: HttpUrl, baseUrl: HttpUrl?): Rule? {
|
||||
val rule = blockRules.find { x -> x(url, baseUrl) }
|
||||
return rule?.takeIf { allowRules.none { x -> x(url, baseUrl) } }
|
||||
}
|
||||
|
||||
fun add(line: String) {
|
||||
val parts = line.lowercase().trim().split('$')
|
||||
parts.first().addImpl(isWhitelist = false, modifiers = parts.getOrNull(1))
|
||||
}
|
||||
|
||||
fun trimToSize() {
|
||||
blockRules.trimToSize()
|
||||
allowRules.trimToSize()
|
||||
}
|
||||
|
||||
private fun String.addImpl(isWhitelist: Boolean, modifiers: String?) {
|
||||
val list = if (isWhitelist) allowRules else blockRules
|
||||
|
||||
when {
|
||||
startsWith('!') || startsWith('[') -> {
|
||||
// Comment, do nothing
|
||||
}
|
||||
|
||||
startsWith("||") -> {
|
||||
// domain
|
||||
list += Rule.Domain(substring(2).substringBefore('^').trim()).withModifiers(modifiers)
|
||||
}
|
||||
|
||||
startsWith('|') -> {
|
||||
val url = substring(1).substringBefore('^').trim().toHttpUrlOrNull()
|
||||
if (url != null) {
|
||||
list += Rule.ExactUrl(url).withModifiers(modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
startsWith("@@") -> {
|
||||
substring(2).substringBefore('^').trim().addImpl(!isWhitelist, modifiers)
|
||||
}
|
||||
|
||||
startsWith("##") -> {
|
||||
// TODO css rules
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (endsWith('*')) {
|
||||
list += Rule.Path(this.dropLast(1), contains = true).withModifiers(modifiers)
|
||||
} else if (!contains('*')) { // wildcards is not supported yet
|
||||
list += Rule.Path(this, contains = false).withModifiers(modifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
private fun Rule.withModifiers(options: String?): Rule {
|
||||
if (options.isNullOrEmpty()) {
|
||||
return this
|
||||
}
|
||||
var script: Boolean? = null
|
||||
var thirdParty: Boolean? = null
|
||||
options.split(',').forEach {
|
||||
val isNot = it.startsWith('~')
|
||||
when (it.removePrefix("~")) {
|
||||
"script" -> script = !isNot
|
||||
"third-party" -> thirdParty = !isNot
|
||||
}
|
||||
}
|
||||
return Rule.WithModifiers(
|
||||
baseRule = this,
|
||||
script = script,
|
||||
thirdParty = thirdParty,
|
||||
domains = null, //TODO
|
||||
domainsNot = null, //TODO
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -320,6 +320,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val screenshotsPolicy: ScreenshotsPolicy
|
||||
get() = prefs.getEnumValue(KEY_SCREENSHOTS_POLICY, ScreenshotsPolicy.ALLOW)
|
||||
|
||||
val isAdBlockEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_ADBLOCK, false)
|
||||
|
||||
var userSpecifiedMangaDirectories: Set<File>
|
||||
get() {
|
||||
val set = prefs.getStringSet(KEY_LOCAL_MANGA_DIRS, emptySet()).orEmpty()
|
||||
@@ -598,6 +601,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val TRACK_HISTORY = "history"
|
||||
const val TRACK_FAVOURITES = "favourites"
|
||||
|
||||
const val KEY_ADBLOCK = "adblock"
|
||||
const val KEY_LIST_MODE = "list_mode_2"
|
||||
const val KEY_LIST_MODE_HISTORY = "list_mode_history"
|
||||
const val KEY_LIST_MODE_FAVORITES = "list_mode_favorites"
|
||||
|
||||
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.AdListUpdateService
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.os.VoiceInputContract
|
||||
@@ -290,6 +291,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
}
|
||||
startService(Intent(this@MainActivity, LocalIndexUpdateService::class.java))
|
||||
startService(Intent(this@MainActivity, PeriodicalBackupService::class.java))
|
||||
if (settings.isAdBlockEnabled) {
|
||||
startService(Intent(this@MainActivity, AdListUpdateService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class SourceAuthActivity : BaseBrowserActivity(), BrowserCallback {
|
||||
return
|
||||
}
|
||||
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this, adBlock)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
proxyProvider.applyWebViewConfig()
|
||||
|
||||
@@ -842,4 +842,6 @@
|
||||
<string name="changelog_summary">Changes history for recently released versions</string>
|
||||
<string name="collapse">Collapse</string>
|
||||
<string name="expand">Expand</string>
|
||||
<string name="adblock">Block ads in browser</string>
|
||||
<string name="adblock_summary">Block advertisement in the built-in browser (beta)</string>
|
||||
</resources>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/network"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:title="@string/network">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
@@ -34,6 +34,12 @@
|
||||
android:title="@string/dns_over_https"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="adblock"
|
||||
android:summary="@string/adblock_summary"
|
||||
android:title="@string/adblock" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/image_proxies"
|
||||
|
||||
Reference in New Issue
Block a user