anubis-android-app-manager
Android app manager with VPN integration that freezes/unfreezes app groups based on VPN connection state using Shizuku's pm disable-user
Best use case
anubis-android-app-manager is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Android app manager with VPN integration that freezes/unfreezes app groups based on VPN connection state using Shizuku's pm disable-user
Teams using anubis-android-app-manager should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/anubis-android-app-manager/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How anubis-android-app-manager Compares
| Feature / Agent | anubis-android-app-manager | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Android app manager with VPN integration that freezes/unfreezes app groups based on VPN connection state using Shizuku's pm disable-user
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Anubis Android App Manager
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
Anubis is an Android app manager that uses Shizuku to completely disable (`pm disable-user`) app groups based on VPN connection state. Unlike sandboxing solutions (Island, Shelter), disabled apps cannot run code, access network interfaces, or detect VPN presence at all.
## Core Concepts
| Group Policy | Behavior |
|---|---|
| **Local** | Frozen when VPN is active; runs without VPN |
| **VPN Only** | Frozen when VPN is inactive; runs through VPN |
| **Launch with VPN** | Never frozen; launching triggers VPN activation |
## Requirements
- Android 10+ (API 29)
- [Shizuku](https://shizuku.rikka.app/) installed and running
- At least one VPN client installed
## Setup
1. Install and start Shizuku (via ADB or Wireless Debugging):
```bash
adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/start.sh
```
2. Install Anubis APK
3. Grant Shizuku permission when prompted
4. Grant VPN permission when prompted (needed for dummy VPN disconnect)
5. Go to **Apps** tab → assign apps to groups
6. Select VPN client in **Settings**
7. Toggle stealth mode on **Home** screen
## Building
```bash
git clone https://github.com/sogonov/anubis
cd anubis
# Debug build
./gradlew assembleDebug
# Release build (requires signing config)
./gradlew assembleRelease
```
Create `signing.properties` in project root for release builds:
```properties
storeFile=release.keystore
storePassword=${KEYSTORE_PASSWORD}
keyAlias=${KEY_ALIAS}
keyPassword=${KEY_PASSWORD}
```
## Tech Stack
- Kotlin + Jetpack Compose (Material 3)
- Shizuku API 13.1.5 (AIDL UserService pattern)
- Room database with TypeConverters for app groups
- `ConnectivityManager` NetworkCallback for VPN state monitoring
- `ShortcutManager` for pinned shortcuts
## Project Structure
```
app/src/main/java/
├── data/
│ ├── AppGroup.kt # Room entity: group name, policy, package list
│ ├── AppGroupDao.kt # DAO: CRUD for app groups
│ └── AppDatabase.kt # Room database setup
├── service/
│ ├── ShizukuService.kt # AIDL UserService: pm/am shell commands
│ ├── VpnStateMonitor.kt # ConnectivityManager NetworkCallback
│ └── FreezeManager.kt # Orchestrates freeze/unfreeze logic
├── vpn/
│ ├── VpnClient.kt # Enum: SEPARATE, TOGGLE, MANUAL
│ └── VpnClientRegistry.kt # Known clients + custom client support
└── ui/
├── HomeScreen.kt # Launcher grid, VPN toggle
├── AppsScreen.kt # Group assignment UI
└── SettingsScreen.kt # VPN client selection
```
## Room Database: App Groups
```kotlin
@Entity(tableName = "app_groups")
data class AppGroup(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
val policy: GroupPolicy,
val packages: List<String> // stored via TypeConverter
)
enum class GroupPolicy {
LOCAL, // frozen when VPN active
VPN_ONLY, // frozen when VPN inactive
LAUNCH_WITH_VPN // never frozen, VPN triggered on launch
}
```
```kotlin
@Dao
interface AppGroupDao {
@Query("SELECT * FROM app_groups")
fun getAllGroups(): Flow<List<AppGroup>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGroup(group: AppGroup)
@Delete
suspend fun deleteGroup(group: AppGroup)
@Update
suspend fun updateGroup(group: AppGroup)
}
```
## Shizuku Integration (AIDL UserService Pattern)
Define the AIDL interface:
```kotlin
// IShizukuService.aidl
interface IShizukuService {
String executeCommand(String command);
void destroy();
}
```
Implement the UserService:
```kotlin
class ShizukuUserService : IShizukuService.Stub() {
override fun executeCommand(command: String): String {
return try {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
process.inputStream.bufferedReader().readText()
} catch (e: Exception) {
"ERROR: ${e.message}"
}
}
override fun destroy() {
exitProcess(0)
}
}
```
Bind to the UserService:
```kotlin
class FreezeManager(private val context: Context) {
private var service: IShizukuService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
service = IShizukuService.Stub.asInterface(binder)
}
override fun onServiceDisconnected(name: ComponentName) {
service = null
}
}
private val userServiceArgs = Shizuku.UserServiceArgs(
ComponentName(context, ShizukuUserService::class.java)
).processNameSuffix("service").debuggable(false).version(1)
fun bindService() {
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
Shizuku.bindUserService(userServiceArgs, serviceConnection)
}
}
fun unbindService() {
Shizuku.unbindUserService(userServiceArgs, serviceConnection, true)
}
suspend fun freezePackage(packageName: String) {
service?.executeCommand("pm disable-user --user 0 $packageName")
}
suspend fun unfreezePackage(packageName: String) {
service?.executeCommand("pm enable --user 0 $packageName")
}
}
```
## VPN State Monitoring
```kotlin
class VpnStateMonitor(private val context: Context) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val _isVpnActive = MutableStateFlow(false)
val isVpnActive: StateFlow<Boolean> = _isVpnActive.asStateFlow()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true) {
_isVpnActive.value = true
}
}
override fun onLost(network: Network) {
// Re-check if any VPN network remains
val hasVpn = connectivityManager.allNetworks.any { net ->
connectivityManager.getNetworkCapabilities(net)
?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
}
_isVpnActive.value = hasVpn
}
}
fun startMonitoring() {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.build()
connectivityManager.registerNetworkCallback(request, networkCallback)
}
fun stopMonitoring() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
```
## VPN Client Control
```kotlin
enum class VpnControlMethod { SEPARATE, TOGGLE, MANUAL }
data class VpnClientConfig(
val packageName: String,
val method: VpnControlMethod,
val startAction: String? = null,
val stopAction: String? = null,
val toggleAction: String? = null,
val receiverClass: String? = null
)
val KNOWN_VPN_CLIENTS = mapOf(
"com.v2ray.ang" to VpnClientConfig(
packageName = "com.v2ray.ang",
method = VpnControlMethod.TOGGLE,
toggleAction = "com.v2ray.ang.action.widget.click",
receiverClass = "com.v2ray.ang.receiver.WidgetProvider"
),
"moe.nb4a" to VpnClientConfig(
packageName = "moe.nb4a",
method = VpnControlMethod.SEPARATE,
startAction = "moe.nb4a.ui.QuickEnable",
stopAction = "moe.nb4a.ui.QuickDisable"
),
"com.happproxy" to VpnClientConfig(
packageName = "com.happproxy",
method = VpnControlMethod.TOGGLE,
toggleAction = "com.happproxy.action.widget.click",
receiverClass = "com.happproxy.receiver.WidgetProvider"
)
)
```
Start/stop VPN via shell:
```kotlin
suspend fun startVpn(config: VpnClientConfig) {
when (config.method) {
VpnControlMethod.TOGGLE, VpnControlMethod.SEPARATE -> {
val action = config.startAction ?: config.toggleAction!!
if (config.receiverClass != null) {
// Broadcast to widget receiver
service?.executeCommand(
"am broadcast -a $action -n ${config.packageName}/${config.receiverClass}"
)
} else {
// Start exported activity (NekoBox SEPARATE style)
service?.executeCommand(
"am start -n ${config.packageName}/$action"
)
}
}
VpnControlMethod.MANUAL -> {
// Open app for user to connect manually
val intent = context.packageManager
.getLaunchIntentForPackage(config.packageName)
context.startActivity(intent)
}
}
}
suspend fun stopVpn(config: VpnClientConfig) {
// Step 1: API stop (SEPARATE clients only)
if (config.method == VpnControlMethod.SEPARATE && config.stopAction != null) {
service?.executeCommand("am start -n ${config.packageName}/${config.stopAction}")
delay(1000)
}
// Step 2: Force-stop as fallback
service?.executeCommand("am force-stop ${config.packageName}")
}
```
## Detecting Active VPN Client
```kotlin
suspend fun detectVpnOwnerPackage(): String? {
val output = service?.executeCommand("dumpsys connectivity") ?: return null
// Find VPN network owner UID
val vpnLine = output.lines().firstOrNull { it.contains("type: VPN[") }
?: return null
val uidMatch = Regex("ownerUid=(\\d+)").find(output) ?: return null
val uid = uidMatch.groupValues[1]
// Resolve UID to package name
val pmOutput = service?.executeCommand("pm list packages --uid $uid") ?: return null
return pmOutput.substringAfter("package:").substringBefore(" ").trim()
.takeIf { it.isNotEmpty() }
}
```
## Adding a Custom VPN Client
Use APK analysis to discover broadcast actions:
1. Open APK in [jadx](https://github.com/skylot/jadx)
2. **Resources** → find `app_widget_name` in `strings.xml`
3. **Manifest** → find `<receiver>` with `android.appwidget.provider` metadata
4. **Receiver code** → find `setAction("...")` calls
5. Verify toggle pattern: `isRunning ? stop : start`
All v2ray/xray forks share the same pattern:
```kotlin
// Discovery template for v2ray forks:
val packageName = "com.example.vpnclient"
val toggleAction = "$packageName.action.widget.click"
val receiverClass = "$packageName.receiver.WidgetProvider"
// Verify manually via ADB:
// adb shell am broadcast -a $toggleAction -n $packageName/$receiverClass
```
Register custom client in Settings UI:
```kotlin
// SettingsViewModel.kt
fun setCustomVpnClient(packageName: String) {
val config = VpnClientConfig(
packageName = packageName,
method = VpnControlMethod.MANUAL // upgrade to TOGGLE if action known
)
preferences.edit().putString("custom_vpn_client", packageName).apply()
}
```
## Pinned Shortcuts
```kotlin
fun createAppShortcut(
context: Context,
packageName: String,
label: String,
icon: Icon
) {
val shortcutManager = context.getSystemService(ShortcutManager::class.java)
if (shortcutManager.isRequestPinShortcutSupported) {
val intent = Intent(context, LaunchActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra("package_name", packageName)
}
val shortcut = ShortcutInfo.Builder(context, "shortcut_$packageName")
.setShortLabel(label)
.setIcon(icon)
.setIntent(intent)
.build()
shortcutManager.requestPinShortcut(shortcut, null)
}
}
```
## Compose UI: App Group Management
```kotlin
@Composable
fun AppGroupCard(
group: AppGroup,
onPolicyChange: (GroupPolicy) -> Unit,
onAddApp: () -> Unit,
onRemoveApp: (String) -> Unit
) {
Card(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(group.name, style = MaterialTheme.typography.titleMedium)
// Policy selector
Row {
GroupPolicy.entries.forEach { policy ->
FilterChip(
selected = group.policy == policy,
onClick = { onPolicyChange(policy) },
label = { Text(policy.name) },
modifier = Modifier.padding(end = 4.dp)
)
}
}
// App list with frozen state indicators
group.packages.forEach { pkg ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(pkg, modifier = Modifier.weight(1f))
IconButton(onClick = { onRemoveApp(pkg) }) {
Icon(Icons.Default.Remove, "Remove")
}
}
}
TextButton(onClick = onAddApp) {
Icon(Icons.Default.Add, "Add app")
Text("Add App")
}
}
}
}
```
## Freeze Orchestration on VPN State Change
```kotlin
class AppOrchestrator(
private val freezeManager: FreezeManager,
private val groupDao: AppGroupDao,
private val vpnMonitor: VpnStateMonitor
) {
suspend fun onVpnStateChanged(isVpnActive: Boolean) {
val groups = groupDao.getAllGroups().first()
groups.forEach { group ->
when (group.policy) {
GroupPolicy.LOCAL -> {
if (isVpnActive) freezeGroup(group) else unfreezeGroup(group)
}
GroupPolicy.VPN_ONLY -> {
if (!isVpnActive) freezeGroup(group) else unfreezeGroup(group)
}
GroupPolicy.LAUNCH_WITH_VPN -> {
// Never auto-freeze; handled at launch time
}
}
}
}
private suspend fun freezeGroup(group: AppGroup) {
group.packages.forEach { pkg ->
freezeManager.freezePackage(pkg)
}
}
private suspend fun unfreezeGroup(group: AppGroup) {
// Safety: never unfreeze while VPN is still transitioning
if (!vpnMonitor.isVpnActive.value || group.policy == GroupPolicy.LOCAL) {
group.packages.forEach { pkg ->
freezeManager.unfreezePackage(pkg)
}
}
}
}
```
## Troubleshooting
### Shizuku Permission Denied
```kotlin
// Check and request Shizuku permission
if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) {
if (Shizuku.shouldShowRequestPermissionRationale()) {
// Show rationale dialog
} else {
Shizuku.requestPermission(REQUEST_CODE_SHIZUKU)
}
}
```
### App Not Freezing
```bash
# Verify pm disable-user works manually
adb shell pm disable-user --user 0 com.example.app
# Check current state
adb shell pm list packages -d # lists disabled packages
# Re-enable manually if stuck
adb shell pm enable --user 0 com.example.app
```
### VPN Detection Failing
```bash
# Debug dumpsys output
adb shell dumpsys connectivity | grep -A5 "type: VPN"
# Check UID resolution
adb shell pm list packages --uid 1234
```
### Custom VPN Client Toggle Not Working
```bash
# Test broadcast manually
adb shell am broadcast -a com.example.vpn.action.widget.click \
-n com.example.vpn/.receiver.WidgetProvider
# Expected: result=0 (RESULT_OK) or check logcat for errors
adb logcat | grep -i "widgetprovider\|broadcast"
```
### Apps Remain Frozen After VPN Disconnect
- Anubis never unfreezes apps while VPN is still active
- Check VPN is fully disconnected: `adb shell dumpsys connectivity | grep VPN`
- Manually unfreeze via long-press on app icon → "Unfreeze"
## Gradle Dependencies
```kotlin
// build.gradle.kts
dependencies {
// Shizuku
implementation("dev.rikka.shizuku:api:13.1.5")
implementation("dev.rikka.shizuku:provider:13.1.5")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Compose
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
// Coroutines + ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
```Related Skills
quip-node-manager
Desktop GUI and CLI app for running and monitoring Quip Network nodes, built with Tauri v2 and Rust
pokeclaw-android-ai-agent
PokeClaw (PocketClaw) — on-device Android AI phone agent using Gemma 4 via LiteRT-LM with tool calling, accessibility automation, and optional cloud models.
lsfg-android-frame-generation
Skill for building, configuring, and extending LSFG-Android — a Vulkan frame-generation overlay for Android using lsfg-vk via MediaProjection capture.
aube-package-manager
Expert guidance for using aube, a fast Rust-based Node.js package manager compatible with pnpm, npm, yarn, and bun lockfiles.
antigravity-manager
AI coding agent skill for Antigravity Manager — a Tauri v2 + Rust desktop app and Docker service that manages multiple Google/Anthropic accounts and proxies them as standard OpenAI/Anthropic/Gemini API endpoints with intelligent account rotation.
```markdown
---
zeroboot-vm-sandbox
Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot
yourvpndead-vpn-detection
Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root
xata-postgres-platform
Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment
x-mentor-skill-nuwa
AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.
wx-favorites-report
End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.
wterm-web-terminal
Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings