Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ app/src/headless/assets
/platform-tools
/platforms
/tools
/.kotlin/sessions
/.kotlin/
app/google-services.json
app/fdroidFull/release
app/playFull/release
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import android.content.pm.ApplicationInfo
import android.os.StrictMode
import com.celzero.bravedns.scheduler.ScheduleManager
import com.celzero.bravedns.scheduler.WorkScheduler
import com.celzero.bravedns.data.PowerProfileManager
import com.celzero.bravedns.util.FirebaseErrorReporting
import com.celzero.bravedns.util.GlobalExceptionHandler
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -56,6 +57,7 @@ class RethinkDnsApplication : Application() {
turnOnStrictMode()

CoroutineScope(SupervisorJob()).launch {
PowerProfileManager.reconcileActiveProfiles(this@RethinkDnsApplication)
scheduleJobs()
}
}
Expand Down
35 changes: 27 additions & 8 deletions app/src/full/java/com/celzero/bravedns/ui/HomeScreenActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.celzero.bravedns.backup.BackupHelper.Companion.INTENT_RESTART_APP
import com.celzero.bravedns.backup.BackupHelper.Companion.INTENT_SCHEME
import com.celzero.bravedns.backup.RestoreAgent
import com.celzero.bravedns.data.AppConfig
import com.celzero.bravedns.data.PowerProfileManager
import com.celzero.bravedns.database.AppInfoRepository
import com.celzero.bravedns.database.RefreshDatabase
import com.celzero.bravedns.service.AppUpdater
Expand Down Expand Up @@ -191,6 +192,9 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) {

override fun onResume() {
super.onResume()
lifecycleScope.launch {
PowerProfileManager.reconcileActiveProfiles(this@HomeScreenActivity)
}
// if app is coming from background, don't reset the activity stack
if (appInBackground) {
appInBackground = false
Expand Down Expand Up @@ -738,8 +742,15 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment
val navController = navHostFragment?.navController
val homeId = R.id.homeScreenFragment
if (navController?.currentDestination?.id != homeId) {
val homeId = R.id.powerFragment
val currentDestinationId = navController?.currentDestination?.id
if (
currentDestinationId != null &&
PowerDestinationPolicy.isDeepPowerDestination(currentDestinationId) &&
navController.previousBackStackEntry != null
) {
navController.navigateUp()
} else if (currentDestinationId != homeId) {
val btmNavView = findViewById<BottomNavigationView>(R.id.nav_view)
btmNavView.selectedItemId = homeId
navController?.navigate(
Expand Down Expand Up @@ -769,7 +780,7 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) {
val navController = navHostFragment?.navController

btmNavView.setOnItemSelectedListener { item ->
val homeId = R.id.homeScreenFragment
val homeId = R.id.powerFragment

when (item.itemId) {
R.id.rethinkPlus -> {
Expand Down Expand Up @@ -828,11 +839,19 @@ class HomeScreenActivity : AppCompatActivity(R.layout.activity_home_screen) {
}
}

// Optionally sync the bottom nav highlight with nav changes
/*navController?.addOnDestinationChangedListener { _, destination, _ ->
// Update Rethink Plus badge or icon here if needed
updateRethinkPlusHighlight()
}*/
navController?.addOnDestinationChangedListener { _, destination, _ ->
val destinationId = destination.id
btmNavView.visibility =
if (PowerDestinationPolicy.isDeepPowerDestination(destinationId)) View.GONE else View.VISIBLE

val menuItem = PowerDestinationPolicy.topLevelMenuItem(destinationId)

menuItem?.let {
if (btmNavView.selectedItemId != it) {
btmNavView.menu.findItem(it)?.isChecked = true
}
}
}
}

private fun io(f: suspend () -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 RethinkDNS and its authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.celzero.bravedns.ui

import com.celzero.bravedns.R

object PowerDestinationPolicy {
fun isDeepPowerDestination(destinationId: Int): Boolean {
return destinationId == R.id.activeProfilesFragment ||
destinationId == R.id.discoverProfilesFragment ||
destinationId == R.id.powerProfileDetailFragment ||
destinationId == R.id.powerProfileEntriesFragment ||
destinationId == R.id.powerProfileAppsFragment
}

fun topLevelMenuItem(destinationId: Int): Int? {
return when (destinationId) {
R.id.powerFragment, R.id.homeScreenFragment -> R.id.powerFragment
R.id.summaryStatisticsFragment -> R.id.summaryStatisticsFragment
R.id.configureFragment -> R.id.configureFragment
R.id.aboutFragment -> R.id.aboutFragment
else -> null
}
}
}
169 changes: 167 additions & 2 deletions app/src/full/java/com/celzero/bravedns/ui/activity/AppInfoActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ import com.bumptech.glide.Glide
import com.celzero.bravedns.R
import com.celzero.bravedns.adapter.AppWiseDomainsAdapter
import com.celzero.bravedns.adapter.AppWiseIpsAdapter
import com.celzero.bravedns.data.PowerProfileAppBlocklist
import com.celzero.bravedns.data.PowerProfileAppDomainRule
import com.celzero.bravedns.data.PowerProfileAppIpRule
import com.celzero.bravedns.data.PowerProfileAppManager
import com.celzero.bravedns.database.AppInfo
import com.celzero.bravedns.database.EventSource
import com.celzero.bravedns.database.EventType
Expand All @@ -63,7 +67,6 @@ import com.celzero.bravedns.util.UIUtils.openAndroidAppInfo
import com.celzero.bravedns.util.Utilities
import com.celzero.bravedns.util.Utilities.isAtleastQ
import com.celzero.bravedns.util.Utilities.showToastUiCentered
import com.celzero.bravedns.util.handleFrostEffectIfNeeded
import com.celzero.bravedns.viewmodel.AppConnectionsViewModel
import com.celzero.bravedns.viewmodel.CustomDomainViewModel
import com.celzero.bravedns.viewmodel.CustomIpViewModel
Expand All @@ -90,32 +93,43 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) {

private var appStatus = FirewallManager.FirewallStatus.NONE
private var connStatus = FirewallManager.ConnectionStatus.ALLOW
private var previewAppBlocklist: PowerProfileAppBlocklist? = null

private var showBypassToolTip: Boolean = true

companion object {
const val INTENT_UID = "UID"
const val INTENT_ACTIVE_CONNS = "ACTIVE_CONNS"
const val INTENT_ASN = "ASN"
const val INTENT_PREVIEW_PROFILE_ID = "PREVIEW_PROFILE_ID"
const val INTENT_PREVIEW_APP_PACKAGE = "PREVIEW_APP_PACKAGE"
private const val TAG = "AppInfoActivity"

// Temp allow duration constants
private const val TEMP_ALLOW_DURATION_MINUTES = 15
private const val MILLIS_PER_MINUTE = 60
private const val MILLIS_PER_SECOND = 1000L
private const val ALPHA_DISABLED = 0.5f
private const val PREVIEW_RULE_DIALOG_LIMIT = 120
}

override fun onCreate(savedInstanceState: Bundle?) {
theme.applyStyle(Themes.getCurrentTheme(isDarkThemeOn(), persistentState.theme), true)
super.onCreate(savedInstanceState)
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

AppInfoActivity no longer calls handleFrostEffectIfNeeded(persistentState.theme) in onCreate(), while most other activities still do. This can cause inconsistent UI theming/blur behavior for the App Details screen. Consider restoring the call for consistency with the rest of the app.

Suggested change
super.onCreate(savedInstanceState)
super.onCreate(savedInstanceState)
handleFrostEffectIfNeeded(persistentState.theme)

Copilot uses AI. Check for mistakes.
handleFrostEffectIfNeeded(persistentState.theme)
if (isAtleastQ()) {
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightNavigationBars = false
window.isNavigationBarContrastEnforced = false
}

val previewProfileId = intent.getStringExtra(INTENT_PREVIEW_PROFILE_ID).orEmpty()
val previewAppPackage = intent.getStringExtra(INTENT_PREVIEW_APP_PACKAGE).orEmpty()
if (previewProfileId.isNotEmpty() && previewAppPackage.isNotEmpty()) {
setupClickListeners()
initPreviewMode(previewProfileId, previewAppPackage)
return
}

uid = intent.getIntExtra(INTENT_UID, INVALID_UID)
Logger.d(LOG_TAG_UI, "AppInfoActivity, intent uid: $uid")
ipRulesViewModel.setUid(uid)
Expand Down Expand Up @@ -192,6 +206,53 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) {
}
}

private fun initPreviewMode(profileId: String, packageName: String) {
io {
val preview = PowerProfileAppManager.buildPreview(this@AppInfoActivity, profileId, packageName)
if (preview == null) {
uiCtx { showNoAppFoundDialog() }
return@io
}

val installedApp = preview.installedAppInfo
val previewAppInfo =
installedApp ?: buildPreviewAppInfo(preview.appBlocklist.packageName, preview.appBlocklist.appName)
uid = previewAppInfo.uid
appInfo = previewAppInfo
previewAppBlocklist = preview.appBlocklist
appStatus = FirewallManager.FirewallStatus.getStatus(preview.appBlocklist.firewallStatus)
connStatus = FirewallManager.ConnectionStatus.getStatus(preview.appBlocklist.connectionStatus)

uiCtx {
b.aadAppDetailName.text = previewAppInfo.appName
b.aadPkgName.text = previewAppInfo.packageName
displayIcon(
Utilities.getIcon(this@AppInfoActivity, previewAppInfo.packageName, previewAppInfo.appName),
b.aadAppDetailIcon
)
applyPreviewUi()
setPreviewRuleCounts(
ipRuleCount = preview.appBlocklist.ipRules.size,
domainRuleCount = preview.appBlocklist.domainRules.size
)
updateFirewallStatusUi(appStatus, connStatus)
PowerProfilePreviewUiPolicy.applyReadOnlyFirewallControls(b)
b.aadFirewallStatus.text =
getString(
R.string.power_profile_app_preview_status,
getFirewallText(appStatus, connStatus)
)
b.aadDataUsageStatus.text =
getString(R.string.power_profile_app_preview_data_usage) +
" " +
getString(
R.string.power_profile_app_preview_source,
preview.profile.resolveTitle(this@AppInfoActivity)
)
}
}
}

private fun displayProxyStatus() {
val proxy = ProxyManager.getProxyIdForApp(uid)
if (proxy.isEmpty() || proxy == ID_NONE) {
Expand All @@ -203,19 +264,123 @@ class AppInfoActivity : AppCompatActivity(R.layout.activity_app_details) {
}

private fun openCustomIpScreen() {
previewAppBlocklist?.let {
if (it.ipRules.isEmpty()) return
showPreviewIpRulesDialog(it)
return
}
val intent = Intent(this, CustomRulesActivity::class.java)
intent.putExtra(VIEW_PAGER_SCREEN_TO_LOAD, CustomRulesActivity.Tabs.IP_RULES.screen)
intent.putExtra(Constants.INTENT_UID, uid)
startActivity(intent)
}

private fun buildPreviewAppInfo(packageName: String, appName: String): AppInfo {
return AppInfo(
packageName = packageName,
appName = appName,
uid = INVALID_UID,
isSystemApp = false,
firewallStatus = FirewallManager.FirewallStatus.NONE.id,
appCategory = "",
wifiDataUsed = 0,
mobileDataUsed = 0,
connectionStatus = FirewallManager.ConnectionStatus.ALLOW.id,
isProxyExcluded = false,
screenOffAllowed = false,
backgroundAllowed = false
)
}

private fun applyPreviewUi() {
b.aadProxyDetails.visibility = View.GONE
b.aadAppInfoIcon.visibility = View.GONE
b.aadActiveConnsRl.visibility = View.GONE
b.aadAsnRl.visibility = View.GONE
b.aadMostContactedDomainRl.visibility = View.GONE
b.aadMostContactedIpsRl.visibility = View.GONE
b.excludeProxyRl.visibility = View.GONE
b.tempAllowRl.visibility = View.GONE
b.aadCloseConnsChip.visibility = View.GONE
b.aadIpsChip.visibility = View.GONE
b.aadDomainsChip.visibility = View.GONE
b.aadActiveConnsChip.visibility = View.GONE
b.aadAsnChip.visibility = View.GONE
val hasIpRules = (previewAppBlocklist?.ipRules?.isNotEmpty() == true)
val hasDomainRules = (previewAppBlocklist?.domainRules?.isNotEmpty() == true)
PowerProfilePreviewUiPolicy.applyRulePreviewState(b, hasIpRules, hasDomainRules)
}

private fun setPreviewRuleCounts(ipRuleCount: Int, domainRuleCount: Int) {
b.aadIpBlockHeader.text = ipRuleCount.toString()
b.aadDomainBlockHeader.text = domainRuleCount.toString()
}

private fun openCustomDomainScreen() {
previewAppBlocklist?.let {
if (it.domainRules.isEmpty()) return
showPreviewDomainRulesDialog(it)
return
}
val intent = Intent(this, CustomRulesActivity::class.java)
intent.putExtra(VIEW_PAGER_SCREEN_TO_LOAD, CustomRulesActivity.Tabs.DOMAIN_RULES.screen)
intent.putExtra(Constants.INTENT_UID, uid)
startActivity(intent)
}

private fun showPreviewIpRulesDialog(appBlocklist: PowerProfileAppBlocklist) {
showPreviewRulesDialog(
title = getString(R.string.lbl_ip_rules),
previewLines = appBlocklist.ipRules.map(::formatPreviewIpRule),
emptyMessage = getString(R.string.power_profile_app_preview_ip_rules_empty)
)
}

private fun showPreviewDomainRulesDialog(appBlocklist: PowerProfileAppBlocklist) {
showPreviewRulesDialog(
title = getString(R.string.lbl_domain_rules),
previewLines = appBlocklist.domainRules.map(::formatPreviewDomainRule),
emptyMessage = getString(R.string.power_profile_app_preview_domain_rules_empty)
)
}

private fun showPreviewRulesDialog(
title: String,
previewLines: List<String>,
emptyMessage: String
) {
val message =
if (previewLines.isEmpty()) {
emptyMessage
} else {
val shown = previewLines.take(PREVIEW_RULE_DIALOG_LIMIT)
val previewText = shown.joinToString("\n")
val meta =
getString(
R.string.power_profile_entries_preview_meta,
shown.size.toString(),
previewLines.size.toString()
)
"$previewText\n\n$meta"
}

MaterialAlertDialogBuilder(this, R.style.App_Dialog_NoDim)
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.lbl_dismiss, null)
.show()
}

private fun formatPreviewIpRule(rule: PowerProfileAppIpRule): String {
val port = if (rule.port > 0) ":${rule.port}" else ""
val protocol = rule.protocol.takeIf { it.isNotBlank() }?.let { " ($it)" }.orEmpty()
return "${rule.ipAddress}$port$protocol"
}

private fun formatPreviewDomainRule(rule: PowerProfileAppDomainRule): String {
return rule.domain
}

private fun displayDataUsage() {
if (!::appInfo.isInitialized) {
Logger.w(LOG_TAG_UI, "AppInfo not initialized yet in displayDataUsage")
Expand Down
Loading