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
c30c171c91
commit
0252f6c63f
9 changed files with 215 additions and 29 deletions
|
|
@ -11,15 +11,33 @@
|
||||||
android:roundIcon="@android:drawable/sym_def_app_icon"
|
android:roundIcon="@android:drawable/sym_def_app_icon"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
||||||
<activity
|
<activity
|
||||||
android:name=".StrawActivity"
|
android:name=".StrawActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package com.sulkta.straw
|
package com.sulkta.straw
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.OnBackPressedCallback
|
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.player.PlayerScreen
|
||||||
import com.sulkta.straw.feature.search.SearchScreen
|
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() {
|
class StrawActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val startUrl = pickYouTubeUrl(intent)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val scheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
val scheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
||||||
MaterialTheme(colorScheme = scheme) {
|
MaterialTheme(colorScheme = scheme) {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
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) {
|
DisposableEffect(nav) {
|
||||||
val cb = object : OnBackPressedCallback(true) {
|
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.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.sulkta.straw.util.stripHtml
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VideoDetailScreen(
|
fun VideoDetailScreen(
|
||||||
|
|
@ -123,7 +124,7 @@ fun VideoDetailScreen(
|
||||||
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
Text("Description", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = d.description.take(2000),
|
text = stripHtml(d.description).take(2000),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@
|
||||||
|
|
||||||
package com.sulkta.straw.feature.player
|
package com.sulkta.straw.feature.player
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -23,6 +26,8 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
|
@ -109,6 +114,7 @@ fun PlayerScreen(
|
||||||
val posSec = exoPlayer.currentPosition / 1000.0
|
val posSec = exoPlayer.currentPosition / 1000.0
|
||||||
val segment = pickActiveSegment(segments, posSec)
|
val segment = pickActiveSegment(segments, posSec)
|
||||||
if (segment != null) {
|
if (segment != null) {
|
||||||
|
Log.i("StrawSb", "skip: ${segment.category} ${segment.startSec}s..${segment.endSec}s (pos=$posSec)")
|
||||||
exoPlayer.seekTo((segment.endSec * 1000).toLong())
|
exoPlayer.seekTo((segment.endSec * 1000).toLong())
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
|
|
@ -137,15 +143,34 @@ fun PlayerScreen(
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> AndroidView(
|
else -> {
|
||||||
factory = { ctx ->
|
AndroidView(
|
||||||
PlayerView(ctx).apply {
|
factory = { ctx ->
|
||||||
player = exoPlayer
|
PlayerView(ctx).apply {
|
||||||
useController = true
|
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
|
package com.sulkta.straw.net
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.sulkta.straw.extractor.NewPipeDownloader
|
import com.sulkta.straw.extractor.NewPipeDownloader
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
@ -23,18 +24,29 @@ data class RydVotes(
|
||||||
)
|
)
|
||||||
|
|
||||||
object RydClient {
|
object RydClient {
|
||||||
|
private const val TAG = "StrawRyd"
|
||||||
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
||||||
|
|
||||||
/** Blocking — call from Dispatchers.IO. */
|
/** Blocking — call from Dispatchers.IO. */
|
||||||
fun fetch(videoId: String): RydVotes? {
|
fun fetch(videoId: String): RydVotes? {
|
||||||
|
val url = "https://returnyoutubedislikeapi.com/votes?videoId=$videoId"
|
||||||
|
Log.d(TAG, "fetch start: $videoId → $url")
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url("https://returnyoutubedislike.com/votes?videoId=$videoId")
|
.url(url)
|
||||||
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
||||||
|
.header("Accept", "application/json")
|
||||||
.build()
|
.build()
|
||||||
return NewPipeDownloader.client().newCall(req).execute().use { r ->
|
return runCatching {
|
||||||
if (!r.isSuccessful) return@use null
|
NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||||
val body = r.body?.string() ?: return@use null
|
val code = r.code
|
||||||
runCatching { json.decodeFromString<RydVotes>(body) }.getOrNull()
|
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
|
package com.sulkta.straw.net
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.sulkta.straw.extractor.NewPipeDownloader
|
import com.sulkta.straw.extractor.NewPipeDownloader
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
@ -32,13 +33,9 @@ data class SbSegment(
|
||||||
}
|
}
|
||||||
|
|
||||||
object SponsorBlockClient {
|
object SponsorBlockClient {
|
||||||
|
private const val TAG = "StrawSb"
|
||||||
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
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(
|
fun fetch(
|
||||||
videoId: String,
|
videoId: String,
|
||||||
categories: List<String> = listOf("sponsor"),
|
categories: List<String> = listOf("sponsor"),
|
||||||
|
|
@ -46,18 +43,28 @@ object SponsorBlockClient {
|
||||||
val prefix = sha256Hex(videoId).substring(0, 4)
|
val prefix = sha256Hex(videoId).substring(0, 4)
|
||||||
val urlStr = "https://sponsor.ajay.app/api/skipSegments/$prefix?" +
|
val urlStr = "https://sponsor.ajay.app/api/skipSegments/$prefix?" +
|
||||||
"categories=" + buildJsonArray(categories)
|
"categories=" + buildJsonArray(categories)
|
||||||
|
Log.d(TAG, "fetch: videoId=$videoId prefix=$prefix url=$urlStr")
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url(urlStr)
|
.url(urlStr)
|
||||||
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
.header("User-Agent", NewPipeDownloader.USER_AGENT)
|
||||||
|
.header("Accept", "application/json")
|
||||||
.build()
|
.build()
|
||||||
return NewPipeDownloader.client().newCall(req).execute().use { r ->
|
return runCatching {
|
||||||
if (!r.isSuccessful) return@use emptyList()
|
NewPipeDownloader.client().newCall(req).execute().use { r ->
|
||||||
val body = r.body?.string() ?: return@use emptyList()
|
val code = r.code
|
||||||
val all = runCatching {
|
val bodyStr = r.body?.string() ?: ""
|
||||||
json.decodeFromString<List<SbVideoSegments>>(body)
|
Log.d(TAG, "response: code=$code body_len=${bodyStr.length}")
|
||||||
}.getOrDefault(emptyList())
|
if (!r.isSuccessful) return@use emptyList()
|
||||||
all.firstOrNull { it.videoID == videoId }?.segments.orEmpty()
|
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 =
|
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
|
||||||
|
* 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
|
||||||
|
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