Skip to content

Cooldown Feature for digital addictions - #204

Open
TaqiHamoda wants to merge 14 commits into
pass-with-high-score:mainfrom
TaqiHamoda:main
Open

Cooldown Feature for digital addictions#204
TaqiHamoda wants to merge 14 commits into
pass-with-high-score:mainfrom
TaqiHamoda:main

Conversation

@TaqiHamoda

@TaqiHamoda TaqiHamoda commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

For users dealing with digital addictions (such as gambling, social media, shopping, or adult content), the standard ad-blocker suffers from an architectural limitation: the user can easily turn it off during a moment of weakness. In a split second, an impulsive urge can lead the user to open settings, whitelist a domain, or disable the VPN entirely, bypassing their own self-imposed protection.

The VPN Timer Lock (Lockdown Mode) introduces cognitive friction to interrupt this immediate feedback/reward loop. By locking down settings and disabling direct shutdown switches, the app forces a delayed cooling-off period. To disable the protection, the user must initiate a countdown (e.g., 30 minutes). During this time, they cannot browse blocked sites, but they are given time to cool down, self-reflect, and ideally let the superficial urge pass.

Summary by CodeRabbit

  • New Features

    • Added Lockdown Mode with configurable cooldown durations, countdown display, cancellation, and clock-tampering detection.
    • Added Device Owner controls for enforcing or clearing device restrictions.
    • Added localized text for these features across multiple languages.
  • Bug Fixes

    • Prevented protection from being stopped, paused, or toggled while Lockdown Mode is active.
    • Ensured protection is restored automatically after device startup during lockdown.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds lockdown mode with configurable cooldown timing, clock-tampering detection, and guards for VPN stop, pause, and toggle actions. Adds device-owner support for always-on VPN and user restrictions, settings controls, persistence, validation, and localized resources.

Changes

Lockdown mode and device-owner controls

Layer / File(s) Summary
Lockdown preference contract and validation
app/.../data/datastore/AppPreferences.kt, app/src/test/.../LockdownDurationValidationTest.kt
Adds lockdown, cooldown, activity, and device-owner preferences. Validates durations and persists the new values. Tests cover allowed durations and defaults.
Lockdown settings and state wiring
app/.../ui/settings/...
Adds lockdown settings, duration selection, device-owner restriction state, and lockdown-aware toggle behavior.
Cooldown overlay and timer enforcement
app/.../ui/LockdownScreen.kt, app/.../ui/BlockAdsApp.kt
Adds the locked overlay, cooldown countdown, timestamp persistence, clock-tampering checks, cancellation, and unlock handling.
Lockdown guards across service entry points
app/.../service/..., app/.../widget/WidgetToggleReceiver.kt
Blocks stop, pause, and toggle actions during lockdown. Boot startup also runs when lockdown is enabled.
Device-owner enforcement and application wiring
app/src/main/AndroidManifest.xml, app/src/main/res/xml/device_admin_policies.xml, app/.../service/..., app/.../BlockAdsApplication.kt
Registers device-admin support, manages always-on VPN and user restrictions, and applies restrictions at startup and from settings.
Localized resources
app/src/main/res/values*/strings.xml
Adds Lockdown Mode and Device Owner Mode strings to the default and translated resources.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LockdownScreen
  participant BlockAdsApp
  participant AppPreferences
  participant AdBlockVpnService
  User->>LockdownScreen: Start cooldown
  LockdownScreen->>BlockAdsApp: Report cooldown timestamp
  BlockAdsApp->>AppPreferences: Persist cooldown and activity timestamps
  User->>AdBlockVpnService: Request stop or pause
  AdBlockVpnService->>AppPreferences: Read lockdownEnabled
  AdBlockVpnService->>AdBlockVpnService: Ignore action while locked
  LockdownScreen->>BlockAdsApp: Report completion or tampering
  BlockAdsApp->>AppPreferences: Clear cooldown or disable lockdown
Loading

Possibly related PRs

Suggested reviewers: nqmgaming

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the cooldown feature and its purpose, but it does not mention the primary Lockdown Mode or VPN protection behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch main
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TaqiHamoda

Copy link
Copy Markdown
Contributor Author

Here are some screenshots from my phone regarding what the implemented feature looks like:

Screenshot_20260624-022440.png

Screenshot_20260624-022419.png

Screenshot_20260624-022429.png

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt`:
- Around line 161-163: The lockdownDuration Flow property does not validate that
stored durations are within allowed preset values, allowing invalid durations
(0, negative, or non-preset values) that can weaken cooldown behavior. Add
validation at both the write-time (around line 412 where the duration is set)
and read-time (in the lockdownDuration Flow mapping) to normalize any incoming
duration to the closest allowed preset duration. First define the allowed preset
durations as constants, then apply this normalization logic in both the setter
operation and the map transformation of the lockdownDuration Flow to ensure
consistency.

In `@app/src/main/java/app/pwhs/blockads/service/AdBlockTileService.kt`:
- Around line 38-43: The onClick() callback in AdBlockTileService is blocking
the SystemUI thread by using runBlocking to read from appPrefs.lockdownEnabled.
Replace the runBlocking call with a non-blocking coroutine-based approach by
launching a coroutine using an appropriate scope (such as viewModelScope or
lifecycleScope if available in the tile service context). Read the
lockdownEnabled value asynchronously and then perform the updateTileState() and
return logic within the coroutine without blocking the caller. Apply the same
fix to line 49 where runBlocking is also used.

In `@app/src/main/java/app/pwhs/blockads/service/AdBlockVpnService.kt`:
- Around line 272-286: Replace the blocking `runBlocking {
appPrefs.lockdownEnabled.first() }` calls in the onStartCommand method with
non-blocking coroutine launches using the available serviceScope. Instead of
synchronously reading the lockdownEnabled preference and blocking execution,
launch an async coroutine using serviceScope that checks the lockdownEnabled
state and performs the appropriate action (either returning early with Timber
logging or calling stopVpn). Ensure the onStartCommand method returns
START_STICKY immediately to indicate the service will continue processing
asynchronously, rather than blocking the main thread waiting for the DataStore
read to complete. This pattern should be applied at both occurrences where
lockdownEnabled is currently being read in a blocking manner.

In `@app/src/main/java/app/pwhs/blockads/service/RootProxyService.kt`:
- Around line 151-164: The onStartCommand callback is using runBlocking on the
main thread to check appPrefs.lockdownEnabled for both the ACTION_STOP and
ACTION_PAUSE_1H cases, which can block service command handling. Replace these
blocking calls with a non-blocking coroutine approach: use a coroutine scope
(such as lifecycleScope.launch or a custom scope tied to the service) to
asynchronously check the lockdown status, and move the conditional logic that
calls stopProxy() and returns START_STICKY or START_NOT_STICKY inside the
coroutine block. Ensure the service returns START_REDELIVER_INTENT or another
appropriate value immediately from onStartCommand while the async check happens
in the background.

In `@app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt`:
- Around line 150-156: The durationText construction in the LockdownScreen.kt
file hardcodes English strings "hour", "hours", and "minutes" instead of using
localized resources. Move these string literals to Android string resources so
they can be properly localized for different languages. The hour/minute duration
labels should be extracted from the durationText variable and replaced with
references to localized string resources, or reuse any localized duration label
utilities that were introduced elsewhere in this PR to maintain consistency.
- Around line 31-50: The lastActiveTime variable retains its initial value
across idle periods and countdown cycles. When a countdown starts after the user
has been on the screen for more than 5 minutes, the check at line 45 comparing
the time difference incorrectly triggers tampering detection. Reset
lastActiveTime to System.currentTimeMillis() immediately after entering the
LaunchedEffect block (after verifying cooldownStart > 0L) so the tamper
detection logic uses a fresh baseline from when the countdown actually begins,
not from when the user initially loaded the screen.

In `@app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt`:
- Around line 148-154: The setLockdownEnabled function has a race condition
where the service start decision relies on the passed-in enabled parameter
instead of the latest persisted state. If the user quickly toggles ON→OFF, a
stale coroutine could still call ServiceController.requestStart even though the
state is already OFF. After calling appPrefs.setLockdownEnabled(enabled),
re-check the current persisted state from appPrefs (using the appropriate getter
method) before the if condition that calls ServiceController.requestStart,
rather than using the enabled parameter directly to ensure the most recent
stored state is used for the service control decision.

In `@app/src/main/res/values-iw/strings.xml`:
- Line 535: The Hebrew translation for singular durations in the
lockdown_duration_1m string (line 535) and the corresponding hour duration
string at line 539 has reversed word order that reads unnaturally in Hebrew.
Correct the string values by placing the number before the unit name instead of
after it, so that "דקה 1" becomes the proper singular form and "שעה 1" is
similarly corrected. Apply this same fix to both duration strings to ensure
natural Hebrew phrasing in the UI.

In `@app/src/main/res/values/strings.xml`:
- Line 580: Move the duration formatting logic from LockdownScreen.kt out of the
hardcoded English text construction and replace it with localized string
resources that handle pluralization properly. Create separate plural string
resources for hours and minutes in the strings.xml file, then update the code
that constructs durationText to use these localized resources instead of
concatenating hardcoded English units like "hour(s)" and "minutes". This ensures
the entire formatted duration string passed to the lockdown_screen_duration
placeholder will be properly localized for all languages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f645f49e-d1d9-43c9-8f81-082a19405df7

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab05cc and 7c55522.

📒 Files selected for processing (31)
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt
  • app/src/main/java/app/pwhs/blockads/service/AdBlockTileService.kt
  • app/src/main/java/app/pwhs/blockads/service/AdBlockVpnService.kt
  • app/src/main/java/app/pwhs/blockads/service/BootReceiver.kt
  • app/src/main/java/app/pwhs/blockads/service/RootProxyService.kt
  • app/src/main/java/app/pwhs/blockads/service/ServiceController.kt
  • app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
  • app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsScreen.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/component/LockdownSection.kt
  • app/src/main/java/app/pwhs/blockads/widget/WidgetToggleReceiver.kt
  • app/src/main/res/values-ar/strings.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-in/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-vi/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml

Comment thread app/src/main/java/app/pwhs/blockads/service/AdBlockTileService.kt Outdated
Comment thread app/src/main/java/app/pwhs/blockads/service/AdBlockVpnService.kt Outdated
Comment thread app/src/main/java/app/pwhs/blockads/service/RootProxyService.kt Outdated
Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt Outdated
Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt Outdated
Comment thread app/src/main/res/values-iw/strings.xml Outdated
Comment thread app/src/main/res/values/strings.xml
@nqmgaming

Copy link
Copy Markdown
Member

thanks I will check it

@TaqiHamoda

Copy link
Copy Markdown
Contributor Author

thanks I will check it

Thank you for all the hard work and for this amazing app! One small question, should I implement the suggestions that code rabbit made?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt`:
- Around line 62-65: The startup enforcement in BlockAdsApplication should
respect the user’s saved restriction state instead of reapplying restrictions
whenever DeviceOwnerManager reports device-owner mode. Update the app launch
flow around the device-owner check to consult the persisted enforcement toggle
before calling DeviceOwnerManager.enforceRestrictions(), or remove the ability
to clear restrictions if persistence is not supported. Use the existing
DeviceOwnerManager and the app startup logic to locate the change.

In `@app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt`:
- Around line 19-23: areRestrictionsEnforced() is only validating one
device-owner restriction, so the reported state can look complete even when
other policy pieces are missing. Update DeviceOwnerManager’s enforcement flow so
enforceRestrictions() and the clear path return success/failure for each
expected policy step, then have areRestrictionsEnforced() verify all required
restrictions and always-on VPN state, including DISALLOW_APPS_CONTROL,
DISALLOW_CONFIG_VPN, and DISALLOW_DEBUGGING_FEATURES, before returning true.

In
`@app/src/main/java/app/pwhs/blockads/ui/settings/component/DeviceOwnerSection.kt`:
- Around line 41-44: The DeviceOwnerSection lockdown state only dims the
SettingsToggleItem visually, but the toggleable remains interactive. Update
SettingsToggleItem to accept an enabled flag and wire it through to its
toggleable behavior, then pass !lockdownEnabled from DeviceOwnerSection so the
control is semantically disabled for accessibility and keyboard input when
lockdown is active.

In `@app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt`:
- Around line 490-498: `SettingsViewModel.setRestrictionsEnforced` currently
updates `_restrictionsEnforced` from the requested flag instead of the real
device policy state, so the UI can become stale when
`deviceOwnerManager.enforceRestrictions()` or `clearRestrictions()` no-op. After
calling the manager, re-read the state with
`deviceOwnerManager.areRestrictionsEnforced()` (or change `DeviceOwnerManager`
to return the actual Boolean result) and assign that value to
`_restrictionsEnforced` inside the `viewModelScope.launch(Dispatchers.IO)`
block.

In `@app/src/main/res/values-th/strings.xml`:
- Line 555: Update the localized string for
settings_device_owner_enforce_restrictions_desc to remove the misleading “clear
data” wording and instead state that disabling will clear or remove the enforced
restrictions only. Keep the meaning aligned with the existing restrictions
toggle in the same resource entry, so users understand it affects app
restrictions rather than app/device data.

In `@app/src/main/res/values-tr/strings.xml`:
- Line 576: Update the localized string for
settings_device_owner_enforce_restrictions_desc so the disable action is
explicit: keep the meaning that turning it off clears enforced restrictions, and
avoid wording that could imply clearing user data. Adjust the text in the
translation entry to clearly reference disabling enforced restrictions rather
than “clearing” generically.

In `@FEATURES.md`:
- Around line 34-466: The FEATURES.md links are using machine-local
file:///home/... paths, which won’t work for reviewers. Replace those absolute
links with repo-relative markdown links throughout the document, using the
existing target file names and paths referenced in the diff (for example the
SettingsScreen.kt, BlockAdsApp.kt, AppPreferences.kt, and service classes) so
the links resolve correctly in the repository.
- Line 255: The uninstall guidance is inconsistent with the behavior of
setUninstallBlocked(admin, packageName, true), which blocks removal in Settings
and via adb uninstall. Update the uninstall-related wording in the FEATURES.md
sections tied to App Uninstallation and the DO removal guidance so they all
describe the actual supported removal path, or remove the claim that advanced
users can still uninstall with standard ADB commands.
- Line 253: The VPN revocation row in FEATURES.md has a mismatch between the
stated behavior and the `setAlwaysOnVpnPackage(admin, packageName,
lockdownEnabled = false)` call. Update the `System VPN Revocation` description
to either pass `true` in `setAlwaysOnVpnPackage` if lockdown is intended, or
remove the lockdown/no-bypass wording from both references so the documented
security claim matches the API usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ce79516-510f-455c-9000-8e1c1a9e5315

📥 Commits

Reviewing files that changed from the base of the PR and between 7c55522 and f99267e.

📒 Files selected for processing (30)
  • CONCLUSION.md
  • FEATURES.md
  • NOTES.md
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt
  • app/src/main/java/app/pwhs/blockads/service/AdBlockDeviceAdminReceiver.kt
  • app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsScreen.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/component/DeviceOwnerSection.kt
  • app/src/main/res/values-ar/strings.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-in/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-vi/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/device_admin_policies.xml
✅ Files skipped from review due to trivial changes (12)
  • app/src/main/res/xml/device_admin_policies.xml
  • NOTES.md
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ar/strings.xml
  • CONCLUSION.md
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-vi/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-ko/strings.xml
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsScreen.kt
  • app/src/main/res/values-zh/strings.xml

Comment thread app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt Outdated
Comment thread app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt
Comment thread app/src/main/java/app/pwhs/blockads/ui/settings/component/DeviceOwnerSection.kt Outdated
Comment thread app/src/main/res/values-th/strings.xml Outdated
Comment thread app/src/main/res/values-tr/strings.xml Outdated
Comment thread FEATURES.md Outdated
Comment thread FEATURES.md Outdated
Comment thread FEATURES.md Outdated
@TaqiHamoda

TaqiHamoda commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

So to help with understanding the new changes and the motivation for this PR, I have made Gemini do a short write-up of the changes:

BlockAds Feature Specification: VPN Timer Lock (Lockdown Mode)

This document specifies the technical design, workflow, code changes, and edge-case handling for the VPN Lockdown Mode & Cooldown Timer feature.


🎯 Feature Overview & Motivation

For users dealing with digital addictions (such as gambling, social media, shopping, or adult content), the standard ad-blocker suffers from an architectural limitation: the user can easily turn it off during a moment of weakness. In a split second, an impulsive urge can lead the user to open settings, whitelist a domain, or disable the VPN entirely, bypassing their own self-imposed protection.

The VPN Timer Lock (Lockdown Mode) introduces cognitive friction to interrupt this immediate feedback/reward loop. By locking down settings and disabling direct shutdown switches, the app forces a delayed cooling-off period. To disable the protection, the user must initiate a countdown (e.g., 30 minutes). During this time, they cannot browse blocked sites, but they are given time to cool down, self-reflect, and ideally let the superficial urge pass.


🔄 User Experience (UX) Flow

stateDiagram-v2
    [*] --> Unlocked : Default State
    Unlocked --> SettingsScreen : Open settings
    SettingsScreen --> Locked : Enable VPN Lockdown + Set Cooldown (e.g., 30m)
    
    state Locked {
        [*] --> LockOverlayActive : App Launched
        LockOverlayActive --> CooldownRunning : Click "Start Cooldown Timer"
        CooldownRunning --> LockOverlayActive : Click "Cancel Cooldown" (Resets timer)
        CooldownRunning --> CooldownRunning : Countdown ticking down
    }
    
    CooldownRunning --> Unlocked : Timer reaches zero (Resets Settings)
Loading
  1. Enabling Lockdown:

    • The user opens SettingsScreen.kt and navigates to the new Impulse Control & Lockdown section.
    • They toggle VPN Lockdown ON and select a Cooldown Timer Duration from the options: 1m, 5m, 10m, 30m, 1h, 6h, 12h, 24h.
    • Upon enabling, the VPN service is immediately started (if off) and the app goes into Lockdown Mode.
  2. Locked State Operations:

    • The user cannot toggle the VPN switch OFF. Clicking the dashboard power toggle, app widgets, Tasker commands, or Quick Settings tiles is intercepted and ignored.
    • When the user opens the BlockAds app, a full-screen Lockdown Overlay blocks the rest of the application (excluding Splash & Onboarding). They cannot view or modify filter lists, whitelist apps, change DNS providers, or access settings.
    • The overlay displays a prominent message: "BlockAds is locked. To modify your settings or disable ad blocking, you must initiate the cooldown timer."
  3. Timer Unlocking Flow:

    • The user clicks "Start Cooldown Timer" on the Lockdown Overlay.
    • A timer begins counting down from their chosen duration (e.g., 30:00, 29:59...).
    • Important: At any point during the countdown, the user has a "Cancel Cooldown" button. If clicked, the timer is immediately aborted, resetting the countdown back to the full duration and returning the app to the initial locked state.
    • If the timer completes successfully (reaches zero), the lockdown state is deactivated. The app reverts to the standard Unlocked state, allowing modifications or VPN toggle control.

🛠️ Technical Design & Code Mapping

To ensure a foolproof lockdown, we must intercept VPN shutdown actions across all entry points and enforce the lockout screen overlay.

1. Data Layer Configuration

We introduce three state variables in AppPreferences.kt:

// AppPreferences.kt additions
val lockdownEnabled: Flow<Boolean> = dataStore.data.map { it[LOCKDOWN_ENABLED] ?: false }
val lockdownDuration: Flow<Long> = dataStore.data.map { it[LOCKDOWN_DURATION] ?: 300000L } // default 5 minutes
val cooldownStartTimestamp: Flow<Long> = dataStore.data.map { it[COOLDOWN_START_TIMESTAMP] ?: 0L }

suspend fun setLockdownEnabled(enabled: Boolean) {
    dataStore.edit { it[LOCKDOWN_ENABLED] = enabled }
}
suspend fun setLockdownDuration(ms: Long) {
    dataStore.edit { it[LOCKDOWN_DURATION] = ms }
}
suspend fun setCooldownStartTimestamp(timestamp: Long) {
    dataStore.edit { it[COOLDOWN_START_TIMESTAMP] = timestamp }
}

2. Lockout UI Overlay (Compose root)

To prevent the user from accessing setting views, we overlay a Compose layout at the root container BlockAdsApp.kt.

// BlockAdsApp.kt UI layout composition
@Composable
fun BlockAdsApp(...) {
    val appPrefs: AppPreferences = 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)

    Box(modifier = Modifier.fillMaxSize()) {
        // Main App Navigation
        NavDisplay(backStack = backStack, ...)

        // Root Lockdown Overlay
        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)
                    }
                }
            )
        }
    }
}

LockdownScreen Ticker Logic

Inside LockdownScreen, a timer checks the duration dynamically:

@Composable
fun LockdownScreen(
    cooldownStart: Long,
    duration: Long,
    onStartCooldown: (Long) -> Unit,
    onCancelCooldown: () -> Unit,
    onUnlockComplete: () -> Unit
) {
    var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
    
    // LaunchedEffect ticker running every 1 second
    LaunchedEffect(cooldownStart) {
        if (cooldownStart > 0L) {
            while (true) {
                currentTime = System.currentTimeMillis()
                val elapsed = currentTime - cooldownStart
                if (elapsed >= duration) {
                    onUnlockComplete()
                    break
                }
                delay(1000L)
            }
        }
    }
    
    // Calculate remaining seconds
    val remainingMs = if (cooldownStart > 0L) duration - (currentTime - cooldownStart) else duration
    val secondsLeft = (remainingMs / 1000).coerceAtLeast(0)

    // Render lockdown UI, warnings, lock icons, progress bar, and trigger buttons...
}

3. Service Lifecycle Guarding (Failsafe)

We prevent external or programmatic stop commands inside AdBlockVpnService.kt and RootProxyService.kt:

// Inside AdBlockVpnService.onStartCommand / stopVpn checks
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (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
        }
    }
    // standard command routing...
}

Similarly, in ServiceController.kt, prevent calling stop functions if lockdown is active.

4. Integration Broadcasts & Interceptors

