Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c973245
ui: #2931, add block indicator in network log
hussainmohd-a Aug 6, 2026
c0da877
test: make private val to internal for tests
hussainmohd-a Aug 6, 2026
f654f01
apps: make sure base id remains for apps in proxyappsmapping
hussainmohd-a Aug 6, 2026
cc446c0
rpn: make fns open for tests, better handle of expiry
hussainmohd-a Aug 6, 2026
ff275a0
tests: more tests for tunnel dns, firewall flow
hussainmohd-a Aug 6, 2026
ce3d355
apps: firewall mgr, handling pxm apps, cloned apps
hussainmohd-a Aug 6, 2026
392a304
proxy: new global proxy handler to reconsile the proxy between ui and…
hussainmohd-a Aug 6, 2026
ee4744d
ui: fix #2624 home screen, collapsed list in configure screen
hussainmohd-a Aug 7, 2026
a319630
ui: fix #2624, home screen alignment fix
hussainmohd-a Aug 8, 2026
9f59a70
ui: show appropriate toast on error, fix #2940
hussainmohd-a Aug 8, 2026
7506009
ui: show appropriate errs when enabling proxy lockdown
hussainmohd-a Aug 8, 2026
42df4c5
minor: rmv unused const value
hussainmohd-a Aug 8, 2026
05fc6bd
log: change label for settings in stats tab
hussainmohd-a Aug 8, 2026
3ba1138
vpn: fix multiple issues and split VPN managers
hussainmohd-a Aug 8, 2026
9b4e347
stat: store mem file output file to externaldir
hussainmohd-a Aug 8, 2026
1295825
#2960 fix wildcard prefixes resulting in empty rules
hussainmohd-a Aug 8, 2026
e2ac7ba
tun: fix multiple issues and improvements
hussainmohd-a Aug 8, 2026
74844a5
rpn: show err codes/reason on rpn load failure, minor ui changes
hussainmohd-a Aug 8, 2026
b2dab8b
tests: add more tests for tunnel dns flow
hussainmohd-a Aug 8, 2026
4614ea2
minor: make play and website variant same for sponsor billing
hussainmohd-a Aug 8, 2026
9a05e95
string literal for v055z+
hussainmohd-a Aug 8, 2026
b04c67d
bump firestack version
hussainmohd-a Aug 8, 2026
8e0cd68
ui: add alpha to the blocked indicator, related #2931
hussainmohd-a Aug 8, 2026
a448aa1
wg: replace the invalid comparison with the intended block detection
hussainmohd-a Aug 8, 2026
1aa519a
ui: remove experimental tags for few settings
hussainmohd-a Aug 8, 2026
9d206eb
ui: disable the lan switch during system lockdown
hussainmohd-a Aug 8, 2026
53c9c66
rmv usage of synchronized from code
hussainmohd-a Aug 8, 2026
4c63062
ui: update the completion block to pass appCtx in about screen
hussainmohd-a Aug 8, 2026
175244c
v055z+: changes from improvements, suggestions
hussainmohd-a Aug 9, 2026
74f786d
rpn: auto server will always be available
hussainmohd-a Aug 9, 2026
1ae9118
minor improvements, rmv incorrect purge
hussainmohd-a Aug 9, 2026
ffd8e46
bump target & compile version to 37
hussainmohd-a Aug 9, 2026
71de4d5
ui: rpn screen font, string changes
hussainmohd-a Aug 9, 2026
85c8d87
ui: home screen stat updates
hussainmohd-a Aug 9, 2026
1d07adb
actions-ocr: comment out review on pr
hussainmohd-a Aug 9, 2026
b91ba82
ui: minor ui changes, rpn screen font, string changes
hussainmohd-a Aug 9, 2026
cbf01b8
vpn: update conn state to working on vpn started
hussainmohd-a Aug 9, 2026
b4e672e
string: rpn string split
hussainmohd-a Aug 9, 2026
ecb3618
vpn: update conn state to working on vpn started
hussainmohd-a Aug 9, 2026
620235a
ui: rpn screen active text changes
hussainmohd-a Aug 9, 2026
d38a5c3
ui: rpn new hero banner desc
hussainmohd-a Aug 9, 2026
a36b0c6
ui: rpn adjust show tag for onetime purchase
hussainmohd-a Aug 9, 2026
b6ddfaa
ui: sponsor ui corrections, home srn click listener fix
hussainmohd-a Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,20 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Collections
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap

class ConnectionLogAdapter(private val context: Context) :
PagingDataAdapter<MergedConnectionLog, ConnectionLogAdapter.ConnectionLogViewHolder>(
DIFF_CALLBACK
) {

// Per-uid cache of package names, invalidated on each new page submission.
private val packageNameCache = Collections.synchronizedMap(HashMap<Int, List<String>>())
// Per-uid cache of package names (immutable snapshots), living for the adapter's
// lifetime (recreated with the fragment; uids are stable across paging pages).
// ConcurrentHashMap: lock-free reads, non-blocking writes. Compute-on-miss cannot
// be atomic (computeIfAbsent's lambda is not suspendable), so a rare duplicate
// compute for the same uid is benign: the values are idempotent and immutable.
private val packageNameCache = ConcurrentHashMap<Int, List<String>>()

companion object {
private val DIFF_CALLBACK =
Expand Down Expand Up @@ -165,7 +169,6 @@ class ConnectionLogAdapter(private val context: Context) :
displayTransactionDetails(log)
displayProtocolDetails(log.port, log.protocol)
displayAppDetails(log)
displaySummaryDetails(log)
val blocked = if (log.blockedByRule == FirewallRuleset.RULE12.id) {
log.proxyDetails.isEmpty()
} else {
Expand All @@ -176,6 +179,7 @@ class ConnectionLogAdapter(private val context: Context) :
} else {
log.blockedByRule
}
displaySummaryDetails(blocked, log)
displayFirewallRulesetHint(blocked, rule)

b.connectionParentLayout.setOnClickListener { openBottomSheet(log) }
Expand Down Expand Up @@ -232,7 +236,12 @@ class ConnectionLogAdapter(private val context: Context) :
private fun displayAppDetails(log: MergedConnectionLog) {
launchBinding {
val apps = packageNameCache.getOrPut(log.uid) {
FirewallManager.getPackageNamesByUid(log.uid)
// Guard against iterator faults from Guava's HashMultimap in
// FirewallManager (see snapshotAppInfos fallbacks). On failure the
// entry is not cached, so the next bind retries; empty list is
// rendered as the default icon by the caller.
runCatching { FirewallManager.getPackageNamesByUid(log.uid) }
.getOrDefault(emptyList())
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment on lines +243 to +244

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · low]
The comment states "On failure the entry is not cached, so the next bind retries", but runCatching { ... }.getOrDefault(emptyList()) swallows the exception and returns normally, so getOrPut DOES cache emptyList(). A transient iterator fault therefore permanently caches the empty list and future binds never retry — the uid keeps the default icon for the adapter's lifetime, which contradicts the documented intent. To match the retry behavior, wrap the whole getOrPut in runCatching so a failed compute never gets stored.

Suggestion:

Suggested change
runCatching { FirewallManager.getPackageNamesByUid(log.uid) }
.getOrDefault(emptyList())
val apps = runCatching {
packageNameCache.getOrPut(log.uid) {
FirewallManager.getPackageNamesByUid(log.uid)
}
}.getOrDefault(emptyList())

}
Comment thread
hussainmohd-a marked this conversation as resolved.
val count = apps.count()
val pkgName = log.packageName ?: ""
Expand Down Expand Up @@ -312,7 +321,7 @@ class ConnectionLogAdapter(private val context: Context) :
}
}

private fun displaySummaryDetails(log: MergedConnectionLog) {
private fun displaySummaryDetails(blocked: Boolean, log: MergedConnectionLog) {
launchBinding {
val hasCid = VpnController.hasCid(log.connId, log.uid)
val connType = ConnectionTracker.ConnType.get(log.connType)
Expand All @@ -335,8 +344,15 @@ class ConnectionLogAdapter(private val context: Context) :
b.connectionDelay.text = ""
hasMinSummary = true
} else {
if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
hasMinSummary = true
Comment thread
hussainmohd-a marked this conversation as resolved.
} else {
b.connectionDuration.text = ""
b.connectionDuration.alpha = 1f
}
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment on lines +347 to +354

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
The blocked branch sets hasMinSummary = true so the summary won't be hidden, but it never sets connectionSummaryLl.visibility = View.VISIBLE. Because update() doesn't clear stale view state when a holder is recycled (only clear() does, and it's only called for null items), a holder previously showing a non-blocked minimal summary (visibility = GONE) will keep the layout hidden — so the red-circle blocked indicator won't be displayed. The hasCid branch above explicitly sets VISIBLE; mirror that here.

Suggestion:

Suggested change
if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
hasMinSummary = true
} else {
b.connectionDuration.text = ""
b.connectionDuration.alpha = 1f
}
if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
b.connectionSummaryLl.visibility = View.VISIBLE
hasMinSummary = true
} else {
b.connectionDuration.text = ""
b.connectionDuration.alpha = 1f
}

b.connectionDataUsage.text = ""
b.connectionDuration.text = ""
}
if (connType.isMetered()) {
b.connectionDelay.text = context.getString(R.string.symbol_currency)
Expand Down Expand Up @@ -370,8 +386,15 @@ class ConnectionLogAdapter(private val context: Context) :
}

b.connectionSummaryLl.visibility = View.VISIBLE
val duration = getDurationInHumanReadableFormat(context, log.duration)
b.connectionDuration.text = context.getString(R.string.single_argument, duration)
if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
} else {
b.connectionDuration.alpha = 1f
val duration = getDurationInHumanReadableFormat(context, log.duration)
b.connectionDuration.text =
context.getString(R.string.single_argument, duration)
}
val download =
context.getString(
R.string.symbol_download,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ class ConnectionTrackerAdapter(private val context: Context) :
displayTransactionDetails(connTracker)
displayProtocolDetails(connTracker.port, connTracker.protocol)
displayAppDetails(connTracker)
displaySummaryDetails(connTracker)
// case: when the rule is set to RULE12 but no proxy is set, consider this as error
// handle this as special case, and display the RULE1C hint
// RULE1C is the hint for RULE12 with no proxy set.
Expand All @@ -140,6 +139,7 @@ class ConnectionTrackerAdapter(private val context: Context) :
connTracker.blockedByRule
}
displayFirewallRulesetHint(blocked, rule)
displaySummaryDetails(blocked, connTracker)

b.connectionParentLayout.setOnClickListener { openBottomSheet(connTracker) }
}
Expand Down Expand Up @@ -268,7 +268,7 @@ class ConnectionTrackerAdapter(private val context: Context) :
}
}

private fun displaySummaryDetails(ct: ConnectionTracker) {
private fun displaySummaryDetails(blocked: Boolean, ct: ConnectionTracker) {
io {
val hasCid = VpnController.hasCid(ct.connId, ct.uid)
val connType = ConnectionTracker.ConnType.get(ct.connType)
Expand All @@ -289,8 +289,14 @@ class ConnectionTrackerAdapter(private val context: Context) :
b.connectionDelay.text = ""
hasMinSummary = true
} else {
if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
} else {
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
b.connectionDuration.text = ""
b.connectionDuration.alpha = 1f
}
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
b.connectionDataUsage.text = ""
b.connectionDuration.text =""
}
if (connType.isMetered()) {
b.connectionDelay.text = context.getString(R.string.symbol_currency)
Expand Down Expand Up @@ -323,9 +329,16 @@ class ConnectionTrackerAdapter(private val context: Context) :
return@uiCtx
}

if (blocked) {
b.connectionDuration.text = context.getString(R.string.symbol_red_circle)
b.connectionDuration.alpha = 0.7f
} else {
val duration = getDurationInHumanReadableFormat(context, ct.duration)
Comment thread
hussainmohd-a marked this conversation as resolved.
b.connectionDuration.text = context.getString(R.string.single_argument, duration)
b.connectionDuration.alpha = 1f
}
Comment thread
hussainmohd-a marked this conversation as resolved.

b.connectionSummaryLl.visibility = View.VISIBLE
val duration = getDurationInHumanReadableFormat(context, ct.duration)
b.connectionDuration.text = context.getString(R.string.single_argument, duration)
// add unicode for download and upload
val download =
context.getString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ import com.celzero.bravedns.service.IpRulesManager
import com.celzero.bravedns.service.ProxyManager
import com.celzero.bravedns.service.VpnController
import com.celzero.bravedns.service.WireguardManager
import com.celzero.bravedns.service.WireguardManager.ERR_CODE_OTHER_WG_ACTIVE
import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_ACTIVE
import com.celzero.bravedns.service.WireguardManager.ERR_CODE_VPN_NOT_FULL
import com.celzero.bravedns.service.WireguardManager.ERR_CODE_WG_INVALID
import com.celzero.bravedns.service.WireguardManager.WG_UPTIME_THRESHOLD
import com.celzero.bravedns.ui.activity.WgConfigDetailActivity
import com.celzero.bravedns.ui.activity.WgConfigDetailActivity.Companion.INTENT_EXTRA_WG_TYPE
Expand All @@ -56,6 +52,7 @@ import com.celzero.bravedns.util.UIUtils
import com.celzero.bravedns.util.UIUtils.fetchColor
import com.celzero.bravedns.util.Utilities
import com.celzero.firestack.backend.RouterStats
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import inet.ipaddr.HostName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -438,7 +435,6 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns
uiCtx {
Utilities.showToastUiCentered(
context,
ERR_CODE_VPN_NOT_ACTIVE +
context.getString(R.string.settings_socks5_vpn_disabled_error),
Toast.LENGTH_LONG
)
Expand All @@ -448,30 +444,15 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns
return
}

// checks only for app mode, so show appropriate error message if not enabled
if (!WireguardManager.canEnableProxy()) {
Logger.i(LOG_TAG_PROXY, "not in DNS+Firewall mode, cannot enable WireGuard")
uiCtx {
// reset the check box
b.oneWgCheck.isChecked = false
Utilities.showToastUiCentered(
context,
ERR_CODE_VPN_NOT_FULL +
context.getString(R.string.wireguard_enabled_failure),
Toast.LENGTH_LONG
)
}
return
}

if (WireguardManager.isAnyOtherOneWgEnabled(config.id)) {
Logger.i(LOG_TAG_PROXY, "another WireGuard is already enabled")
uiCtx {
// reset the check box
b.oneWgCheck.isChecked = false
Utilities.showToastUiCentered(
context,
ERR_CODE_OTHER_WG_ACTIVE +
context.getString(R.string.wireguard_enabled_failure),
context.getString(R.string.wireguard_dns_mode_conflict),
Toast.LENGTH_LONG
)
}
Expand All @@ -483,33 +464,65 @@ class OneWgConfigAdapter(private val context: Context, private val listener: Dns
uiCtx {
// reset the check box
b.oneWgCheck.isChecked = false
Utilities.showToastUiCentered(
context,
ERR_CODE_WG_INVALID + context.getString(R.string.wireguard_enabled_failure),
Toast.LENGTH_LONG
)
showInvalidConfigDialog()
}
return
}

// One-WireGuard is mutually exclusive: if another one-wg config is active, swap it
// out for the newly selected one rather than blocking the user with an error.
var replacedOthers = false
if (WireguardManager.isAnyOtherOneWgEnabled(config.id)) {
Logger.i(LOG_TAG_PROXY, "another one-wg config is active; replacing it")
WireguardManager.disableOtherOneWireGuardConfigs(config.id)
replacedOthers = true
}
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment on lines +474 to +479

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
The new "replace" flow permanently disables the previously active one-wg config before enabling the newly selected one. disableOtherOneWireGuardConfigs() writes isActive=false/oneWireGuard=false to the DB for the old config, then enableConfig(...) runs. If enableConfig fails — either silently (early return when the mapping/config is missing) or by throwing (e.g. db.update/VpnController.addWireGuardProxy) — the old config is already disabled with no rollback, leaving the user with no active WireGuard tunnel and no error surfaced (the switch stays checked). The previous behavior blocked this case with an error, keeping the existing config intact. Consider enabling the new config first and disabling the others only on success, or wrapping the sequence in try/catch to restore the previous config and reset the switch state on failure.


Logger.i(LOG_TAG_PROXY, "enabling WireGuard, id: ${config.id}")
WireguardManager.updateOneWireGuardConfig(config.id, owg = true)
config.oneWireGuard = true
WireguardManager.enableConfig(config.toImmutable())
uiCtx { listener.onDnsStatusChanged() }
uiCtx {
listener.onDnsStatusChanged()
if (replacedOthers) {
Utilities.showToastUiCentered(
context,
context.getString(R.string.wireguard_replaced_active_config),
Toast.LENGTH_SHORT
)
}
}
logEvent("One-WireGuard enabled", "WG ID: ${config.id}")
}

// Shows an "invalid config" AlertDialog when an Activity context is
// available; otherwise falls back to a toast so the error is never silently dropped.
private fun showInvalidConfigDialog() {
val ctx = context
if (ctx is android.app.Activity && !ctx.isFinishing) {
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.
Comment thread
hussainmohd-a marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
showInvalidConfigDialog() only checks !ctx.isFinishing, not !ctx.isDestroyed. This code runs on the main thread after an IO round-trip, so the activity may already be destroyed (e.g. configuration change or navigating away) while not finishing, and calling .show() on a destroyed activity can throw WindowManager.BadTokenException. Add && !ctx.isDestroyed to match the existing lifecycle guards in the codebase (e.g. WgMainActivity.showOneWgToggle checks isDestroyed).

Suggestion:

Suggested change
if (ctx is android.app.Activity && !ctx.isFinishing) {
if (ctx is android.app.Activity && !ctx.isFinishing && !ctx.isDestroyed) {

MaterialAlertDialogBuilder(ctx, R.style.App_Dialog_NoDim)
.setTitle(R.string.wireguard_invalid_config_title)
.setMessage(R.string.wireguard_invalid_config_message)
.setCancelable(true)
.setPositiveButton(R.string.lbl_dismiss) { d, _ -> d.dismiss() }
.show()
} else {
Utilities.showToastUiCentered(
context,
context.getString(R.string.wireguard_invalid_config_message),
Toast.LENGTH_LONG
)
}
}

private suspend fun disableWgIfPossible(config: WgConfigFiles) {
if (!VpnController.hasTunnel()) {
Logger.i(LOG_TAG_PROXY, "VPN not active, cannot disable WireGuard")
uiCtx {
// reset the check box
b.oneWgCheck.isChecked = true
Utilities.showToastUiCentered(
context,
ERR_CODE_VPN_NOT_ACTIVE +
context.getString(R.string.settings_socks5_vpn_disabled_error),
context, context.getString(R.string.settings_socks5_vpn_disabled_error),
Toast.LENGTH_LONG
)
}
Expand Down
Loading
Loading