Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -70,6 +70,10 @@ class AppPreferences(private val context: Context) {
private val KEY_PAUSE_ON_TRUSTED = booleanPreferencesKey("pause_on_trusted")
private val KEY_PAUSED_BY_TRUSTED = booleanPreferencesKey("paused_by_trusted")
private val KEY_PAUSED_TRUSTED_SSID = stringPreferencesKey("paused_trusted_ssid")
private val KEY_LOCKDOWN_ENABLED = booleanPreferencesKey("lockdown_enabled")
private val KEY_LOCKDOWN_DURATION = longPreferencesKey("lockdown_duration")
private val KEY_COOLDOWN_START_TIMESTAMP = longPreferencesKey("cooldown_start_timestamp")
private val KEY_LAST_ACTIVE_TIMESTAMP = longPreferencesKey("last_active_timestamp")

const val ROUTING_MODE_DIRECT = "direct"
const val ROUTING_MODE_WIREGUARD = "wireguard"
Expand Down Expand Up @@ -150,6 +154,22 @@ class AppPreferences(private val context: Context) {
prefs[KEY_VPN_ENABLED] ?: false
}

val lockdownEnabled: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[KEY_LOCKDOWN_ENABLED] ?: false
}

val lockdownDuration: Flow<Long> = context.dataStore.data.map { prefs ->
prefs[KEY_LOCKDOWN_DURATION] ?: 300000L
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

val cooldownStartTimestamp: Flow<Long> = context.dataStore.data.map { prefs ->
prefs[KEY_COOLDOWN_START_TIMESTAMP] ?: 0L
}

val lastActiveTimestamp: Flow<Long> = context.dataStore.data.map { prefs ->
prefs[KEY_LAST_ACTIVE_TIMESTAMP] ?: 0L
}

val autoReconnect: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[KEY_AUTO_RECONNECT] ?: true
}
Expand Down Expand Up @@ -383,6 +403,30 @@ class AppPreferences(private val context: Context) {
}
}

suspend fun setLockdownEnabled(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[KEY_LOCKDOWN_ENABLED] = enabled
}
}

suspend fun setLockdownDuration(ms: Long) {
context.dataStore.edit { prefs ->
prefs[KEY_LOCKDOWN_DURATION] = ms
}
}

suspend fun setCooldownStartTimestamp(timestamp: Long) {
context.dataStore.edit { prefs ->
prefs[KEY_COOLDOWN_START_TIMESTAMP] = timestamp
}
}

suspend fun setLastActiveTimestamp(timestamp: Long) {
context.dataStore.edit { prefs ->
prefs[KEY_LAST_ACTIVE_TIMESTAMP] = timestamp
}
}

