๐ Build Your Own Private Web Browser With Built-In Ad Blocking
๐๐จ๐ข๐ง ๐๐จ๐ฆ๐ฆ๐ฎ๐ง๐ข๐ญ๐ฒ - ๐๐๐๐๐ ๐๐๐๐Want to make your own lightweight Android browser instead of simply using an existing browser?
In this A-to-Z guide, we'll build a private Android browser using Kotlin + WebView, then add a simple domain-based ad blocker that intercepts known advertising/tracking requests before they load.
Android's WebView is designed for embedding web content inside Android apps, while shouldInterceptRequest() can be used to inspect resource requests.
What we'll build
- Custom browser UI
- Address/search bar
- Back/forward buttons
- Reload button
- WebView browsing
- HTTPS-only behavior
- Safe Browsing
- Basic ad/tracker blocking
- Block counter
- Clear browsing data
- External handling for unsupported links
Important: this is a learning browser. A simple domain blocklist will not block every advertisement. Modern ad blocking requires a full filter engine supporting rules such as EasyList/uBlock-style filters, cosmetic rules and exceptions. EasyList itself describes rules covering ad scripts, images, CSS ads, video ads, ad servers and more.
1. Create the Android Project
Install Android Studio:
https://developer.android.com/studio
Create:
New Project โ Empty Views Activity
Use:
Name: PrivateBrowser Package: com.example.privatebrowser Language: Kotlin
For the SDK versions, use the current versions offered by your installed Android Studio.
2. Add the WebKit Dependency
Open:
app/build.gradle.kts
Inside dependencies, add:
implementation("androidx.webkit:webkit:1.15.0")
Then press:
Sync Now
Android's WebKit library provides compatibility APIs for modern WebView functionality.
3. Add Internet Permission
Open:
app/src/main/AndroidManifest.xml
Add:
<uses-permission android:name="android.permission.INTERNET" />
Your manifest will look similar to:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:label="Private Browser"
android:theme="@style/Theme.PrivateBrowser">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
4. Create the Browser Layout
Open:
app/src/main/res/layout/activity_main.xml
Replace its contents with:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="horizontal">
<EditText
android:id="@+id/addressBar"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="Search or enter website"
android:singleLine="true"
android:inputType="textUri"
android:imeOptions="actionGo" />
<Button
android:id="@+id/goButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="Go" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="3dp"
android:visibility="gone"
style="?android:attr/progressBarStyleHorizontal" />
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="52dp"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/backButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="โน" />
<Button
android:id="@+id/forwardButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="โบ" />
<Button
android:id="@+id/reloadButton"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="โป" />
</LinearLayout>
</LinearLayout>
Now the basic browser interface is ready.
5. Create the Browser Code
Open:
app/src/main/java/com/example/privatebrowser/MainActivity.kt
Replace it with:
package com.example.privatebrowser
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var addressBar: EditText
private lateinit var progressBar: ProgressBar
private var blockedCount = 0
private val blockedDomains = setOf(
"doubleclick.net",
"googlesyndication.com",
"googleadservices.com",
"adservice.google.com",
"adnxs.com",
"adsrvr.org",
"taboola.com",
"outbrain.com"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
addressBar = findViewById(R.id.addressBar)
progressBar = findViewById(R.id.progressBar)
val goButton: Button = findViewById(R.id.goButton)
val backButton: Button = findViewById(R.id.backButton)
val forwardButton: Button = findViewById(R.id.forwardButton)
val reloadButton: Button = findViewById(R.id.reloadButton)
configureWebView()
goButton.setOnClickListener {
loadFromAddressBar()
}
addressBar.setOnEditorActionListener { _, _, event ->
if (event == null || event.keyCode == KeyEvent.KEYCODE_ENTER) {
loadFromAddressBar()
true
} else {
false
}
}
backButton.setOnClickListener {
if (webView.canGoBack()) {
webView.goBack()
}
}
forwardButton.setOnClickListener {
if (webView.canGoForward()) {
webView.goForward()
}
}
reloadButton.setOnClickListener {
webView.reload()
}
webView.loadUrl("https://www.google.com/")
}
private fun configureWebView() {
val settings = webView.settings
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.loadsImagesAutomatically = true
settings.allowFileAccess = false
settings.allowContentAccess = false
WebView.setWebContentsDebuggingEnabled(false)
if (WebViewFeature.isFeatureSupported(
WebViewFeature.SAFE_BROWSING_ENABLE
)
) {
WebSettingsCompat.setSafeBrowsingEnabled(settings, true)
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url
if (url.scheme != "http" &&
url.scheme != "https"
) {
try {
startActivity(Intent(Intent.ACTION_VIEW, url))
} catch (_: Exception) {
}
return true
}
return false
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val host = request.url.host ?: return null
if (isBlockedDomain(host)) {
blockedCount++
return WebResourceResponse(
"text/plain",
"UTF-8",
204,
"No Content",
emptyMap(),
null
)
}
return super.shouldInterceptRequest(view, request)
}
override fun onPageFinished(
view: WebView,
url: String
) {
addressBar.setText(url)
super.onPageFinished(view, url)
}
}
webView.webChromeClient =
object : android.webkit.WebChromeClient() {
override fun onProgressChanged(
view: WebView,
newProgress: Int
) {
progressBar.progress = newProgress
progressBar.visibility =
if (newProgress < 100)
View.VISIBLE
else
View.GONE
}
}
}
private fun isBlockedDomain(host: String): Boolean {
val normalizedHost = host.lowercase()
return blockedDomains.any { domain ->
normalizedHost == domain ||
normalizedHost.endsWith(".$domain")
}
}
private fun loadFromAddressBar() {
var input = addressBar.text
.toString()
.trim()
if (input.isEmpty()) return
if (!input.startsWith("http://") &&
!input.startsWith("https://")
) {
if (input.contains(" ")) {
input =
"https://www.google.com/search?q=" +
Uri.encode(input)
} else {
input = "https://$input"
}
}
webView.loadUrl(input)
}
fun showBlockedCount() {
Toast.makeText(
this,
"Blocked requests: $blockedCount",
Toast.LENGTH_SHORT
).show()
}
override fun onBackPressed() {
if (webView.canGoBack()) {
webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onDestroy() {
webView.stopLoading()
webView.webChromeClient = null
webView.webViewClient = null
webView.destroy()
super.onDestroy()
}
}
6. How the Ad Blocking Works
The important part is:
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse?
Every time WebView requests a resource, the browser checks its hostname.
For example:
example.com
โ
page loads
โ
ad.example.com
โ
Check blocklist
โ
BLOCK
But:
example.com
โ
images.example.com
โ
Not blocked
โ
LOAD
Android documents shouldInterceptRequest() as a way for WebView request handling to inspect/intercept web resources.
7. Improve the Blocklist
The small list above is only for demonstrating the concept.
Real-world adblock lists are much larger.
EasyList maintains a continuously updated filter subscription, while EasyPrivacy focuses on trackers and privacy-related resources.
Official lists:
https://easylist.to/easylist/easylist.txt https://easylist.to/easylist/easyprivacy.txt
You shouldn't simply paste the entire EasyList into the blockedDomains set.
Why?
Because EasyList contains a much richer filter syntax:
||example.com^ example.com##.advertisement ||ads.example.com/script.js
A production browser therefore needs a proper filter parser/engine rather than a simple domain matcher.
8. Add a Simple Local Blocklist File
A better beginner architecture is to keep your domains outside the Kotlin code.
Create:
app/src/main/assets/blocklist.txt
Example:
doubleclick.net googlesyndication.com googleadservices.com adservice.google.com adnxs.com adsrvr.org taboola.com outbrain.com
Then load these domains when the app starts.
This means you can update the list without rewriting the browser's main logic.
9. Load the Blocklist From Assets
Add:
private val blockedDomains =
mutableSetOf<String>()
Then create:
private fun loadBlocklist() {
assets.open("blocklist.txt")
.bufferedReader()
.useLines { lines ->
lines
.map { it.trim().lowercase() }
.filter {
it.isNotEmpty() &&
!it.startsWith("#")
}
.forEach {
blockedDomains.add(it)
}
}
}
Call it inside onCreate():
loadBlocklist()
Now your browser reads its blocklist from:
assets/blocklist.txt
10. Add a Block Counter
You already have:
private var blockedCount = 0
Every blocked request increments it:
blockedCount++
You could eventually display:
๐ก 127 requests blocked
in the browser toolbar.
This makes your browser feel much more like a real privacy-focused browser.
11. Add Clear Browsing Data
For a privacy-focused browser, add a Settings or menu button.
The basic clearing code is:
webView.clearHistory()
webView.clearCache(true)
android.webkit.CookieManager
.getInstance()
.removeAllCookies(null)
android.webkit.CookieManager
.getInstance()
.flush()
You can expose this through:
Menu โ Privacy โ Clear browsing data
For a real browser, give users separate options for:
โ History โ Cache โ Cookies โ Site data
12. Keep HTTPS as the Default
Your browser should normally load:
https://example.com
rather than:
http://example.com
Don't disable Android's network security protections just to make arbitrary HTTP sites work.
Android provides Network Security Configuration for controlling cleartext traffic and trusted certificates.
For a privacy-focused browser, HTTPS should be the normal path.
13. Keep Safe Browsing Enabled
We enabled:
WebSettingsCompat.setSafeBrowsingEnabled(
settings,
true
)
Safe Browsing support is exposed through AndroidX WebKit APIs.
This is separate from ad blocking.
Think of it as:
Ad blocker
โ
Blocks advertising/tracking resources
Safe Browsing
โ
Helps protect against dangerous websites
They solve different problems.
14. Don't Add a Dangerous JavaScript Bridge
Avoid doing this casually:
webView.addJavascriptInterface(...)
Android specifically warns that JavaScript interfaces can expose Android functionality to web content and recommends extreme care with untrusted pages.
For this browser, you don't need a JavaScript bridge.
15. Don't Allow Arbitrary File Access
We've deliberately configured:
settings.allowFileAccess = false settings.allowContentAccess = false
Android's WebView documentation also warns against insecure file:// configurations and recommends safer mechanisms such as WebViewAssetLoader when local content needs to be loaded.
16. Test Your Browser
Run the app on your Android phone.
Test:
https://www.google.com
Then:
https://wikipedia.org
Then several normal websites.
Check:
Back Forward Reload Search URL navigation HTTPS JavaScript Images Videos
17. Test the Ad Blocker
Open a site containing advertising resources.
Your browser will inspect requests like:
ads.example.com tracking.example.com doubleclick.net
If the hostname matches your blocklist:
REQUEST โ Check domain โ Matched? โ YES โ Return empty response โ Resource doesn't load
Don't expect every advertisement to disappear with this simple version.
Some advertising is served from the same domain as the website itself, while other sites use dynamic URLs or first-party ad delivery.
18. Add a Better Architecture Later
Once the basic browser works, upgrade it to:
PRIVATE BROWSER
โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
โ โ โ
WebView Ad Blocker Privacy
โ โ โ
โ Filter Engine Clear Data
โ โ โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
โ
Browser Interface
Possible upgrades:
โ Tabs โ Bookmarks โ Downloads โ Incognito mode โ Dark mode โ History โ Custom homepage โ Search-engine selector โ Ad-block statistics โ Per-site allowlist โ Per-site blocklist โ Cookie controls โ Desktop mode โ Full-screen video
19. Add an Allowlist
Sometimes a website breaks because an important resource is blocked.
Add:
Settings โ Ad Blocking โ Allowed Websites
Example:
example.com news.example.com
Then your blocker can skip those domains.
This is important because aggressive blocking can break legitimate site functionality.
20. Build the APK
When everything works:
Build โ Generate App Bundle / APK โ APK
For testing:
Build โ Build APK(s)
For publishing:
Build โ Generate Signed App Bundle / APK
Android's official publishing documentation covers signing and preparing apps for distribution.
https://developer.android.com/studio/publish
21. Final Project Structure
Your project can eventually look like:
PrivateBrowser/ โ โโโ app/ โ โโโ src/ โ โโโ main/ โ โโโ java/ โ โ โโโ com.example.privatebrowser/ โ โ โโโ MainActivity.kt โ โ โ โโโ res/ โ โ โโโ layout/ โ โ โโโ activity_main.xml โ โ โ โโโ assets/ โ โ โโโ blocklist.txt โ โ โ โโโ AndroidManifest.xml โ โโโ build.gradle.kts
๐ Final Result
You'll have your own Android browser:
PRIVATE BROWSER
โ
โโโโโโโโโโโโโดโโโโโโโโโโโโ
โ โ
WebView Blocklist
โ โ
โ Ad/Tracker
โ Requests
โ โ
Website โโโโโโโโโโโโโ BLOCK
โ
โ
Clean Web Experience
The important part is that you aren't just wrapping a website in an APK. You're learning how a browser UI, WebView, request interception, filtering and privacy controls fit together.
For a production-quality browser, the next major step would be replacing the small domain list with a proper filter engine capable of parsing established filter-list syntax such as EasyList/EasyPrivacy. EasyList's official repository documents the scope and maintenance of those lists.
๐ Useful Links
Android Studio:
https://developer.android.com/studio
Android WebView:
https://developer.android.com/develop/ui/views/layout/webapps/webview
Android WebView security:
https://developer.android.com/privacy-and-security/security-best-practices
Android WebKit:
https://developer.android.com/reference/androidx/webkit/package-summary
EasyList:
EasyList filter:
https://easylist.to/easylist/easylist.txt
EasyPrivacy:
https://easylist.to/easylist/easyprivacy.txt
EasyList GitHub:
https://github.com/easylist/easylist
Android app publishing: