DEV Community

Cover image for Yet another productivity app
Chris Johnson
Chris Johnson

Posted on

Yet another productivity app

My day job is network optimization at a regional carrier. I spend my time on DWDM transport, BGP routing, and site consolidation. Kotlin and Jetpack Compose are not in my job description, but I had a focus problem, and none of the existing solutions fixed it the way I needed so I built TapBlok. Sure, there are apps like Brick etc, but I didn't want to pay $60 for an NFC tag inside of a 3D printed case. There were no free open-source options available on Android.

The Problem with App Blockers:
Most focus apps are just too easy to bypass. You tell yourself you just want to take a quick look at Instagram, and there's nothing stopping you; a couple taps and the blocker is off. The barrier is digital, so bypassing it is digital too. I wanted to put my NFC tag somewhere inconvenient. Mine is upstairs in my bedroom, far from my office. To end a session I have to get up and walk upstairs just to tap it. It's not physically hard, it just makes you stop and ask yourself if that's really what you want to do.

How TapBlok Works:
TapBlok monitors your foreground app using Android's UsageStatsManager. Every second, it queries which app is in front. If it's on your block list, it immediately launches a full-screen BlockingActivity as an overlay.

private fun getForegroundApp(): String? {
val usm = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()
val stats = usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 10000, time)
return stats?.maxByOrNull { it.lastTimeUsed }?.packageName
}

To end the session, you scan a physical NFC tag or a printed QR code. Some people tape the QR code to their router or the back of their door. Wherever it's inconvenient enough to make you think twice.

The NFC Side:
Writing the tag uses Android's NDEF API with a custom MIME type so TapBlok tags can't be accidentally triggered by other apps:

val mimeRecord = NdefRecord.createMime(
"application/vnd.com.cj.tapblok",
"work".toByteArray()
)

When the tag is scanned, Android fires an intent to NfcHandlerActivity, which validates the MIME type and toggles the monitoring service. No UI, just a toggle and finish. The QR path uses ZXing with the same toggle logic, for devices without NFC or for people who'd rather print something out.

Technical Choices Worth Mentioning:
*No Device Admin API. I looked at it, but it requires a factory reset to uninstall and is overkill for this use case. The UsageStats polling plus overlay approach is simpler and sufficient.
*Room for the block list. The list of blocked apps is stored in a Room database with a Flow-based query so the UI stays reactive as you add or remove apps.
*Boot persistence. A BootCompletedReceiver checks SharedPreferences on startup and resumes the service if a session was active when the device was last powered off.
*Emergency override. There's a 90-second hold gesture that force-stops the service. The long hold time is intentional; you won't do it by accident, but you can get out if you genuinely need to.

Building It with Claude Code:
I used Claude Code as a collaborator throughout. I'd never touched Jetpack Compose before this project and my Kotlin was surface-level at best. It helped me navigate Compose's state model, caught lifecycle bugs I'd have missed (particularly around DisposableEffect cleanup), and pushed commits directly to GitHub. I still owned the product decisions and the debugging logic but it just got me through the unfamiliar parts faster.

Where It Stands:
The app is on GitHub and currently being submitted to the Play Store.

Source:
https://github.com/cajdata/TapBlok

If you've built something similar, I'd be curious what you ran into. Happy to go deeper on any of the technical choices, or talk through what it's like picking up a new stack as someone who doesn't write code for a living.

Top comments (0)