suspend fun setAutoReconnect(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[KEY_AUTO_RECONNECT] = enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import app.pwhs.blockads.MainActivity
import app.pwhs.blockads.R
import app.pwhs.blockads.utils.VpnUtils

import org.koin.android.ext.android.inject
import org.koin.android.ext.android.inject
import app.pwhs.blockads.data.datastore.AppPreferences
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

class AdBlockTileService : TileService() {
Expand All @@ -33,6 +35,12 @@ class AdBlockTileService : TileService() {
val isRootProxyRunning = RootProxyService.isRunning
val isVpnRunning = AdBlockVpnService.isRunning

val isLocked = runBlocking { appPrefs.lockdownEnabled.first() }
if (isLocked && (isRootProxyRunning || isVpnRunning)) {
updateTileState()
return
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (isRootProxyRunning) {
RootProxyService.stop(this)
} else if (isVpnRunning) {
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/app/pwhs/blockads/service/AdBlockVpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,21 @@ class AdBlockVpnService : VpnService() {

when (intent?.action) {
ACTION_STOP -> {
val isLocked = runBlocking { appPrefs.lockdownEnabled.first() }
if (isLocked) {
Timber.w("Stop request ignored: VPN is in Lockdown Mode.")
return START_STICKY
}
stopVpn()
return START_NOT_STICKY
}

ACTION_PAUSE_1H -> {
val isLocked = runBlocking { appPrefs.lockdownEnabled.first() }
if (isLocked) {
Timber.w("Pause request ignored: VPN is in Lockdown Mode.")
return START_STICKY
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
pauseVpn()
return START_NOT_STICKY
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/app/pwhs/blockads/service/BootReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ class BootReceiver : BroadcastReceiver() {
val autoReconnect = prefs.autoReconnect.first()
val wasEnabled = prefs.vpnEnabled.first()
val routingMode = prefs.routingMode.first()
val isLocked = prefs.lockdownEnabled.first()

// Root Mode: iptables rules are volatile (cleared on reboot).
// Re-apply rules by starting RootProxyService.
// Also restarts after app update (MY_PACKAGE_REPLACED).
if (autoReconnect && wasEnabled) {
if (isLocked || (autoReconnect && wasEnabled)) {
val trigger = if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) "app update" else "boot"
if (routingMode == AppPreferences.ROUTING_MODE_ROOT) {
Timber.d("Auto-starting Root Proxy mode after $trigger")
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/app/pwhs/blockads/service/RootProxyService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,20 @@ class RootProxyService : Service() {

when (intent?.action) {
ACTION_STOP -> {
val isLocked = kotlinx.coroutines.runBlocking { appPrefs.lockdownEnabled.first() }
if (isLocked) {
Timber.w("Stop request ignored: Root Proxy is in Lockdown Mode.")
return START_STICKY
}
stopProxy()
return START_NOT_STICKY
}
ACTION_PAUSE_1H -> {
val isLocked = kotlinx.coroutines.runBlocking { appPrefs.lockdownEnabled.first() }
if (isLocked) {
Timber.w("Pause request ignored: Root Proxy is in Lockdown Mode.")
return START_STICKY
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
pauseProxy()
return START_NOT_STICKY
}
Expand Down
19 changes: 14 additions & 5 deletions app/src/main/java/app/pwhs/blockads/service/ServiceController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber

/**
* Unified service controller that dispatches restart/stop requests
Expand Down Expand Up @@ -52,11 +53,19 @@ object ServiceController {
* Stop whichever service is currently running.
*/
fun requestStop(context: Context) {
if (RootProxyService.isRunning) {
RootProxyService.stop(context)
}
if (AdBlockVpnService.isRunning) {
AdBlockVpnService.stop(context)
CoroutineScope(Dispatchers.IO).launch {
val appPrefs = AppPreferences(context)
val isLocked = appPrefs.lockdownEnabled.first()
if (isLocked) {
Timber.w("Stop request ignored: System is in Lockdown Mode.")
return@launch
}
if (RootProxyService.isRunning) {
RootProxyService.stop(context)
}
if (AdBlockVpnService.isRunning) {
AdBlockVpnService.stop(context)
}
}
}
}
97 changes: 66 additions & 31 deletions app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package app.pwhs.blockads.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.fillMaxSize
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
Expand All @@ -11,6 +14,7 @@ import app.pwhs.blockads.ui.data.SplashKey
import app.pwhs.blockads.ui.dialog.VPNConflictDialog
import app.pwhs.blockads.ui.onboarding.OnboardingScreen
import app.pwhs.blockads.ui.splash.SplashScreen
import kotlinx.coroutines.launch


@Composable
Expand All @@ -29,38 +33,69 @@ fun BlockAdsApp(
}

val backStack = rememberNavBackStack(SplashKey)
NavDisplay(
backStack = backStack,
onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
modifier = modifier,
entryProvider = entryProvider {
entry<SplashKey> {
SplashScreen(
onNavigateToHome = {
backStack.removeLastOrNull()
backStack.add(HomeAppKey)
},
onNavigateToOnboarding = {
backStack.removeLastOrNull()
backStack.add(OnboardingKey)
}
)
val appPrefs: app.pwhs.blockads.data.datastore.AppPreferences = org.koin.compose.koinInject()
val isLocked by appPrefs.lockdownEnabled.collectAsState(initial = false)
val cooldownStart by appPrefs.cooldownStartTimestamp.collectAsState(initial = 0L)
val duration by appPrefs.lockdownDuration.collectAsState(initial = 300000L)
val coroutineScope = androidx.compose.runtime.rememberCoroutineScope()

androidx.compose.foundation.layout.Box(modifier = androidx.compose.ui.Modifier.fillMaxSize()) {
NavDisplay(
backStack = backStack,
onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
modifier = modifier,
entryProvider = entryProvider {
entry<SplashKey> {
SplashScreen(
onNavigateToHome = {
backStack.removeLastOrNull()
backStack.add(HomeAppKey)
},
onNavigateToOnboarding = {
backStack.removeLastOrNull()
backStack.add(OnboardingKey)
}
)
}
entry<OnboardingKey> {
OnboardingScreen(
onNavigateToHome = {
backStack.removeLastOrNull()
backStack.add(HomeAppKey)
}
)
}
entry<HomeAppKey> {
HomeApp(
onRequestVpnPermission = onRequestVpnPermission,
onShowVpnConflictDialog = onShowVpnConflictDialog
)
}
}
entry<OnboardingKey> {
OnboardingScreen(
onNavigateToHome = {
backStack.removeLastOrNull()
backStack.add(HomeAppKey)
)

if (isLocked) {
LockdownScreen(
cooldownStart = cooldownStart,
duration = duration,
onStartCooldown = { timestamp ->
coroutineScope.launch { appPrefs.setCooldownStartTimestamp(timestamp) }
},
onCancelCooldown = {
coroutineScope.launch { appPrefs.setCooldownStartTimestamp(0L) }
},
onUnlockComplete = {
coroutineScope.launch {
appPrefs.setLockdownEnabled(false)
appPrefs.setCooldownStartTimestamp(0L)
}
)
}
entry<HomeAppKey> {
HomeApp(
onRequestVpnPermission = onRequestVpnPermission,
onShowVpnConflictDialog = onShowVpnConflictDialog
)
}

},
onTimeTamperingDetected = {
coroutineScope.launch {
appPrefs.setCooldownStartTimestamp(0L)
}
}
)
}
)
}
}
Loading
Loading