Pulse is an Android transparent proxy client built on the sing-box libbox core. It creates a TUN virtual interface through Android VpnService, then lets sing-box handle DNS, routing, and proxy egress for device traffic without requiring a separate proxy client.
The application ID is com.andrew.proxyapp. It supports Android 8.0 (API 26) and later, targets SDK 36, and currently produces arm64-v8a APKs only.
libbox.aaris a locally obtained binary core and is intentionally excluded from GitHub. After cloning the repository, complete the “Getlibbox.aar” section below before building.
- Device-wide transparent proxying through
VpnService, TUN, and sing-box. - Rule and global modes, LAN direct access, custom domain/IP rules, routing groups, and local rule sets.
- VLESS, VMess, Hysteria2, TUIC, AnyTLS, Shadowsocks, Trojan, SOCKS5, and HTTP outbounds.
- Manual single-link import plus Base64 URI lists, Clash YAML, and sing-box JSON subscriptions.
- Subscription updates with multiple User-Agents, old-node cleanup, and stable-ID duplicate merging.
- DNS strategy, proxy/direct DNS, per-app include or exclude mode, and fixed nodes for selected apps.
- Runtime node switching, connection list, logs and diagnostics, latency tests, and battery-optimization checks.
- Simplified Chinese, English, Russian, Persian, Azerbaijani, and Arabic, plus light, dark, and system themes.
- A first-run English language-selection page; the language can be changed later in General settings.
- A centralized UI design system for colors, spacing, corner radii, typography, and common controls.
ssr:// links can be recognized as import items, but sing-box 1.13.14 does not support ShadowsocksR. The app reports that the profile is unavailable before starting the tunnel.
Device application traffic
|
v
Android VpnService / TUN (gVisor stack)
|
v
TunnelService -> sing-box libbox
| |
| +-> DNS hijacking, sniffing, rules, per-app routing
v
selector: current profile / fixed profile / direct-out / block-out
|
v
Proxy server or direct destination
By default, applications other than Pulse enter the VPN. In Per-app routing, you can include only selected applications or exclude selected applications. A specific application can also be assigned its own profile.
TunnelService protects proxy outbound sockets with VpnService.protect() so that proxy connections do not re-enter the TUN and create a routing loop.
| Component | Current configuration |
|---|---|
| Language | Kotlin, JVM target 17 |
| Android Gradle Plugin | 8.13.2 |
| Gradle Wrapper | 8.13 |
| Minimum / target SDK | 26 / 36 |
| Core | sing-box libbox 1.13.14 |
| Packaged ABI | arm64-v8a |
| UI | Android Views, Material Components, ViewBinding |
| Persistence | SharedPreferences + Gson |
Install:
- Android Studio (latest stable is recommended) and Android SDK Platform 36.
- JDK 17 or later. The Gradle Wrapper downloads the required Gradle version.
- Android Platform Tools for device installation (optional).
Clone the repository and enter its root directory:
git clone <repository-url> Pulse
Set-Location PulseAndroid Studio creates the machine-specific local.properties on first open. It is Git-ignored and must not be committed.
This project depends on the 1.13.14 artifact from singbox-android/libbox. Use the version that matches the source code. Replacing it with an arbitrary newer core may break Java/Kotlin APIs or sing-box configuration fields.
Fixed download URL:
https://jitpack.io/com/github/singbox-android/libbox/1.13.14/libbox-1.13.14.aar
From the project root, create the expected directory and download the expected filename.
Windows PowerShell:
New-Item -ItemType Directory -Force .\app\libs | Out-Null
Invoke-WebRequest -Uri "https://jitpack.io/com/github/singbox-android/libbox/1.13.14/libbox-1.13.14.aar" -OutFile .\app\libs\libbox.aarmacOS / Linux:
mkdir -p app/libs
curl -fL https://jitpack.io/com/github/singbox-android/libbox/1.13.14/libbox-1.13.14.aar -o app/libs/libbox.aarThe final path must be:
Pulse/
└── app/
└── libs/
└── libbox.aar
app/libs/.gitkeep only preserves the empty directory in the repository. libbox.aar and extracted AAR directories are ignored by .gitignore and must never be committed to GitHub.
SHA-256 for the current 1.13.14 artifact:
D8EE7620047E4485199A9CF8DB30E67D1497534117F1774A93C0696068B7B012
Windows PowerShell:
(Get-FileHash .\app\libs\libbox.aar -Algorithm SHA256).Hash
jar tf .\app\libs\libbox.aar | Select-String "jni/arm64-v8a/libbox.so"macOS / Linux:
shasum -a 256 app/libs/libbox.aar
jar tf app/libs/libbox.aar | grep 'jni/arm64-v8a/libbox.so'The archive must contain jni/arm64-v8a/libbox.so. The AAR may contain multiple ABIs, but app/build.gradle.kts locks the final APK to arm64-v8a; do not change it to a multi-ABI build.
Do not download and rename the Android tar.gz executable from a sing-box release, or extract a replacement from an SFA APK. Those files are not the Java/JNI library wrapper required by this project.
On Windows, the repository build helper is recommended. It cleans old outputs and disables the Gradle Build Cache so modified Kotlin code cannot be replaced by stale cached results.
cmd /c "build-arm64.bat"Equivalent manual command:
cmd /c ".\gradlew clean assembleDebug --no-daemon --no-build-cache"Debug APK output:
app/build/outputs/apk/debug/app-debug.apk
Enable USB or wireless debugging on an Android device, then run:
adb install -r -t app\build\outputs\apk\debug\app-debug.apk
adb shell am start -n com.andrew.proxyapp/.MainActivityIf adb is not on PATH, replace it with the full path to Android SDK platform-tools/adb.
- On the first launch, use the English language-selection page to choose a preferred language and tap Continue.
- Open Profiles to add a single proxy profile, or open Subscription manager to add a subscription URL.
- Select an available profile and configure rule/global mode, DNS, and per-app routing as needed.
- Tap the connection button and approve the system VPN permission dialog.
- Review runtime state, traffic, logs, and categorized errors in Connections and Logs & diagnostics.
Example URI for format illustration only; it contains no usable credentials:
hysteria2://password@example.com:443?sni=example.com&alpn=h3#Example
Do not put real subscription URLs, node passwords, private keys, or certificate-skip settings in source code, README files, issues, or logs.
Release signing is local configuration and is not distributed with this repository. To build a signed Release, create the ignored keystore.properties in the project root:
storeFile=app/release-key.jks
storePassword=<keystore-password>
keyAlias=<key-alias>
keyPassword=<key-password>Place the keystore at app/release-key.jks, then run:
cmd /c ".\gradlew clean assembleRelease --no-daemon --no-build-cache"Release APK output:
app/build/outputs/apk/release/app-release.apk
For public releases, use a dedicated keystore and CI Secrets. Do not reuse local development credentials.
The project maintainer may use local publish-release.bat / publish-release.ps1 helpers. These scripts are local release tools, are Git-ignored, and are not distributed in the public repository. The current local scripts:
- Check the worktree, version,
libbox.aar, and local signing files. - Run unit tests, full Lint, and a signed Release build with
clean testDebugUnitTest lintDebug assembleRelease --no-build-cache. - Verify the APK signature and confirm that only
arm64-v8aandlibbox.soare packaged. - Push the current branch to
origin. - Create the matching GitHub Release and upload the APK as an asset.
The first use requires GitHub CLI and authentication:
winget install --id GitHub.cli
gh auth loginChoose GitHub.com, SSH or HTTPS, and complete the prompts. Check the session with:
gh auth statusCommit the source and documentation changes before publishing so the worktree is clean. The local helper reads versionName from app/build.gradle.kts and creates a matching v<version> tag. For version 1.1.0:
cmd /c "publish-release.bat"You can also pass the same version explicitly or create a Draft Release for review:
cmd /c "publish-release.bat 1.1.0"
powershell -NoProfile -ExecutionPolicy Bypass -File .\publish-release.ps1 -DraftThe following files must be prepared locally and are never uploaded:
app/libs/libbox.aarapp/release-key.jkskeystore.properties
After publishing, the GitHub Release page contains the APK download and SHA-256. The helper stops without overwriting an existing Release if the tag already exists, the version does not match Gradle, signature validation fails, or the worktree is dirty.
GitHub Actions does not run on ordinary pushes or pull requests, avoiding duplicate remote builds and notification emails. To verify that a clean Linux environment can build the project from scratch:
- Open the repository's
Actionspage on GitHub. - Select
Android checks. - Click
Run workflowand select the branch to verify. - Click
Run workflowagain to confirm.
The manual job downloads and verifies libbox.aar in a temporary GitHub environment, then runs unit tests, Lint, and a Debug build. It does not access the local Release keystore or publish an APK.
Pulse/
├── app/
│ ├── libs/
│ │ ├── .gitkeep
│ │ └── libbox.aar # downloaded locally, Git-ignored
│ ├── src/main/
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin/com/andrew/proxyapp/
│ │ │ ├── MainActivity.kt
│ │ │ ├── config/
│ │ │ │ └── SingBoxConfigBuilder.kt
│ │ │ ├── data/
│ │ │ │ ├── ConfigStore.kt
│ │ │ │ ├── SubscriptionManager.kt
│ │ │ │ └── UriParser.kt
│ │ │ ├── manager/
│ │ │ │ ├── ProxyManager.kt
│ │ │ │ └── RuntimeController.kt
│ │ │ ├── service/
│ │ │ │ └── TunnelService.kt
│ │ │ └── ui/
│ │ └── res/
│ │ ├── layout/
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ └── styles.xml
│ └── build.gradle.kts
├── gradle/
├── build-arm64.bat
├── README.md
├── README_CN.md
└── AGENTS.md # local maintenance context, Git-ignored
| Module | Responsibility |
|---|---|
TunnelService |
Implements Android VpnService and libbox PlatformInterface, creates TUN, protects outbound sockets, and starts or restores the core. |
SingBoxConfigBuilder |
Generates sing-box 1.13-compatible JSON from profiles and app settings. |
ProxyManager |
Requests VPN permission and coordinates service start, stop, and restart. |
RuntimeController |
Reads runtime state, connections, and logs, and switches selector profiles while running. |
ConfigStore |
Persists profiles, subscriptions, DNS, rules, themes, language, and per-app settings. |
SubscriptionManager |
Downloads subscriptions, tries multiple User-Agents, identifies URI / Clash / sing-box formats, and imports profiles. |
RuleSetManager |
Manages available local rule-set files. |
ui/ |
Main screen, profiles, subscriptions, DNS, routing, per-app routing, diagnostics, and general settings. |
Do not maintain page styling with scattered hard-coded values. Prefer these shared resources when adding or adjusting UI:
| File | Purpose |
|---|---|
app/src/main/res/values/colors.xml |
Semantic color tokens; dark-theme overrides are in values-night/. |
app/src/main/res/values/dimens.xml |
Spacing, corner radii, typography, toolbar, touch targets, and list-row dimensions. |
app/src/main/res/values/styles.xml |
Card, button, input, search, settings-row, switch, text, and dialog themes. |
app/src/main/kotlin/com/andrew/proxyapp/ui/ChoiceSheet.kt |
Shared bottom single-choice component for DNS and general settings. |
Use Widget.Pulse.* styles, @color/... semantic resources, and @dimen/... dimensions on regular pages. Do not duplicate common colors, corner radii, text sizes, or spacing in layouts or Kotlin code.
Before committing:
cmd /c ".\gradlew testDebugUnitTest lintDebug --no-daemon --no-build-cache"For an installable package, run the complete build:
cmd /c ".\gradlew clean assembleDebug --no-daemon --no-build-cache"On at least one real arm64-v8a device, check VPN authorization, start/stop, profile switching, subscription import, DNS editing, per-app routing, light/dark themes, and the rounded dialog background.
The AAR is usually missing or is not at app/libs/libbox.aar. Repeat the “Get libbox.aar” section, then sync Gradle or rebuild.
This project requires --no-build-cache. Prefer build-arm64.bat, which includes clean and this flag.
Start with the diagnostic logs. Common causes include an unavailable profile, missing network permission, invalid DNS configuration, or libbox failing to identify the physical network interface. Do not simplify the return behavior of TunnelService.getInterfaces(), autoDetectInterfaceControl(), or protect(), or you may get no available network interface or an outbound loop.
A provider may return different formats for different User-Agents. The app automatically identifies URI lists, Clash YAML, and sing-box JSON. If it still fails, inspect the response format, subscription reachability, and whether the profile protocol is supported by the current core in the diagnostic log.
Do not only replace the AAR. First check the new PlatformInterface method signatures, sing-box JSON migration notes, and required feature tags. Then update TunnelService, SingBoxConfigBuilder, and tests together.
.gitignoreexcludeslibbox.aar, keystores, signing properties, local SDKs, build outputs, logs, screenshots, and local maintenance documents.AGENTS.mdis local maintenance context and may contain development-environment or signing information; it must not be committed to the public repository.- If a sensitive file was previously committed, changing
.gitignoredoes not remove it from Git history. Rotate the credential immediately and usegit rm --cached <file>to stop future tracking. - When using sing-box / libbox, follow the upstream license and all laws, network policies, and service terms applicable to your location.