Skip to content

Repository files navigation

Candy Browser logo

Candy Browser

A gesture-first Android browser with Material 3 Expressive design, local privacy tools, and a tab system built for visual navigation.

Release Android 14+ Kotlin Jetpack Compose License: MPL 2.0

Buy me a coffee

Candy Browser start page   Candy Browser cover-flow tab overview   Candy Browser Privacy X-Ray

Why Candy?

  • Made for gestures. Switch tabs from the address bar, swipe into the visual overview, and dismiss cards with spring motion and haptic feedback.
  • Private by design. Filtering, history, favorites, profiles, and privacy telemetry stay local.
  • Feels at home on Android. Dynamic color, edge-to-edge content, Predictive Back, Autofill, passkeys, downloads, sharing, printing, and default-browser integration.

Features

Browsing and gestures

  • Floating bottom chrome over edge-to-edge WebView content
  • Pull to refresh, direct URL navigation, QR scanning, local domain completion, and optional provider-backed search suggestions (disabled in private tabs)
  • Google, DuckDuckGo, Bing, Brave, Ecosia, Startpage, and Qwant search
  • Address commands with > for tab, profile, cache, cookie, and navigation actions
  • Background tabs, downloads, sharing, printing, external apps, and assistant summaries

Candy Browser profile commands in the address bar

Tabs, profiles, and journeys

  • Persistent tabs with saved page previews, favicons, pinning, reordering, and automatic cleanup
  • Cover flow, compact grid, and preview-free list layouts
  • Optional per-profile WebView storage isolation where the installed provider supports it
  • Private tabs that keep their session and journey data in memory only
  • Candy Trails: persistent branching navigation graphs with pan, zoom, direct navigation, and forkable paths
  • Site Capsules: profile-bound home-screen shortcuts with configurable navigation boundaries and minimal browser chrome

Creating an isolated Candy Browser profile   Candy Trail with a branching navigation journey

Cover-flow tab overview   Compact grid tab overview   Preview-free list tab overview

Local protection

  • EasyList/EasyPrivacy hosts and domain-scoped cosmetic CSS plus a pinned, safely representable uAssets subset
  • Third-party-cookie blocking and cosmetic cookie-banner hiding
  • Privacy X-Ray: live per-tab block counts, categories, domains, and exceptions
  • Filter Studio: global or profile rules, import/export, and confirmed HTTPS subscriptions
  • Safe Browsing, TLS failure handling, blocked unsafe schemes, and external-scheme allowlisting

Download

Candy Browser requires Android 14 (API 34) or newer.

Download Candy Browser

Production builds check GitHub for updates at startup and offer the signed APK for download. Android still requires you to open the downloaded file and approve installation.

Build from source

Requirements: Android SDK 35 and JDK 17. Point JAVA_HOME to your JDK 17 installation.

./gradlew testDebugUnitTest lintDebug assembleDebug

Debug APK: app/build/outputs/apk/debug/app-debug.apk. It installs as dev.sk2andy.materialbrowser.debug, uses the label Candy Browser Debug, and has a badged launcher icon, so it can stay installed beside the release app.

Signed release builds

Release builds are minified with R8 and require a signing key. Never commit a keystore or its credentials. Configure signing locally with either an ignored project file or environment variables.

For the first release only, create a long-lived key if no release key exists yet. Reuse that same key for every later release:

mkdir -p .signing
keytool -genkeypair \
  -keystore .signing/candy-release.keystore \
  -storetype PKCS12 \
  -alias candy \
  -keyalg RSA \
  -keysize 4096 \
  -validity 10000

For an ignored project file:

cp keystore.properties.example keystore.properties
# Replace every placeholder in keystore.properties, then:
./gradlew assembleLocalRelease

Alternatively, export the same values from ~/.zshrc.shared:

export CANDY_RELEASE_KEYSTORE_PATH=/absolute/path/to/project/.signing/candy-release.keystore
export CANDY_RELEASE_STORE_PASSWORD='replace-me'
export CANDY_RELEASE_KEY_ALIAS='candy'
export CANDY_RELEASE_KEY_PASSWORD='replace-me'

Signed local APK: app/build/outputs/apk/localRelease/app-localRelease.apk

localRelease installs beside the GitHub build as dev.sk2andy.materialbrowser.local and uses a separate launcher icon and the label Candy Browser Local. GitHub update prompts are disabled for this side-by-side build because production APKs cannot update its package. The GitHub workflow continues to use assembleRelease, preserving the production application ID and icon.

GitHub releases

The manual Release Android APK workflow tests the selected source revision, builds and verifies a signed APK, creates a v<version> source tag, and publishes the APK plus its SHA-256 checksum. Add four repository secrets once:

base64 < "$CANDY_RELEASE_KEYSTORE_PATH" | gh secret set CANDY_RELEASE_KEYSTORE_BASE64
printf '%s' "$CANDY_RELEASE_STORE_PASSWORD" | gh secret set CANDY_RELEASE_STORE_PASSWORD
printf '%s' "$CANDY_RELEASE_KEY_ALIAS" | gh secret set CANDY_RELEASE_KEY_ALIAS
printf '%s' "$CANDY_RELEASE_KEY_PASSWORD" | gh secret set CANDY_RELEASE_KEY_PASSWORD

Pin the public certificate fingerprint separately. This prevents an accidentally replaced keystore from publishing an APK that installed copies cannot update to:

keytool -exportcert \
  -keystore "$CANDY_RELEASE_KEYSTORE_PATH" \
  -storepass:env CANDY_RELEASE_STORE_PASSWORD \
  -alias "$CANDY_RELEASE_KEY_ALIAS" \
  | shasum -a 256 \
  | awk '{print $1}' \
  | gh variable set CANDY_RELEASE_CERTIFICATE_SHA256

The workflow uses the release GitHub environment. Configure required reviewers for that environment if releases should require a manual approval after dispatch.

Then dispatch a release from GitHub Actions or with GitHub CLI:

gh workflow run release.yml \
  -f version=0.2 \
  -f prerelease=false

Android versionCode is the monotonically increasing GitHub workflow run number plus the current source-code base of 1, so the first automated release starts at 2. Tags and releases are created only after tests, release build checks, APK signing, certificate pinning, and signature verification succeed. Back up the original keystore and credentials securely: Android updates must always use the same signing key.

Privacy and limitations

Candy uses Android System WebView as its only browser engine and renderer. It does not bundle a Chromium fork, extension runtime, proxy, VPN, or second rendering engine. Filtering stays inside each WebView through local request interception, document-start CSS/DOM rules, and service-worker interception. WebSockets, CNAME cloaking, some redirects, and content inside closed cross-origin or Shadow DOM contexts may get through. Candy pre-registers cosmetic CSS for known GET navigations. Android WebView does not expose cross-origin form POST targets early enough for origin-scoped document-start registration; those navigations receive the existing page-commit fallback and can show a brief cosmetic flash.

Privacy X-Ray includes only blocked requests that WebView can reliably attribute to one tab. Service-worker requests are filtered but excluded from per-tab telemetry when no reliable tab ID is available.

Candy Rules and filter-source details

Candy Rules v1 supports exact host block/allow rules, positive site-to-host pairs, HOSTS entries, and origin-scoped standard CSS selectors. It deliberately rejects JavaScript, regular expressions, redirects, response-header filters, scriptlets, and advanced cosmetic operators rather than approximating them.

Bundled EasyList/EasyPrivacy and uAssets cosmetics are compiled separately from user rules and merged in memory so exceptions work across sources. Only domain-specific standard ##selector rules, their domain exclusions, and matching #@# exceptions are retained. Generic selectors, conditional capability blocks, and procedural operators are skipped. Runtime lookup resolves selectors for the current host before registering a bounded, exact-origin document-start script; invalid browser-specific selectors fail independently through insertRule. Mail, Maps, and Accounts Google hosts intentionally receive no bundled cosmetic CSS.

Bundled defaults may additionally use audited, declarative WebView rules: a site-scoped literal request-path prefix or a known Reject/Remind-later consent control. These rules cannot run imported JavaScript, never click Accept, and never intercept a main-frame navigation. An exact bundled path rule may override the general same-party escape only for its audited page-host, request-host, and literal non-root path prefix.

Imports are bounded and validated atomically. HTTPS subscriptions update only after a user-requested fetch, show a diff, and require confirmation. Private-profile imports remain in memory.

EasyList, EasyPrivacy, and EasyList Cookie data are distributed under CC BY-SA 3.0 or later. The uAssets-derived network and cosmetic subsets are generated from one pinned revision of the official uBlock Origin Ads source and distributed under GPL-3.0. Exact sources, revisions, transformations, and notices ship in app/src/main/assets/; maintenance entry points live in scripts/update_*.sh.

Languages

English, German, French, Portuguese, and Spanish.

Licensing

Candy Browser source code is available under the Mozilla Public License 2.0. Third-party components and filter data retain their own licenses; bundled notices are available in app/src/main/assets/third_party_notices.txt and inside the app.

About

Gesture-first Android browser with Material 3 Expressive design and local privacy tools

Topics

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages