DEV Community

Cover image for I Spent 80 Hours Building a Production-Ready Ad Blocker for Android (Here's How It Works)
Cahyanudien Aziz Saputra
Cahyanudien Aziz Saputra

Posted on

I Spent 80 Hours Building a Production-Ready Ad Blocker for Android (Here's How It Works)

Last week, I went from WakaTime global rank #135 to #2. Not by building some flashy new framework or jumping on the latest AI hype train. I rebuilt Lens Browser's ad blocking system from scratch because the old one sucked. ๐Ÿ˜ค

Let me show you what 78 hours and 49 minutes of focused development looks like. โšก

๐Ÿ’ฅ The Problem

Lens Browser is a privacy-first mobile browser. Think "open and go"โ€”no accounts, no sync, no tracking. But here's the thing: if you claim to be privacy-first and your ad blocker barely works, you've failed. ๐Ÿ˜ฌ

My old implementation:

  • โŒ ~30-40% blocking rate
  • โŒ False positives breaking legitimate content
  • โŒ Noticeable performance impact
  • โŒ No user control

That's not acceptable. โŒ

๐Ÿ—๏ธ The New Architecture

Three-Layer Defense System ๐Ÿ›ก๏ธ

// Pseudo-code showing the decision flow
fun shouldBlockRequest(url: String): Boolean {
    // Layer 1: Check whitelist
    if (whitelistManager.isWhitelisted(url)) return false

    // Layer 2: Check manual blocks
    if (blockedDomainManager.isBlocked(url)) return true

    // Layer 3: Check against 80k rules
    if (adBlockerEngine.matches(url)) return true

    // Layer 4: JS observer handles DOM-level blocking
    return false
}
Enter fullscreen mode Exit fullscreen mode

Layer 1: Connection Interceptor ๐Ÿ”Œ
Catches requests before they hit the WebView. This is where the magic happens:

override fun shouldInterceptRequest(
    view: WebView?, 
    request: WebResourceRequest?
): WebResourceResponse? {
    val url = request?.url?.toString() ?: return null

    if (shouldBlockRequest(url)) {
        return WebResourceResponse(
            "text/plain", 
            "utf-8", 
            ByteArrayInputStream("".toByteArray())
        )
    }
    return super.shouldInterceptRequest(view, request)
}
Enter fullscreen mode Exit fullscreen mode

Layer 2: JavaScript Sanitizer ๐Ÿงน
Even if something slips through (dynamic content, inline scripts), the JS layer catches it:

const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
            if (isAdElement(node)) {
                window.LensBridge.shouldBlock(node.id, result => {
                    if (result) node.remove();
                });
            }
        });
    });
});

observer.observe(document.body, { 
    childList: true, 
    subtree: true 
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” The 80,000 Rule Problem

You can't iterate through 80,000 rules for every request. That's O(n) per requestโ€”death by a thousand cuts. ๐Ÿ’€

My Solution: โœจ

class AdBlockerEngine {
    private val domainTrie = Trie()
    private val urlPatternMap = HashMap<String, Pattern>()
    private val bloomFilter = BloomFilter(expectedElements = 80000)

    fun matches(url: String): Boolean {
        // Fast negative check
        if (!bloomFilter.mightContain(url)) return false

        // Trie-based domain lookup: O(m) where m = domain length
        val domain = extractDomain(url)
        if (domainTrie.search(domain)) return true

        // Pattern matching for complex rules
        return urlPatternMap.values.any { it.matches(url) }
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Results: ๐Ÿ“Š

  • Average request processing: <5ms โšก
  • Memory footprint: ~30MB for 80k rules ๐Ÿ’พ
  • False positive rate: <1% โœ…

๐Ÿ“Š Real-Time Statistics

Users want to see what's being blocked. I built a live counter: ๐Ÿ‘€

data class BlockedContent(
    val domain: String,
    val url: String,
    val timestamp: Long,
    val type: BlockType
)

object BlockStatistics {
    private val blockedItems = mutableListOf<BlockedContent>()

    fun addBlocked(item: BlockedContent) {
        blockedItems.add(item)
        notifyObservers()
    }

    fun getStats(): Stats {
        return Stats(
            totalBlocked = blockedItems.size,
            uniqueDomains = blockedItems.map { it.domain }.distinct().size,
            blocksByType = blockedItems.groupBy { it.type }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Users can now see:

  • Total blocked count (updates in real-time)
  • List of blocked domains
  • Full URLs that were blocked
  • When they were blocked

๐ŸŽฏ Key Features Shipped

1. Privacy Mode ๐Ÿ”’

Hides User-Agent, screen resolution, timezone, and other fingerprinting vectors:

webView.settings.apply {
    userAgentString = "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36"
    // More obfuscation here
}
Enter fullscreen mode Exit fullscreen mode

2. Trusted Domain Management โœ…

Whitelist system because not everything is an ad:

class WhitelistManager {
    fun addDomain(domain: String) {
        whitelist.add(domain.lowercase())
        persistToStorage()
    }

    fun isWhitelisted(url: String): Boolean {
        return whitelist.any { url.contains(it) }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Security Modal Before Load ๐Ÿšจ

Old way: Load page โ†’ detect threat โ†’ warn user (too late) โŒ
New way: Detect threat โ†’ show modal โ†’ user decides โœ…

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
    if (threatDetector.isDangerous(url)) {
        showSecurityModal(url, threatLevel)
        view?.stopLoading()
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Blocklist Updates ๐Ÿ”„

Users can refresh the 80k rules without reinstalling:

suspend fun refreshBlocklist() {
    val newRules = api.fetchLatestRules()
    adBlockerEngine.updateRules(newRules)
    notifyUser("Blocklist updated!")
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช Testing & Results

SuperAdBlockTest.com Results: ๐Ÿ“ˆ

  • Before: ~35% blocked โŒ
  • After: 65%+ blocked โœ…

Real-World Testing: ๐ŸŒ

  • CNN.com: 47 trackers blocked ๐Ÿšซ
  • Reddit.com: 23 ad domains blocked ๐Ÿšซ
  • Random blog: 15 tracking scripts blocked ๐Ÿšซ

Performance: โšก

  • Page load time: -15% (faster with ads blocked) ๐Ÿš€
  • Memory usage: +8% (acceptable for 80k rules) ๐Ÿ’พ
  • Battery impact: Negligible ๐Ÿ”‹

๐Ÿ’ก Lessons Learned

1. Premature Optimization Is Real ๐ŸŽฏ

I spent 2 days building a "perfect" rule matching algorithm. Scrapped it. The current one is 95% as good and shipped in 4 hours.

2. Test on Real Sites ๐ŸŒ

SuperAdBlockTest is great for benchmarking, but real sites (news, e-commerce, social media) show where your blocker actually fails.

3. Users Want Control ๐ŸŽ›๏ธ

The #1 feature request: "Let me whitelist this site." Privacy users want agency, not just defaults.

4. Performance Matters More Than Features โšก

I could add 50 more features, but if the browser is janky, nobody will use it. Every millisecond counts.

๐Ÿ”ฎ What's Next

This update proves you can build genuinely private software without VC money or user tracking. But I'm not done: ๐Ÿ’ช

  • [ ] Custom blocklist imports
  • [ ] Enhanced fingerprint resistance
  • [ ] Per-site settings
  • [ ] Optional tab management
  • [ ] Sync (optional, encrypted, self-hosted)

๐Ÿ“ฒ Try It

Lens Browser is free and will never have ads or tracking. ๐Ÿ™Œ

๐Ÿ“ฑ Download on Google Play

๐Ÿงช Test it on SuperAdBlockTest.com

๐Ÿ“Š The Stats

  • WakaTime rank: #135 โ†’ #2 globally ๐Ÿ”ฅ
  • Hours coded: 78h 49m โฑ๏ธ
  • Daily average: 11h 15m ๐Ÿ’ช
  • Lines changed: Several thousand ๐Ÿ’ป
  • Coffee consumed: Yes โ˜•

๐Ÿ‘จโ€๐Ÿ’ป For Other Developers

If you're building privacy tools:

  1. Start with threat models ๐ŸŽฏ: What attacks are you preventing?
  2. Measure everything ๐Ÿ“Š: Can't improve what you don't measure
  3. Ship imperfect code ๐Ÿš€: Iterate fast, fail fast
  4. Test with real users ๐Ÿงช: They'll break your assumptions immediately

Building privacy tech is hard. Building usable privacy tech is harder. But it's worth it. ๐Ÿ’ช


What ad blocker do you use? Drop a comment. ๐Ÿ’ฌ๐Ÿ‘‡

P.S. If you're working on similar problems, let's chat. Privacy should be accessible to everyone. ๐Ÿ”’โœจ

Top comments (0)