forked from recloudstream/cloudstream
Merge pull request #158 from Blatzar/master
Added DNS over HTTPS and cache
This commit is contained in:
commit
12977e1788
12 changed files with 242 additions and 14 deletions
|
@ -63,6 +63,9 @@ android {
|
|||
debuggable true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -106,6 +109,7 @@ dependencies {
|
|||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
|
||||
|
||||
implementation 'jp.wasabeef:glide-transformations:4.0.0'
|
||||
|
||||
|
@ -141,5 +145,6 @@ dependencies {
|
|||
implementation "androidx.work:work-runtime-ktx:2.7.0-rc01"
|
||||
|
||||
// Networking
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.1"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
||||
}
|
|
@ -26,6 +26,9 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||
import com.lagradost.cloudstream3.APIHolder.restrictedApis
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.network.get
|
||||
import com.lagradost.cloudstream3.network.initRequestClient
|
||||
import com.lagradost.cloudstream3.network.text
|
||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||
|
@ -46,6 +49,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
|||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
|
@ -290,6 +294,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
true
|
||||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||
updateLocale()
|
||||
initRequestClient()
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
if (isCastApiAvailable()) {
|
||||
|
@ -334,7 +339,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}*/
|
||||
|
||||
// Fucks up anime info layout since that has its own layout
|
||||
cast_mini_controller_holder?.isVisible = !listOf(R.id.navigation_results,R.id.navigation_player).contains(destination.id)
|
||||
cast_mini_controller_holder?.isVisible =
|
||||
!listOf(R.id.navigation_results, R.id.navigation_player).contains(destination.id)
|
||||
|
||||
nav_view.isVisible = listOf(
|
||||
R.id.navigation_home,
|
||||
|
|
|
@ -183,7 +183,7 @@ class TrailersToProvider : MainAPI() {
|
|||
|
||||
return isSucc
|
||||
} else if (url.contains("/episode/")) {
|
||||
val response = get(url).text
|
||||
val response = get(url, params = mapOf("preview" to "1")).text
|
||||
val document = Jsoup.parse(response)
|
||||
// val qSub = document.select("subtitle-content")
|
||||
val subUrl = document.select("subtitle-content")?.attr("data-url") ?: ""
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package com.lagradost.cloudstream3.network
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import java.net.InetAddress
|
||||
|
||||
/**
|
||||
* Based on https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt
|
||||
*/
|
||||
|
||||
fun OkHttpClient.Builder.addGenericDns(url: String, ips: List<String>) = dns(
|
||||
DnsOverHttps
|
||||
.Builder()
|
||||
.client(build())
|
||||
.url(url.toHttpUrl())
|
||||
.bootstrapDnsHosts(
|
||||
ips.map { InetAddress.getByName(it) }
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
fun OkHttpClient.Builder.addGoogleDns() = (
|
||||
addGenericDns(
|
||||
"https://dns.google/dns-query",
|
||||
listOf(
|
||||
"8.8.4.4",
|
||||
"8.8.8.8"
|
||||
)
|
||||
))
|
||||
|
||||
fun OkHttpClient.Builder.addCloudFlareDns() = (
|
||||
addGenericDns(
|
||||
"https://cloudflare-dns.com/dns-query",
|
||||
// https://www.cloudflare.com/ips/
|
||||
listOf(
|
||||
"1.1.1.1",
|
||||
"1.0.0.1",
|
||||
"2606:4700:4700::1111",
|
||||
"2606:4700:4700::1001"
|
||||
)
|
||||
))
|
||||
|
||||
// Commented out as it doesn't work
|
||||
//fun OkHttpClient.Builder.addOpenDns() = (
|
||||
// addGenericDns(
|
||||
// "https://doh.opendns.com/dns-query",
|
||||
// // https://support.opendns.com/hc/en-us/articles/360038086532-Using-DNS-over-HTTPS-DoH-with-OpenDNS
|
||||
// listOf(
|
||||
// "208.67.222.222",
|
||||
// "208.67.220.220",
|
||||
// "2620:119:35::35",
|
||||
// "2620:119:53::53",
|
||||
// )
|
||||
// ))
|
||||
|
||||
|
||||
fun OkHttpClient.Builder.addAdGuardDns() = (
|
||||
addGenericDns(
|
||||
"https://dns.adguard.com/dns-query",
|
||||
// https://github.com/AdguardTeam/AdGuardDNS
|
||||
listOf(
|
||||
// "Non-filtering"
|
||||
"94.140.14.140",
|
||||
"94.140.14.141",
|
||||
)
|
||||
))
|
|
@ -1,12 +1,20 @@
|
|||
package com.lagradost.cloudstream3.network
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import okhttp3.*
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
var baseClient = OkHttpClient()
|
||||
private const val DEFAULT_TIME = 10
|
||||
private val DEFAULT_TIME_UNIT = TimeUnit.MINUTES
|
||||
private const val DEFAULT_USER_AGENT = USER_AGENT
|
||||
|
@ -15,6 +23,29 @@ private val DEFAULT_DATA: Map<String, String> = mapOf()
|
|||
private val DEFAULT_COOKIES: Map<String, String> = mapOf()
|
||||
private val DEFAULT_REFERER: String? = null
|
||||
|
||||
fun Context.initRequestClient(): OkHttpClient {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val dns = settingsManager.getInt(this.getString(R.string.dns_pref), 0)
|
||||
baseClient = OkHttpClient.Builder()
|
||||
.cache(
|
||||
// Note that you need to add a ResponseInterceptor to make this 100% active.
|
||||
// The server response dictates if and when stuff should be cached.
|
||||
Cache(
|
||||
directory = File(cacheDir, "http_cache"),
|
||||
maxSize = 50L * 1024L * 1024L // 50 MiB
|
||||
)
|
||||
).apply {
|
||||
when (dns) {
|
||||
1 -> addGoogleDns()
|
||||
2 -> addCloudFlareDns()
|
||||
// 3 -> addOpenDns()
|
||||
4 -> addAdGuardDns()
|
||||
}
|
||||
}
|
||||
// Needs to be build as otherwise the other builders will change this object
|
||||
.build()
|
||||
return baseClient
|
||||
}
|
||||
|
||||
/** WARNING! CAN ONLY BE READ ONCE */
|
||||
val Response.text: String
|
||||
|
@ -93,14 +124,13 @@ fun get(
|
|||
timeout: Long = 0L,
|
||||
interceptor: Interceptor? = null
|
||||
): Response {
|
||||
|
||||
val client = OkHttpClient().newBuilder()
|
||||
val client = baseClient
|
||||
.newBuilder()
|
||||
.followRedirects(allowRedirects)
|
||||
.followSslRedirects(allowRedirects)
|
||||
.callTimeout(timeout, TimeUnit.SECONDS)
|
||||
|
||||
if (interceptor != null) client.addInterceptor(interceptor)
|
||||
|
||||
val request = getRequestCreator(url, headers, referer, params, cookies, cacheTime, cacheUnit)
|
||||
return client.build().newCall(request).execute()
|
||||
}
|
||||
|
@ -118,7 +148,8 @@ fun post(
|
|||
cacheUnit: TimeUnit = DEFAULT_TIME_UNIT,
|
||||
timeout: Long = 0L
|
||||
): Response {
|
||||
val client = OkHttpClient().newBuilder()
|
||||
val client = baseClient
|
||||
.newBuilder()
|
||||
.followRedirects(allowRedirects)
|
||||
.followSslRedirects(allowRedirects)
|
||||
.callTimeout(timeout, TimeUnit.SECONDS)
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.network
|
|||
import android.annotation.SuppressLint
|
||||
import android.net.http.SslError
|
||||
import android.webkit.*
|
||||
import androidx.core.view.contains
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -10,6 +11,7 @@ import kotlinx.coroutines.runBlocking
|
|||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.net.URI
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WebViewResolver(val interceptUrl: Regex) : Interceptor {
|
||||
|
@ -56,7 +58,7 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
|
|||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
val webViewUrl = request.url.toString()
|
||||
// println("Override url $webViewUrl")
|
||||
|
||||
if (interceptUrl.containsMatchIn(webViewUrl)) {
|
||||
fixedRequest = getRequestCreator(
|
||||
webViewUrl,
|
||||
|
@ -71,7 +73,27 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
|
|||
println("Web-view request finished: $webViewUrl")
|
||||
destroyWebView()
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
|
||||
return try {
|
||||
when {
|
||||
// suppress favicon requests as we don't display them anywhere
|
||||
webViewUrl.endsWith("/favicon.ico") -> WebResourceResponse("image/png", null, null)
|
||||
webViewUrl.contains("recaptcha") -> super.shouldInterceptRequest(view, request)
|
||||
|
||||
request.method == "GET" -> get(
|
||||
webViewUrl,
|
||||
headers = request.requestHeaders
|
||||
).toWebResourceResponse()
|
||||
|
||||
request.method == "POST" -> post(
|
||||
webViewUrl,
|
||||
headers = request.requestHeaders
|
||||
).toWebResourceResponse()
|
||||
else -> return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||
|
@ -99,4 +121,18 @@ class WebViewResolver(val interceptUrl: Regex) : Interceptor {
|
|||
return null
|
||||
}
|
||||
|
||||
fun Response.toWebResourceResponse(): WebResourceResponse {
|
||||
val contentTypeValue = this.header("Content-Type")
|
||||
// 1. contentType. 2. charset
|
||||
val typeRegex = Regex("""(.*);(?:.*charset=(.*)(?:|;)|)""")
|
||||
return if (contentTypeValue != null) {
|
||||
val found = typeRegex.find(contentTypeValue ?: "")
|
||||
val contentType = found?.groupValues?.getOrNull(1)?.ifBlank { null } ?: contentTypeValue
|
||||
val charset = found?.groupValues?.getOrNull(2)?.ifBlank { null }
|
||||
WebResourceResponse(contentType, charset, this.body?.byteStream())
|
||||
} else {
|
||||
WebResourceResponse("application/octet-stream", null, this.body?.byteStream())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.setLocale
|
|||
import com.lagradost.cloudstream3.MainActivity.Companion.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.network.initRequestClient
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
|
@ -53,6 +54,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
val localePreference = findPreference<Preference>(getString(R.string.locale_key))!!
|
||||
val benenePreference = findPreference<Preference>(getString(R.string.benene_count))!!
|
||||
val watchQualityPreference = findPreference<Preference>(getString(R.string.quality_pref_key))!!
|
||||
val dnsPreference = findPreference<Preference>(getString(R.string.dns_key))!!
|
||||
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
|
||||
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
|
||||
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
|
||||
|
@ -154,6 +156,25 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
dnsPreference.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.dns_pref)
|
||||
val prefValues = resources.getIntArray(R.array.dns_pref_values)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val currentDns =
|
||||
settingsManager.getInt(getString(R.string.dns_pref), 0)
|
||||
context?.showBottomDialog(
|
||||
prefNames.toList(),
|
||||
prefValues.indexOf(currentDns),
|
||||
getString(R.string.dns_pref),
|
||||
true,
|
||||
{}) {
|
||||
settingsManager.edit().putInt(getString(R.string.dns_pref), prefValues[it]).apply()
|
||||
context?.initRequestClient()
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
try {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.signature.ObjectKey
|
||||
import com.lagradost.cloudstream3.network.initRequestClient
|
||||
import java.io.InputStream
|
||||
|
||||
@GlideModule
|
||||
class GlideModule : AppGlideModule() {
|
||||
|
@ -18,4 +24,16 @@ class GlideModule : AppGlideModule() {
|
|||
.signature(ObjectKey(System.currentTimeMillis().toShort()))
|
||||
}
|
||||
}
|
||||
|
||||
// Needed for DOH
|
||||
// https://stackoverflow.com/a/61634041
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val client = context.initRequestClient()
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(client)
|
||||
)
|
||||
super.registerComponents(context, glide, registry)
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_baseline_dns_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_dns_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,13H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-6c0,-0.55 -0.45,-1 -1,-1zM7,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM20,3H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1V4c0,-0.55 -0.45,-1 -1,-1zM7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
||||
</vector>
|
|
@ -31,6 +31,21 @@
|
|||
<item>-2</item>
|
||||
</array>
|
||||
|
||||
<array name="dns_pref">
|
||||
<item>None</item>
|
||||
<item>Google</item>
|
||||
<item>Cloudflare</item>
|
||||
<!-- <item>OpenDns</item>-->
|
||||
<item>AdGuard</item>
|
||||
</array>
|
||||
<array name="dns_pref_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<!-- <item>3</item>-->
|
||||
<item>4</item>
|
||||
</array>
|
||||
|
||||
<array name="episode_long_click_options">
|
||||
<item>@string/episode_action_chomecast_episode</item>
|
||||
<item>@string/episode_action_chomecast_mirror</item>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<string name="display_sub_key" translatable="false">display_sub_key</string>
|
||||
<string name="show_fillers_key" translatable="false">show_fillers_key</string>
|
||||
<string name="provider_lang_key" translatable="false">provider_lang_key</string>
|
||||
<string name="dns_key" translatable="false">dns_key</string>
|
||||
|
||||
|
||||
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
|
||||
|
@ -166,7 +167,9 @@
|
|||
<string name="double_tap_to_seek_settings_des">Tap twice on the right or left side to seek forwards or backwards
|
||||
</string>
|
||||
<string name="use_system_brightness_settings">Use system brightness</string>
|
||||
<string name="use_system_brightness_settings_des">Use system brightness in the app player instead of an dark overlay</string>
|
||||
<string name="use_system_brightness_settings_des">Use system brightness in the app player instead of an dark
|
||||
overlay
|
||||
</string>
|
||||
|
||||
<string name="search">Search</string>
|
||||
<string name="settings_info">Info</string>
|
||||
|
@ -261,6 +264,10 @@
|
|||
<string name="dont_show_again">Don\'t show again</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="watch_quality_pref">Preferred watch quality</string>
|
||||
<string name="dns_pref">DNS over HTTPS</string>
|
||||
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
|
||||
|
||||
|
||||
<string name="display_subbed_dubbed_settings">Display Dubbed/Subbed Anime</string>
|
||||
|
||||
<string name="resize_fit">Fit to screen</string>
|
||||
|
@ -269,15 +276,21 @@
|
|||
|
||||
<string name="legal_notice" translatable="false">Disclaimer</string>
|
||||
<string name="legal_notice_key" translatable="false">legal_notice_key</string>
|
||||
<string name="legal_notice_text" translatable="false">Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them.
|
||||
<string name="legal_notice_text" translatable="false">Any legal issues regarding the content on this application
|
||||
should be taken up with the actual file hosts and providers themselves as we are not affiliated with them.
|
||||
|
||||
In case of copyright infringement, please directly contact the responsible parties or the streaming websites.
|
||||
|
||||
The app is purely for educational and personal use.
|
||||
|
||||
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface.
|
||||
CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down.
|
||||
CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or
|
||||
manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient,
|
||||
user-friendly interface.
|
||||
|
||||
It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk.
|
||||
It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the
|
||||
responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use
|
||||
CloudStream 3 at your own risk.
|
||||
</string>
|
||||
<string name="general">General</string>
|
||||
<string name="provider_lang_settings">Provider Languages</string>
|
||||
|
|
|
@ -93,6 +93,12 @@
|
|||
android:icon="@drawable/ic_baseline_skip_next_24"
|
||||
android:title="@string/show_fillers_settings"
|
||||
android:defaultValue="true"/>
|
||||
<Preference
|
||||
android:key="@string/dns_key"
|
||||
android:title="@string/dns_pref"
|
||||
android:summary="@string/dns_pref_summary"
|
||||
android:icon="@drawable/ic_baseline_dns_24">
|
||||
</Preference>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:key="search"
|
||||
|
|
Loading…
Reference in a new issue