Straw phase F: visible polish — RYD, HTML, intent filter, network sec
Four polish items, all visible: 1. Fixed RYD URL: was returnyoutubedislike.com (the website, 404s on /votes) → returnyoutubedislikeapi.com (the actual API). 2. Bundled Sectigo "Public Server Authentication CA DV R36" intermediate as an additional trust anchor (network_security_config.xml) for the .com web host — the API uses Google Trust Services already so this is defensive, in case we ever fetch the landing page. 3. Added intent-filter for YouTube URLs (VIEW + SEND) on StrawActivity. Single-task launchMode; pickYouTubeUrl() routes the initial Intent to Screen.VideoDetail(url) instead of Home. 4. stripHtml() utility removes <br>, </p>, and other tags from NewPipeExtractor's description. Chapter timestamps now render readably; raw HTML gone. Also added Log.d to SponsorBlockClient and RydClient for visible verification (StrawSb / StrawRyd tags). PlayerScreen now shows a "SB: N segments" overlay chip in the top-left so the user can see that SponsorBlock is armed. Verified on Android 14 emulator with "Me at the zoo" intent: - RYD: 👍 18.9M / 👎 424.2K renders - Intent: direct URL → VideoDetail (no Home pass-through) - HTML: chapter timestamps render clean
This commit is contained in:
parent
496ed30bda
commit
f3b78b4530
9 changed files with 215 additions and 29 deletions
|
|
@ -11,15 +11,33 @@
|
|||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".StrawActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- Open YouTube URLs with Straw. -->
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="youtu.be" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package com.sulkta.straw
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
|
|
@ -22,15 +23,23 @@ import com.sulkta.straw.feature.detail.VideoDetailScreen
|
|||
import com.sulkta.straw.feature.player.PlayerScreen
|
||||
import com.sulkta.straw.feature.search.SearchScreen
|
||||
|
||||
private val YT_HOSTS = setOf("youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be")
|
||||
private val YT_URL_RE = Regex("https?://(?:www\\.|m\\.)?(?:youtube\\.com/[^\\s]+|youtu\\.be/[A-Za-z0-9_-]+)")
|
||||
|
||||
class StrawActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val startUrl = pickYouTubeUrl(intent)
|
||||
|
||||
setContent {
|
||||
val scheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
||||
MaterialTheme(colorScheme = scheme) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
val nav = rememberNavigator()
|
||||
val initial: Screen =
|
||||
if (startUrl != null) Screen.VideoDetail(startUrl, "") else Screen.Home
|
||||
val nav = rememberNavigator(initial)
|
||||
|
||||
DisposableEffect(nav) {
|
||||
val cb = object : OnBackPressedCallback(true) {
|
||||
|
|
@ -70,4 +79,26 @@ class StrawActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Pull a YouTube URL out of an incoming Intent (VIEW or SEND). */
|
||||
private fun pickYouTubeUrl(intent: Intent?): String? {
|
||||
intent ?: return null
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW -> {
|
||||
val data = intent.data?.toString() ?: return null
|
||||
if (looksLikeYouTube(data)) return data
|
||||
}
|
||||
Intent.ACTION_SEND -> {
|
||||
val shared = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return null
|
||||
val match = YT_URL_RE.find(shared) ?: return null
|
||||
return match.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun looksLikeYouTube(url: String): Boolean {
|
||||
val host = runCatching { java.net.URI(url).host }.getOrNull() ?: return false
|
||||
return host.lowercase() in YT_HOSTS
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import com.sulkta.straw.util.stripHtml
|
||||
|
||||
@Composable
|
||||
fun VideoDetailScreen(
|
||||
|
|
@ -123,7 +124,7 @@ fun VideoDetailScreen(
|
|||
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = d.description.take(2000),
|
||||
text = stripHtml(d.description).take(2000),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@
|
|||
|
||||
package com.sulkta.straw.feature.player
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -23,6 +26,8 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
|
@ -109,6 +114,7 @@ fun PlayerScreen(
|
|||
val posSec = exoPlayer.currentPosition / 1000.0
|
||||
val segment = pickActiveSegment(segments, posSec)
|
||||
if (segment != null) {
|
||||
Log.i("StrawSb", "skip: ${segment.category} ${segment.startSec}s..${segment.endSec}s (pos=$posSec)")
|
||||
exoPlayer.seekTo((segment.endSec * 1000).toLong())
|
||||
Toast.makeText(
|
||||
context,
|
||||
|
|
@ -137,15 +143,34 @@ fun PlayerScreen(
|
|||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
|
||||
else -> AndroidView(
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).apply {
|
||||
player = exoPlayer
|
||||
useController = true
|
||||
else -> {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).apply {
|
||||
player = exoPlayer
|
||||
useController = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
// SponsorBlock segment count badge — small overlay top-left.
|
||||
resolved?.let { r ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(Color(0xCC222222))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "SB: ${r.segments.size} segment${if (r.segments.size == 1) "" else "s"}",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package com.sulkta.straw.net
|
||||
|
||||
import android.util.Log
|
||||
import com.sulkta.straw.extractor.NewPipeDownloader
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -23,18 +24,29 @@ data class RydVotes(
|
|||
)
|
||||
|
||||
object RydClient {
|
||||
private const val TAG = "StrawRyd"
|
||||
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
||||
|
||||
/** Blocking — call from Dispatchers.IO. */
|
||||
fun fetch(videoId: String): RydVotes? {
|
||||
val url = "https://returnyoutubedislikeapi.com/votes?videoId=$videoId"
|
||||
Log.d(TAG, "fetch start: $videoId → $url")
|
||||
val req = Request.Builder()
|
||||
.url("https://returnyoutubedislike.com/votes?videoId=$videoId")
|
||||
.url(url)
|
||||
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
return NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||
if (!r.isSuccessful) return@use null
|
||||
val body = r.body?.string() ?: return@use null
|
||||
runCatching { json.decodeFromString<RydVotes>(body) }.getOrNull()
|
||||
}
|
||||
return runCatching {
|
||||
NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||
val code = r.code
|
||||
val bodyStr = r.body?.string() ?: ""
|
||||
Log.d(TAG, "response: code=$code, body[0..120]=${bodyStr.take(120)}")
|
||||
if (!r.isSuccessful) return@use null
|
||||
runCatching { json.decodeFromString<RydVotes>(bodyStr) }
|
||||
.onFailure { Log.w(TAG, "json decode failed: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
}.onFailure { Log.w(TAG, "fetch failed: ${it.javaClass.simpleName}: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package com.sulkta.straw.net
|
||||
|
||||
import android.util.Log
|
||||
import com.sulkta.straw.extractor.NewPipeDownloader
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -32,13 +33,9 @@ data class SbSegment(
|
|||
}
|
||||
|
||||
object SponsorBlockClient {
|
||||
private const val TAG = "StrawSb"
|
||||
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
||||
|
||||
/**
|
||||
* Fetch SponsorBlock segments for [videoId] limited to [categories].
|
||||
* Returns segments matching the exact video ID (the API returns matches
|
||||
* for the prefix, so we filter client-side).
|
||||
*/
|
||||
fun fetch(
|
||||
videoId: String,
|
||||
categories: List<String> = listOf("sponsor"),
|
||||
|
|
@ -46,18 +43,28 @@ object SponsorBlockClient {
|
|||
val prefix = sha256Hex(videoId).substring(0, 4)
|
||||
val urlStr = "https://sponsor.ajay.app/api/skipSegments/$prefix?" +
|
||||
"categories=" + buildJsonArray(categories)
|
||||
Log.d(TAG, "fetch: videoId=$videoId prefix=$prefix url=$urlStr")
|
||||
val req = Request.Builder()
|
||||
.url(urlStr)
|
||||
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
return NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||
if (!r.isSuccessful) return@use emptyList()
|
||||
val body = r.body?.string() ?: return@use emptyList()
|
||||
val all = runCatching {
|
||||
json.decodeFromString<List<SbVideoSegments>>(body)
|
||||
}.getOrDefault(emptyList())
|
||||
all.firstOrNull { it.videoID == videoId }?.segments.orEmpty()
|
||||
}
|
||||
return runCatching {
|
||||
NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||
val code = r.code
|
||||
val bodyStr = r.body?.string() ?: ""
|
||||
Log.d(TAG, "response: code=$code body_len=${bodyStr.length}")
|
||||
if (!r.isSuccessful) return@use emptyList()
|
||||
val all = runCatching {
|
||||
json.decodeFromString<List<SbVideoSegments>>(bodyStr)
|
||||
}.onFailure { Log.w(TAG, "json decode failed: ${it.message}") }
|
||||
.getOrDefault(emptyList())
|
||||
val mine = all.firstOrNull { it.videoID == videoId }?.segments.orEmpty()
|
||||
Log.d(TAG, "armed ${mine.size} segments for $videoId (response had ${all.size} matching-prefix videos)")
|
||||
mine
|
||||
}
|
||||
}.onFailure { Log.w(TAG, "fetch failed: ${it.javaClass.simpleName}: ${it.message}") }
|
||||
.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
private fun buildJsonArray(items: List<String>): String =
|
||||
|
|
|
|||
30
strawApp/src/main/kotlin/com/sulkta/straw/util/HtmlText.kt
Normal file
30
strawApp/src/main/kotlin/com/sulkta/straw/util/HtmlText.kt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Sulkta-Coop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Strip HTML tags from NewPipeExtractor's description.content for plain-text
|
||||
* rendering. Day-3 polish replaces this with a real Markwon/Compose annotated
|
||||
* renderer; for now we just want readable text.
|
||||
*/
|
||||
|
||||
package com.sulkta.straw.util
|
||||
|
||||
private val tagRe = Regex("</?[^>]+>")
|
||||
private val entityRe = Regex("&[a-zA-Z#0-9]+;")
|
||||
private val brRe = Regex("<br\\s*/?>", RegexOption.IGNORE_CASE)
|
||||
private val pCloseRe = Regex("</p>", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val entityMap = mapOf(
|
||||
"&" to "&", "<" to "<", ">" to ">", """ to "\"",
|
||||
"'" to "'", " " to " ", "'" to "'", """ to "\"",
|
||||
)
|
||||
|
||||
fun stripHtml(input: String): String {
|
||||
if (input.isBlank()) return ""
|
||||
var s = input
|
||||
s = brRe.replace(s, "\n")
|
||||
s = pCloseRe.replace(s, "\n\n")
|
||||
s = tagRe.replace(s, "")
|
||||
s = entityRe.replace(s) { entityMap[it.value] ?: it.value }
|
||||
return s.trim().replace(Regex("\n{3,}"), "\n\n")
|
||||
}
|
||||
36
strawApp/src/main/res/raw/sectigo_dv_r36.crt
Normal file
36
strawApp/src/main/res/raw/sectigo_dv_r36.crt
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
|
||||
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
|
||||
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
|
||||
HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
|
||||
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
|
||||
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
|
||||
AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
|
||||
RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
|
||||
12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
|
||||
EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
|
||||
M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
|
||||
38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
|
||||
pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
|
||||
2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
|
||||
xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
|
||||
lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
|
||||
DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
|
||||
KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
|
||||
VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
|
||||
UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
|
||||
AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
|
||||
Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
|
||||
BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
|
||||
YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
|
||||
ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
|
||||
q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
|
||||
h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
|
||||
5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
|
||||
4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
|
||||
unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
|
||||
WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
|
||||
7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
|
||||
r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
|
||||
JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
|
||||
-----END CERTIFICATE-----
|
||||
26
strawApp/src/main/res/xml/network_security_config.xml
Normal file
26
strawApp/src/main/res/xml/network_security_config.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 Sulkta-Coop
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
Returnyoutubedislike.com serves a leaf cert without including the
|
||||
Sectigo "Public Server Authentication CA DV R36" intermediate.
|
||||
Android's system trust store has the USERTrust root but not the
|
||||
intermediate, so chain reconstruction fails. We bundle the intermediate
|
||||
as an additional trust anchor scoped to that domain.
|
||||
-->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config>
|
||||
<domain includeSubdomains="true">returnyoutubedislike.com</domain>
|
||||
<domain includeSubdomains="true">returnyoutubedislikeapi.com</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="@raw/sectigo_dv_r36" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
Loading…
Add table
Add a link
Reference in a new issue