Skip to content

Commit ea6901d

Browse files
errorcodeQQerrorcodeQQ
authored andcommitted
feat: Add gorgeous per-provider block settings UI and stack trace mapping
1 parent 029bc99 commit ea6901d

5 files changed

Lines changed: 171 additions & 215 deletions

File tree

CSGuard/src/main/kotlin/com/csguard/AllowlistStore.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object AllowlistStore {
2828
private const val TAG = "CSGuard"
2929
private const val PREFS_NAME = "csguard_allowlist"
3030
private const val KEY_ALWAYS = "always_allow_hosts"
31+
private const val KEY_BLOCKED_PROVIDERS = "blocked_providers"
3132
private const val KEY_BLOCKED = "blocked_attempts_log"
3233
private const val MAX_BLOCKED_LOG = 200
3334

@@ -71,6 +72,35 @@ object AllowlistStore {
7172
return true
7273
}
7374

75+
// ───────────────────── Blocked Providers (Per-Provider Blocklist) ─────────────────────
76+
77+
fun blockedProviders(): Set<String> {
78+
val raw = prefs?.getString(KEY_BLOCKED_PROVIDERS, "[]") ?: "[]"
79+
return try {
80+
val arr = JSONArray(raw)
81+
(0 until arr.length()).map { arr.getString(it).trim() }.toSet()
82+
} catch (_: Throwable) { emptySet() }
83+
}
84+
85+
fun addBlockedProvider(providerName: String): Boolean {
86+
val normalized = providerName.trim()
87+
if (normalized.isEmpty()) return false
88+
val current = blockedProviders().toMutableSet()
89+
if (!current.add(normalized)) return false
90+
prefs?.edit()?.putString(KEY_BLOCKED_PROVIDERS, JSONArray(current.toList()).toString())?.apply()
91+
Log.i(TAG, "AllowlistStore: added blocked provider → $normalized")
92+
return true
93+
}
94+
95+
fun removeBlockedProvider(providerName: String): Boolean {
96+
val normalized = providerName.trim()
97+
val current = blockedProviders().toMutableSet()
98+
if (!current.remove(normalized)) return false
99+
prefs?.edit()?.putString(KEY_BLOCKED_PROVIDERS, JSONArray(current.toList()).toString())?.apply()
100+
Log.i(TAG, "AllowlistStore: removed blocked provider → $normalized")
101+
return true
102+
}
103+
74104
// ───────────────────── Allow-once (session-only) ─────────────────────
75105

76106
fun allowOnce(host: String) {

CSGuard/src/main/kotlin/com/csguard/GuardSettingsDialog.kt

Lines changed: 106 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,16 @@ package com.csguard
22

33
import android.app.AlertDialog
44
import android.content.Context
5-
import android.text.InputType
6-
import android.view.ViewGroup
7-
import android.widget.CheckBox
8-
import android.widget.EditText
5+
import android.graphics.Color
6+
import android.graphics.Typeface
7+
import android.graphics.drawable.GradientDrawable
8+
import android.view.Gravity
99
import android.widget.LinearLayout
10-
import android.widget.RadioButton
11-
import android.widget.RadioGroup
1210
import android.widget.ScrollView
11+
import android.widget.Switch
1312
import android.widget.TextView
14-
import android.widget.Toast
15-
import java.text.DateFormat
16-
import java.util.Date
17-
18-
/**
19-
* Settings UI for CSGuard.
20-
*
21-
* Three sections:
22-
* 1. **Policy presets** — Strict / Strict-Verbose / Balanced / Permissive
23-
* 2. **Allowlist management** — view/add/remove always-allow hosts
24-
* 3. **Blocked log review** — see what CSGuard voided, with tap-to-approve
25-
*
26-
* The aggressive default is STRICT (silent void). The blocked-log section is
27-
* the user's main tool for tuning: when something legit stops working (e.g.
28-
* a CloudStream repo install URL got voided), they can find it in the log
29-
* and tap "Allow always" to whitelist the host going forward.
30-
*/
13+
import com.lagradost.cloudstream3.APIHolder
14+
3115
class GuardSettingsDialog(
3216
private val context: Context,
3317
private val current: GuardPolicy,
@@ -36,216 +20,139 @@ class GuardSettingsDialog(
3620
fun show() {
3721
val container = LinearLayout(context).apply {
3822
orientation = LinearLayout.VERTICAL
39-
setPadding(48, 32, 48, 32)
23+
setPadding(48, 48, 48, 48)
24+
setBackgroundColor(Color.parseColor("#121212")) // Sleek Dark Mode
4025
}
4126

42-
// ── Section 1: Policy preset ────────────────────────────────────
27+
// Title
4328
container.addView(TextView(context).apply {
44-
text = "Protection Level"
45-
textSize = 16f
29+
text = "🛡 CSGuard Protection"
30+
textSize = 24f
31+
typeface = Typeface.DEFAULT_BOLD
32+
setTextColor(Color.WHITE)
4633
setPadding(0, 0, 0, 16)
4734
})
4835

49-
val radioGroup = RadioGroup(context).apply { orientation = RadioGroup.VERTICAL }
50-
val strictRb = RadioButton(context).apply {
51-
id = 1
52-
text = "Strict (silent) — block ALL external launches except allowlist\n[DEFAULT — the void is silent]"
53-
isChecked = current == GuardPolicy.STRICT
54-
}
55-
val strictVerboseRb = RadioButton(context).apply {
56-
id = 2
57-
text = "Strict (verbose) — same as Strict, but show a toast on each block\n[for tuning your allowlist]"
58-
isChecked = current == GuardPolicy.STRICT_VERBOSE
59-
}
60-
val balancedRb = RadioButton(context).apply {
61-
id = 3
62-
text = "Balanced — block known ad hosts + ad-pattern paths\n(allow other external launches)"
63-
isChecked = current == GuardPolicy.DEFAULT && current != GuardPolicy.STRICT && current != GuardPolicy.STRICT_VERBOSE
64-
}
65-
val permissiveRb = RadioButton(context).apply {
66-
id = 4
67-
text = "Permissive — block only known ad-network hosts"
68-
isChecked = current == GuardPolicy.PERMISSIVE
69-
}
70-
radioGroup.addView(strictRb)
71-
radioGroup.addView(strictVerboseRb)
72-
radioGroup.addView(balancedRb)
73-
radioGroup.addView(permissiveRb)
74-
container.addView(radioGroup)
75-
76-
// ── Section 2: Allowlist management ─────────────────────────────
36+
// Subtitle
7737
container.addView(TextView(context).apply {
78-
text = "\nAllowlist (hosts that bypass the block)"
79-
textSize = 16f
80-
setPadding(0, 16, 0, 8)
38+
text = "Select which providers are forbidden from launching external browser intents. CSGuard will silently void their ads."
39+
textSize = 14f
40+
setTextColor(Color.parseColor("#B0B0B0"))
41+
setPadding(0, 0, 0, 32)
8142
})
8243

83-
val alwaysAllow = AllowlistStore.alwaysAllow().sorted()
84-
val allowlistInfo = TextView(context).apply {
85-
text = if (alwaysAllow.isEmpty()) {
86-
"• (empty — relying on built-in SAFE_HOSTS baseline)"
87-
} else {
88-
alwaysAllow.joinToString(prefix = "", separator = "\n")
89-
}
90-
textSize = 12f
91-
setPadding(16, 0, 16, 8)
92-
}
93-
container.addView(allowlistInfo)
94-
95-
val addHostBtn = android.widget.Button(context).apply {
96-
text = "+ Add host to allowlist"
97-
setOnClickListener { showAddHostDialog(context, allowlistInfo) }
98-
}
99-
container.addView(addHostBtn)
44+
var isGlobalStrict = current.blockAllUnknown
10045

101-
val removeHostBtn = android.widget.Button(context).apply {
102-
text = "− Remove host from allowlist"
103-
setOnClickListener { showRemoveHostDialog(context, allowlistInfo) }
46+
// Master Toggle: Global Strict Mode
47+
val masterRow = createRow("Global Strict Mode", "Block ALL unknown external intents across the entire app.", isGlobalStrict) { checked ->
48+
isGlobalStrict = checked
10449
}
105-
container.addView(removeHostBtn)
50+
container.addView(masterRow)
10651

107-
// ── Section 3: Blocked log review ───────────────────────────────
52+
// Section Title
10853
container.addView(TextView(context).apply {
109-
text = "\nRecent Blocks (tap to approve)"
110-
textSize = 16f
111-
setPadding(0, 16, 0, 8)
54+
text = "Per-Provider Blocks"
55+
textSize = 18f
56+
typeface = Typeface.DEFAULT_BOLD
57+
setTextColor(Color.parseColor("#E0E0E0"))
58+
setPadding(0, 32, 0, 16)
11259
})
11360

114-
val blockedLog = AllowlistStore.blockedLog()
115-
val logInfo = TextView(context).apply {
116-
text = if (blockedLog.isEmpty()) {
117-
"• (no blocks recorded yet — try playing a video)"
118-
} else {
119-
val df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
120-
buildString {
121-
blockedLog.take(10).forEach { entry ->
122-
val host = try { android.net.Uri.parse(entry.url).host ?: "?" } catch (_: Throwable) { "?" }
123-
append("• [${df.format(Date(entry.timestamp))}]\n")
124-
append(" host: $host\n")
125-
append(" caller: ${entry.caller}\n")
126-
append(" url: ${entry.url.take(80)}${if (entry.url.length > 80) "..." else ""}\n\n")
127-
}
128-
if (blockedLog.size > 10) {
129-
append("… and ${blockedLog.size - 10} more (showing 10 most recent)")
130-
}
61+
val blockedSet = AllowlistStore.blockedProviders().toMutableSet()
62+
63+
// Fetch all providers
64+
val providers = try {
65+
APIHolder.allProviders.sortedBy { it.name }
66+
} catch (_: Throwable) { emptyList() }
67+
68+
if (providers.isEmpty()) {
69+
container.addView(TextView(context).apply {
70+
text = "No providers loaded yet."
71+
setTextColor(Color.RED)
72+
})
73+
} else {
74+
providers.forEach { provider ->
75+
val row = createRow(provider.name, "Plugin: ${provider.javaClass.simpleName}", blockedSet.contains(provider.name)) { checked ->
76+
if (checked) blockedSet.add(provider.name) else blockedSet.remove(provider.name)
13177
}
78+
container.addView(row)
13279
}
133-
textSize = 11f
134-
setPadding(16, 0, 16, 8)
13580
}
136-
container.addView(logInfo)
13781

138-
val approveFromLogBtn = android.widget.Button(context).apply {
139-
text = "✓ Approve host from blocked log"
140-
setOnClickListener { showApproveFromLogDialog(context, allowlistInfo) }
82+
val scroll = ScrollView(context).apply {
83+
addView(container)
14184
}
142-
container.addView(approveFromLogBtn)
143-
144-
val clearLogBtn = android.widget.Button(context).apply {
145-
text = "🗑 Clear blocked log"
146-
setOnClickListener {
147-
AllowlistStore.clearBlockedLog()
148-
logInfo.text = "• (log cleared)"
149-
Toast.makeText(context, "Blocked log cleared", Toast.LENGTH_SHORT).show()
150-
}
151-
}
152-
container.addView(clearLogBtn)
153-
154-
// ── Wrap in scroll view (might be tall) ─────────────────────────
155-
val scroll = ScrollView(context).apply { addView(container) }
15685

157-
AlertDialog.Builder(context)
158-
.setTitle("CSGuard Settings")
86+
AlertDialog.Builder(context, android.R.style.Theme_DeviceDefault_Dialog_Alert)
15987
.setView(scroll)
160-
.setPositiveButton("Apply") { _, _ ->
161-
val newPolicy = when (radioGroup.checkedRadioButtonId) {
162-
1 -> GuardPolicy.STRICT
163-
2 -> GuardPolicy.STRICT_VERBOSE
164-
3 -> GuardPolicy.DEFAULT
165-
4 -> GuardPolicy.PERMISSIVE
166-
else -> GuardPolicy.STRICT
167-
}
88+
.setPositiveButton("Save Settings") { _, _ ->
89+
// Apply blocked providers
90+
val currentBlocks = AllowlistStore.blockedProviders()
91+
currentBlocks.forEach { AllowlistStore.removeBlockedProvider(it) }
92+
blockedSet.forEach { AllowlistStore.addBlockedProvider(it) }
93+
94+
// Create new policy
95+
val newPolicy = GuardPolicy(
96+
blockKnownAdHosts = true,
97+
blockAdPaths = true,
98+
blockAllUnknown = isGlobalStrict,
99+
showToast = current.showToast
100+
)
168101
onApply(newPolicy)
169-
Toast.makeText(context, "CSGuard: $newPolicy", Toast.LENGTH_SHORT).show()
170102
}
171103
.setNegativeButton("Cancel", null)
172104
.show()
173105
}
174106

175-
private fun showAddHostDialog(ctx: Context, allowlistInfo: TextView) {
176-
val input = EditText(ctx).apply {
177-
hint = "e.g. example.com (no scheme, no path)"
178-
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
107+
private fun createRow(title: String, subtitle: String, isChecked: Boolean, onToggle: (Boolean) -> Unit): LinearLayout {
108+
val row = LinearLayout(context).apply {
109+
orientation = LinearLayout.HORIZONTAL
110+
gravity = Gravity.CENTER_VERTICAL
111+
setPadding(32, 32, 32, 32)
112+
113+
val shape = GradientDrawable()
114+
shape.shape = GradientDrawable.RECTANGLE
115+
shape.cornerRadius = 24f
116+
shape.setColor(Color.parseColor("#1E1E1E"))
117+
background = shape
118+
119+
val params = LinearLayout.LayoutParams(
120+
LinearLayout.LayoutParams.MATCH_PARENT,
121+
LinearLayout.LayoutParams.WRAP_CONTENT
122+
)
123+
params.setMargins(0, 0, 0, 16)
124+
layoutParams = params
179125
}
180-
AlertDialog.Builder(ctx)
181-
.setTitle("Add host to allowlist")
182-
.setView(input)
183-
.setPositiveButton("Add") { _, _ ->
184-
val host = input.text.toString().trim()
185-
if (host.isNotEmpty()) {
186-
AllowlistStore.addAlwaysAllow(host)
187-
refreshAllowlistView(allowlistInfo)
188-
Toast.makeText(ctx, "Added: $host", Toast.LENGTH_SHORT).show()
189-
}
190-
}
191-
.setNegativeButton("Cancel", null)
192-
.show()
193-
}
194126

195-
private fun showRemoveHostDialog(ctx: Context, allowlistInfo: TextView) {
196-
val hosts = AllowlistStore.alwaysAllow().sorted()
197-
if (hosts.isEmpty()) {
198-
Toast.makeText(ctx, "Allowlist is empty", Toast.LENGTH_SHORT).show()
199-
return
127+
val textLayout = LinearLayout(context).apply {
128+
orientation = LinearLayout.VERTICAL
129+
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
200130
}
201-
val items = hosts.toTypedArray()
202-
val checked = BooleanArray(items.size) { false }
203-
AlertDialog.Builder(ctx)
204-
.setTitle("Remove hosts from allowlist")
205-
.setMultiChoiceItems(items, checked) { _, which, isChecked ->
206-
checked[which] = isChecked
207-
}
208-
.setPositiveButton("Remove selected") { _, _ ->
209-
items.indices.filter { checked[it] }.forEach { AllowlistStore.removeAlwaysAllow(items[it]) }
210-
refreshAllowlistView(allowlistInfo)
211-
}
212-
.setNegativeButton("Cancel", null)
213-
.show()
214-
}
215131

216-
private fun showApproveFromLogDialog(ctx: Context, allowlistInfo: TextView) {
217-
val log = AllowlistStore.blockedLog()
218-
if (log.isEmpty()) {
219-
Toast.makeText(ctx, "Blocked log is empty", Toast.LENGTH_SHORT).show()
220-
return
132+
val titleView = TextView(context).apply {
133+
text = title
134+
textSize = 16f
135+
setTextColor(Color.WHITE)
136+
typeface = Typeface.DEFAULT_BOLD
137+
}
138+
139+
val subtitleView = TextView(context).apply {
140+
text = subtitle
141+
textSize = 12f
142+
setTextColor(Color.parseColor("#888888"))
143+
setPadding(0, 4, 0, 0)
221144
}
222-
// Group by host, show distinct hosts
223-
val hosts = log.mapNotNull { entry ->
224-
try { android.net.Uri.parse(entry.url).host } catch (_: Throwable) { null }
225-
}.distinct()
226-
227-
val items = hosts.toTypedArray()
228-
val checked = BooleanArray(items.size) { false }
229-
AlertDialog.Builder(ctx)
230-
.setTitle("Approve hosts from blocked log")
231-
.setMultiChoiceItems(items, checked) { _, which, isChecked ->
232-
checked[which] = isChecked
233-
}
234-
.setPositiveButton("Add to allowlist") { _, _ ->
235-
items.indices.filter { checked[it] }.forEach { AllowlistStore.addAlwaysAllow(items[it]) }
236-
refreshAllowlistView(allowlistInfo)
237-
Toast.makeText(ctx, "Added ${checked.count { it }} hosts", Toast.LENGTH_SHORT).show()
238-
}
239-
.setNegativeButton("Cancel", null)
240-
.show()
241-
}
242145

243-
private fun refreshAllowlistView(view: TextView) {
244-
val alwaysAllow = AllowlistStore.alwaysAllow().sorted()
245-
view.text = if (alwaysAllow.isEmpty()) {
246-
"• (empty — relying on built-in SAFE_HOSTS baseline)"
247-
} else {
248-
alwaysAllow.joinToString(prefix = "", separator = "\n")
146+
textLayout.addView(titleView)
147+
textLayout.addView(subtitleView)
148+
149+
val toggle = Switch(context).apply {
150+
this.isChecked = isChecked
151+
setOnCheckedChangeListener { _, checked -> onToggle(checked) }
249152
}
153+
154+
row.addView(textLayout)
155+
row.addView(toggle)
156+
return row
250157
}
251158
}

0 commit comments

Comments
 (0)