We intercept requests from remote widgets, Tasker, and tiles:

  • Quick Settings Tile (AdBlockTileService.kt):
    Check lockdownEnabled snapshot inside onClick(). If locked, block state changes, keep tile Tile.STATE_ACTIVE, and dispatch a system status notification stating: "BlockAds VPN is locked. Open the app to begin the unlock cooldown."
  • App Widgets (WidgetToggleReceiver.kt):
    Inside toggleVpn, check lockdown preferences. If locked, abort stop, trigger a status notification, and send update broadcasts to keep the widget UI showing active.
  • Tasker Automation (TaskerReceiver.kt):
    Check lockdown status when receiving ACTION_STOP. Ignore the broadcast to maintain active filtering.

🛡️ Edge Cases & Handling Mitigations

1. Clock Manipulation (Time Tampering)

Scenario: The user starts a 12-hour cooldown timer and then shifts the Android system calendar clock forward 12 hours to force completion.

Mitigations:

  • Monotonic Boot Reference: When a cooldown starts, save the monotonic system time (SystemClock.elapsedRealtime()) and the startup epoch. While the app is active, compute elapsed time using SystemClock.elapsedRealtime().
  • Clock Regress Detection: Periodically persist lastActiveTimestamp = System.currentTimeMillis(). If the app launches and the current system time is earlier than lastActiveTimestamp, time-tampering is detected.
  • Time Jump Detection: If the difference between subsequent ticks is significantly larger than the delay duration (e.g., a jump of >10 minutes in 1 second), flag time-tampering.
  • Penalty Logic: Upon detecting time tampering, freeze the countdown timer, reset cooldownStartTimestamp = 0L to stop the cooldown, and notify the user that the timer has reset due to time tampering.

2. Device Reboots

Scenario: The user restarts the device to clean volatile states or break loop checking.

Mitigations:

  • Boot Persistence: The cooldownStartTimestamp is stored in the persistent DataStore preferences. Reboots do not clear it.
  • System Boot Override (BootReceiver.kt):
    Inside onReceive, if lockdownEnabled is true, immediately start the filtering VPN service, bypass the autoReconnect and wasEnabled flags, and enforce the VPN boot sequence.
  • Handling monotonic reset: Since SystemClock.elapsedRealtime() resets to zero on boot, detect if the monotonic reference is lost (boot time has reset) and fall back safely to System.currentTimeMillis() for calculation, but re-calculate constraints.

3. Settings Bypass via Back Press / Gestures

Scenario: The user attempts to dismiss the Lockout screen via back navigation or system gestures.

Mitigations:

  • The overlay layout uses Compose BackHandler which overrides back presses while lockdown is active:
    BackHandler(enabled = isLocked) {
        // Do nothing, consuming the back press action entirely
    }
  • Because the overlay is declared outside the navigation stacks inside the root layout in BlockAdsApp.kt, Android routing keys cannot swap or transition it away.

4. TV Platform Limitations

Scenario: How does the TV Companion App handle the lock?

Mitigations:

  • The lock feature is primarily targeted toward mobile devices where personal digital addictions are most prevalent.
  • In the TV app (blockadstv), we can either synchronize preferences via accounts (if a shared cloud DB exists) or keep the TV app unlocked since system settings access on TV devices is rare and remote navigation is tedious. Mobile-only configuration is recommended for the initial implementation phase.

📈 Standard Sandbox Limitations vs. Enterprise Enforcement

In a standard Android sandboxed environment, a regular application is constrained by the OS security model. Without administrative privileges, the following bypass actions cannot be fully prevented at the system level:

  1. System VPN Revocation: The user can navigate to Settings -> Network -> VPN and toggle the VPN off or remove the profile.
  2. Force Stopping the App: The user can open Settings -> Apps -> BlockAds and click Force Stop, halting all background processes.
  3. App Uninstallation: The user can uninstall BlockAds, removing all constraints.

While the standard Lockdown Mode relies on cognitive friction (adding steps to interrupt impulsive urges), advanced users or individuals experiencing strong urges can bypass it by performing these OS-level actions.

To address these vulnerabilities and offer an unbreakable impulse control mechanism, BlockAds can be configured in Android Device Owner (DO) Mode. This leverages Android Enterprise APIs to enforce system-level compliance, effectively blocking the standard bypass vectors.


🏢 Android Device Owner (DO) Mode: Unbreakable Impulse Control

Android Device Owner (DO) mode allows BlockAds to act as the device's Device Policy Controller (DPC). By enrolling the app with elevated administrative privileges, we can call privileged APIs under DevicePolicyManager to lock down the operating system itself.

🤫 Hidden Activation & Auto-Enforcement Design

To prevent average users from being confused or accidentally lock-in their settings, DO Mode is designed as a hidden, auto-enforcing state:

  1. Zero UI Triggers for Regular Users: There is no toggle or settings entry to "Enable Device Owner" in standard mode.
  2. Auto-Enforcement on Boot: On app startup, BlockAds queries the system (DevicePolicyManager.isDeviceOwnerApp()). If provisioned as Device Owner, it immediately and automatically applies all restrictions. If not provisioned, it falls back to standard sandbox mode with absolutely no visual differences or changes to the user interface.
  3. Guarded Settings Visibility: The UI elements to manage DO state are dynamically exposed only if the app is already installed as Device Owner. These elements include:
    • Clear/Suspend Restrictions: Temporarily lifts system restrictions (e.g. Always-on VPN, debugging block).
      Crucial Guardrail: This administrative command is only clickable when the cooldown timer has elapsed and the app is in the Unlocked state. Note: there is no deprovisioning setting; an advanced user that can install the app as DO should be able to uninstall it if they want to.

🔄 Mitigation Matrix: Resolving standard bypasses

Bypass Action Standard Behavior Device Owner Mode Protection Android API Used
System VPN Revocation User toggles off VPN or deletes profile in System Settings. Enforces Always-on VPN. Settings gear and toggle are disabled. setAlwaysOnVpnPackage(admin, packageName, lockdownEnabled = false) & addUserRestriction(admin, UserManager.DISALLOW_CONFIG_VPN)
Force Stopping the App User clicks "Force Stop" in app settings. System grays out/disables the Force Stop button. User cannot terminate background VPN or UI processes. Automatic OS behavior for active DPCs.
App Uninstallation User uninstalls BlockAds. System grays out the Uninstall button in settings and disables launcher drag-to-uninstall actions. setUninstallBlocked(admin, packageName, true)
Settings / Cache Wiping User clicks "Clear Storage" or "Clear Cache" in settings. Disables modification of application settings, preventing data wipes that would reset the cooldown state. Automatic OS behavior for active DPCs.

🛠️ Proposed Architecture & Code Integration

Integrating Device Owner capabilities requires minimal overhead because it utilizes built-in Android system framework classes, but it requires strict lifecycle guarding.

graph TD
    Startup[App Startup / Service Boot] -->|Check Device Owner Status| DPM_Check{isDeviceOwnerApp?}
    DPM_Check -->|Yes| EnforcePolicies[Automatically Apply DPM restrictions]
    DPM_Check -->|No| NormalMode[Run in Standard Sandbox Mode]
    
    subgraph UISettings ["UI Settings (Only Visible if Device Owner)"]
        DO_UIVisible[Render DO Settings section] --> ClearRest[Clear Restrictions Button]
        ClearRest -->|Only in Unlocked State| RunClear[Suspend restrictions]
    end
    
    subgraph SystemRestrictions ["System Restrictions"]
        EnforcePolicies -->|1. Lock VPN| AlwaysOn[Always-on VPN + Lockdown]
        EnforcePolicies -->|2. Lock Package| UninstallBlocked[setUninstallBlocked = true]
        EnforcePolicies -->|3. Disallow Settings| RestrictVPN[DISALLOW_CONFIG_VPN]
        EnforcePolicies -->|4. Close ADB Bypass| RestrictDebug[DISALLOW_DEBUGGING_FEATURES]
    end
Loading

1. Component Registration: AdBlockDeviceAdminReceiver

To receive device management lifecycle events, we must register a DeviceAdminReceiver.

  • Class Definition (AdBlockDeviceAdminReceiver.kt):

    package app.pwhs.blockads.service
    
    import android.app.admin.DeviceAdminReceiver
    import android.content.Context
    import android.content.Intent
    import timber.log.Timber
    
    class AdBlockDeviceAdminReceiver : DeviceAdminReceiver() {
        override fun onEnabled(context: Context, intent: Intent) {
            super.onEnabled(context, intent)
            Timber.i("BlockAds Device Admin enabled.")
        }
    
        override fun onDisabled(context: Context, intent: Intent) {
            super.onDisabled(context, intent)
            Timber.w("BlockAds Device Admin disabled.")
        }
    }
  • Policy Metadata (device_admin_policies.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <device-admin xmlns:android="http://schemas.android.com/apk/res/android">
        <uses-policies>
            <!-- Declares that the admin receiver is used for device-owner enterprise management -->
        </uses-policies>
    </device-admin>
  • Manifest Registration (AndroidManifest.xml):

    <receiver
        android:name=".service.AdBlockDeviceAdminReceiver"
        android:label="@string/app_name"
        android:description="@string/device_admin_description"
        android:permission="android.permission.BIND_DEVICE_ADMIN"
        android:exported="true">
        <meta-data
            android:name="android.app.device_admin"
            android:resource="@xml/device_admin_policies" />
        <intent-filter>
            <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
        </intent-filter>
    </receiver>

2. Policy Enforcer Service: DeviceOwnerManager

We expose a manager class to handle programmatic applying and clearing of policies based on the application state.

class DeviceOwnerManager(
    private val context: Context,
    private val appPrefs: AppPreferences
) {
    private val dpm = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
    private val adminComponent = ComponentName(context, AdBlockDeviceAdminReceiver::class.java)

    /**
     * Checks if the app is currently provisioned as the Device Owner.
     */
    fun isDeviceOwner(): Boolean {
        return dpm.isDeviceOwnerApp(context.packageName)
    }

    /**
     * Applies system restrictions. This is triggered when the app starts.
     */
    fun enforceRestrictions() {
        if (!isDeviceOwner()) return
        
        try {
            // 1. Force always-on VPN connection with lockdown (blocks traffic when disconnected)
            dpm.setAlwaysOnVpnPackage(adminComponent, context.packageName, false)

            // 2. Block user access to VPN configurations
            dpm.addUserRestriction(adminComponent, UserManager.DISALLOW_CONFIG_VPN)

            // 3. Disable USB debugging and Developer options to close the ADB bypass vector
            dpm.addUserRestriction(adminComponent, UserManager.DISALLOW_DEBUGGING_FEATURES)

            Timber.i("Device Owner restrictions successfully applied.")
        } catch (e: Exception) {
            Timber.e(e, "Failed to apply Device Owner restrictions")
        }
    }

    /**
     * Clears restrictions. This can ONLY be called when the cooldown timer has completed 
     * and the app is in the 'Unlocked' state.
     */
    fun clearRestrictions() {
        if (!isDeviceOwner()) return

        try {
            dpm.setAlwaysOnVpnPackage(adminComponent, null, false)
            dpm.clearUserRestriction(adminComponent, UserManager.DISALLOW_CONFIG_VPN)
            dpm.clearUserRestriction(adminComponent, UserManager.DISALLOW_DEBUGGING_FEATURES)

            Timber.i("Device Owner restrictions cleared.")
        } catch (e: Exception) {
            Timber.e(e, "Failed to clear Device Owner restrictions")
        }
    }

}

🔍 Threat Model, Edge Cases & Meticulous Vulnerability Analysis

A state-of-the-art protection system must anticipate malicious self-tampering by a user under the influence of strong impulses. Below is an exhaustive breakdown of potential attack vectors and their corresponding mitigations.

1. The USB Debugging / ADB Bypass

  • Vulnerability: A user with programming experience can connect their phone to a computer and run adb shell pm uninstall app.pwhs.blockads or adb shell pm disable-user app.pwhs.blockads.
  • Mitigation: When DO mode is active, the app applies UserManager.DISALLOW_DEBUGGING_FEATURES. This disables Android Developer Options and shuts down USB Debugging instantly. If the user tries to go to developer options, the screen is grayed out with a message: "This setting is disabled by your administrator."
  • Edge Case: If USB debugging was already active and a debug session is currently running while the policy is toggled on, the current ADB connection might persist until unplugged.
  • Mitigation: The app should prompt the user to unplug the cable before locking down, and verify that dpm.addUserRestriction has fully closed active debug connections.

2. The Safe Mode Escape Hatch

  • Vulnerability: If the user powers off the phone, holds down the physical volume keys during boot, and enters Safe Mode, Android disables all third-party services and applications. In Safe Mode, the BlockAds VPN is inactive, allowing unrestricted internet access.
  • Mitigation:
    • Standard Android APIs do not support disabling Safe Mode. However, on specific OEM enterprise-managed devices (e.g., Samsung Knox, Zebra, Honeywell), DPC apps can disable Safe Mode entirely using OEM SDK extensions.
    • On standard Android, booting into Safe Mode cannot be blocked. However, this action requires a full hardware reboot. Re-starting the device creates a significant cognitive barrier and delay, which aligns with the core goal of the cooldown timer (adding friction). Once the user reboots out of Safe Mode, BlockAds immediately restarts via BootReceiver and re-applies the system restrictions, maintaining the lockdown integrity.

3. Hardware Recovery Partition / Factory Reset

  • Vulnerability: The user triggers a Factory Data Reset (either via system settings or physical recovery buttons) to wipe the device and remove the Device Owner profile.
  • Design Decision (No Restriction):
    • In alignment with user control guidelines, the app does not apply UserManager.DISALLOW_FACTORY_RESET. Allowing factory resets is an essential user safety feature and ensures that the user maintains ultimate hardware ownership.
    • Furthermore, a factory reset requires the user to wipe all local personal data (photos, messages, accounts). This extreme penalty creates a massive barrier (friction), which successfully deters impulsive bypassing while preserving the user's right to reset their device in an emergency.

4. The De-provisioning Policy Loophole

  • Vulnerability: DPC applications cannot be removed by the user in settings, but they can be de-provisioned programmatically inside the app using dpm.clearDeviceOwnerApp(). If the user finds a way to trigger this method during the lockdown period, the protection is neutralized.
  • Mitigation:
    • The app intentionally omits a deprovisioning feature. If an advanced user needs to remove the app, they can uninstall it using standard ADB commands, eliminating the risk of a programmatic UI bypass.
    • The preference logic and database entries for the cooldown duration must reside in a secure, encrypted database space or protected DataStore that is read-only when the lockdown screen is active.

5. Work Profile Limitations

  • Vulnerability: If BlockAds is provisioned as a Profile Owner (PO) of a Work Profile instead of a Device Owner (DO):
    • The restrictions (Always-on VPN, uninstall block) will only apply to applications running inside the Work Profile container.
    • Applications in the Personal Profile will remain unaffected, rendering the VPN block useless for personal apps.
  • Mitigation: The settings UI must explicitly check dpm.isDeviceOwnerApp() and not dpm.isProfileOwnerApp(). It must warn the user that Profile Owner mode is insufficient and that full Device Owner provisioning is mandatory for system-wide protection.

🚀 Device Provisioning & Setup Workflows

Because Android enforces strict security boundaries around Device Owner designation, a standard APK installation cannot request DO privileges. Users must enroll the device using one of the following methods:

Method 1: ADB Command (Post-Install, Account-Free)

Ideal for advanced users who do not want to wipe their device, but requires temporary removal of all accounts:

  1. Remove all Google, email, and social accounts from Settings -> Accounts.
  2. Enable USB Debugging in Developer Options.
  3. Install the BlockAds APK.
  4. Connect the phone to a computer and execute:
    adb shell dpm set-device-owner app.pwhs.blockads/app.pwhs.blockads.service.AdBlockDeviceAdminReceiver
  5. Re-add system accounts. The app is now provisioned as Device Owner.

Method 2: QR Code Provisioning (On Factory Reset)

Best for fresh device installations:

  1. Factory reset the device.
  2. At the initial "Welcome" screen, tap the screen background 6 times in the exact same spot.
  3. This triggers the hidden camera scanner. Scan a QR code containing the following provisioning payload:
    {
      "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME": "app.pwhs.blockads/app.pwhs.blockads.service.AdBlockDeviceAdminReceiver",
      "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION": "https://github.com/TaqiHamoda/blockads-android/releases/download/latest/blockads.apk",
      "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED": true
    }
  4. The system will download the APK, install it, and launch setup with Device Owner mode automatically configured.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt (1)

116-116: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize or remove the decorative icon description.

"Locked" bypasses the localized string resources; use stringResource(...) or contentDescription = null if the title already conveys the state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt` at line 116, The
decorative icon in LockdownScreen still uses a hardcoded contentDescription of
"Locked" instead of localized resources. Update the icon’s contentDescription in
the LockdownScreen composable to either use stringResource(...) with the
existing string resources or set it to null if the nearby title already
communicates the locked state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt`:
- Around line 70-89: Use a cumulative monotonic baseline for cooldown tracking
in LockdownScreen’s cooldown loop. The current drift check updates
lastActiveTime/lastActiveRealtime every tick, which lets repeated small
wall-clock jumps accumulate and still unlock via elapsed; instead, keep the
effect’s initial wall-time and realtime pair as the baseline for
onTimeTamperingDetected checks, and compute the cooldown completion from
monotonic elapsed since that baseline rather than current wall-clock elapsed.
- Around line 45-54: The persisted tamper check in LockdownScreen’s
LaunchedEffect currently skips validation when elapsedRealtime() has reset after
reboot because initialReal is less than lastPersistedRealtime. Update the logic
around initialReal, lastPersistedRealtime, and onTimeTamperingDetected so
rebooted monotonic baselines are treated as untrusted: cancel and restart the
cooldown or reinitialize the persisted baseline before evaluating wall-clock
differences, instead of bypassing the check. Make the fix in the same
tamper-detection block that compares initialWall, lastPersistedTime, and
realDiff.

---

Outside diff comments:
In `@app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt`:
- Line 116: The decorative icon in LockdownScreen still uses a hardcoded
contentDescription of "Locked" instead of localized resources. Update the icon’s
contentDescription in the LockdownScreen composable to either use
stringResource(...) with the existing string resources or set it to null if the
nearby title already communicates the locked state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54a36f89-2127-43d2-bedc-6369af4cbb58

📥 Commits

Reviewing files that changed from the base of the PR and between f99267e and f265cbe.

📒 Files selected for processing (4)
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt
  • app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt
  • app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
  • app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
  • app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt

Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/test/java/app/pwhs/blockads/LockdownDurationValidationTest.kt (1)

27-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the AppPreferences behavior, not only the constant.

These assertions only restate membership in ALLOWED_LOCKDOWN_DURATIONS. They do not verify that setLockdownDuration normalizes an invalid value or that lockdownDuration falls back to DEFAULT_LOCKDOWN_DURATION.

Add DataStore-backed tests for both paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/test/java/app/pwhs/blockads/LockdownDurationValidationTest.kt` around
lines 27 - 44, Replace the membership-only assertions in
testInvalidDurationsRejected and testValidDurationsAccepted with
DataStore-backed tests exercising AppPreferences.setLockdownDuration and
lockdownDuration. Verify invalid inputs are normalized to
DEFAULT_LOCKDOWN_DURATION, and valid inputs are persisted and returned unchanged
through the lockdownDuration property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt`:
- Around line 83-85: Update the LockdownScreen timing state so the computed
totalElapsed value is stored in Compose state and drives the displayed
remainingMs countdown and progress calculation. Replace the wall-clock-based UI
calculations near remainingMs with values derived from totalElapsed, while
preserving the existing monotonic unlock condition.
- Around line 45-51: Update onStartCooldown’s initial persistence path to
atomically write setLastActiveTimestamp and setLastActiveRealtime in a single
DataStore.edit operation, preventing LockdownScreen from observing only one
baseline after interruption. Preserve the existing cooldown-start timestamp
behavior and ensure the countdown starts only after the combined write
completes.

---

Nitpick comments:
In `@app/src/test/java/app/pwhs/blockads/LockdownDurationValidationTest.kt`:
- Around line 27-44: Replace the membership-only assertions in
testInvalidDurationsRejected and testValidDurationsAccepted with
DataStore-backed tests exercising AppPreferences.setLockdownDuration and
lockdownDuration. Verify invalid inputs are normalized to
DEFAULT_LOCKDOWN_DURATION, and valid inputs are persisted and returned unchanged
through the lockdownDuration property.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 009ac540-efef-49c1-bdc1-9363e8662f8b

📥 Commits

Reviewing files that changed from the base of the PR and between f265cbe and 74ea3c8.

📒 Files selected for processing (11)
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt
  • app/src/main/java/app/pwhs/blockads/service/DeviceOwnerManager.kt
  • app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/component/DeviceOwnerSection.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/component/SettingsToggleItem.kt
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/app/pwhs/blockads/LockdownDurationValidationTest.kt
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt

Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
Comment thread app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt`:
- Around line 60-61: Update the Device Owner initialization flow around
deviceOwnerManager.enforceRestrictions() to handle its false result instead of
discarding it. When enforcement fails, either retry using the existing
enforcement lifecycle or update the relevant preference/state and user-facing
status to indicate Device Owner Mode is not enforced; preserve the current path
when enforcement succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 139703ce-26ab-4664-b114-04fbbd79c347

📥 Commits

Reviewing files that changed from the base of the PR and between 74ea3c8 and 26cf5d9.

📒 Files selected for processing (24)
  • app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt
  • app/src/main/java/app/pwhs/blockads/data/datastore/AppPreferences.kt
  • app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
  • app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt
  • app/src/main/res/values-ar/strings.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-in/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-vi/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (22)
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-iw/strings.xml
  • app/src/main/java/app/pwhs/blockads/ui/BlockAdsApp.kt
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-vi/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-ar/strings.xml
  • app/src/main/res/values-th/strings.xml
  • app/src/main/res/values-cs/strings.xml
  • app/src/main/res/values-in/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/java/app/pwhs/blockads/ui/LockdownScreen.kt
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/java/app/pwhs/blockads/ui/settings/SettingsViewModel.kt

Comment thread app/src/main/java/app/pwhs/blockads/BlockAdsApplication.kt Outdated
@TaqiHamoda

TaqiHamoda commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

I have been daily driving the app on my main phone for the past month with zero issues popping up. I have been using both the cool down timer and the device admin restriction every day for the past month, enabling and disabling them and stress testing them in general.

Given that I finally had some free time to get around implementing the enhancements code rabbit suggested, I will install the app with the latest changes and daily drive it for another month. I stress tested the latest changes on an Android Go 14 device with no problems. I will install it on my Pixel 7 with Android 17 installed and see if any issues pop up throughout the next month.

I will write down an update here with my findings in September.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants