anubis-android-app-manager

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Anubis Android App Manager

Anubis Android应用管理器

Skill by 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.
ara.so开发的技能——属于Daily 2026 Skills合集。
Anubis是一款Android应用管理器,它利用Shizuku通过
pm disable-user
命令,根据VPN连接状态完全禁用应用组。与沙箱解决方案(Island、Shelter)不同,被禁用的应用无法运行代码、访问网络接口,甚至无法检测到VPN的存在。

Core Concepts

核心概念

Group PolicyBehavior
LocalFrozen when VPN is active; runs without VPN
VPN OnlyFrozen when VPN is inactive; runs through VPN
Launch with VPNNever frozen; launching triggers VPN activation
组策略行为
本地组VPN激活时冻结;无VPN时运行
仅VPN组VPN未激活时冻结;通过VPN运行
随VPN启动组永不冻结;启动时触发VPN激活

Requirements

要求

  • Android 10+ (API 29)
  • Shizuku installed and running
  • At least one VPN client installed
  • Android 10+(API 29)
  • Shizuku已安装并运行
  • 至少安装一个VPN客户端

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
  1. 安装并启动Shizuku(通过ADB或无线调试):
    bash
    adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/start.sh
  2. 安装Anubis APK
  3. 出现提示时授予Shizuku权限
  4. 出现提示时授予VPN权限(用于虚拟VPN断开操作)
  5. 进入应用标签页 → 将应用分配到对应组
  6. 设置中选择VPN客户端
  7. 首页开启隐身模式

Building

构建步骤

bash
git clone https://github.com/sogonov/anubis
cd anubis
bash
git clone https://github.com/sogonov/anubis
cd anubis

Debug build

调试构建

./gradlew assembleDebug
./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}
./gradlew assembleRelease

在项目根目录创建`signing.properties`用于发布构建:
```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
  • Kotlin + Jetpack Compose(Material 3)
  • Shizuku API 13.1.5(AIDL UserService模式)
  • 带TypeConverters的Room数据库用于存储应用组
  • ConnectivityManager
    NetworkCallback用于VPN状态监控
  • ShortcutManager
    用于固定快捷方式

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
app/src/main/java/
├── data/
│   ├── AppGroup.kt          # Room实体:组名称、策略、包列表
│   ├── AppGroupDao.kt       # DAO:应用组的增删改查操作
│   └── AppDatabase.kt       # Room数据库配置
├── service/
│   ├── ShizukuService.kt    # AIDL UserService:执行pm/am shell命令
│   ├── VpnStateMonitor.kt   # ConnectivityManager NetworkCallback实现
│   └── FreezeManager.kt     # 协调冻结/解冻逻辑
├── vpn/
│   ├── VpnClient.kt         # 枚举类型:SEPARATE、TOGGLE、MANUAL
│   └── VpnClientRegistry.kt # 已知客户端+自定义客户端支持
└── ui/
    ├── HomeScreen.kt         # 启动器网格、VPN开关
    ├── AppsScreen.kt         # 应用组分配界面
    └── SettingsScreen.kt     # VPN客户端选择界面

Room Database: App Groups

Room数据库:应用组

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)
}
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> // 通过TypeConverter存储
)

enum class GroupPolicy {
    LOCAL,        // VPN激活时冻结
    VPN_ONLY,     // VPN未激活时冻结
    LAUNCH_WITH_VPN // 永不冻结,启动时触发VPN连接
}
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)

Shizuku集成(AIDL UserService模式)

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")
    }
}
定义AIDL接口:
kotlin
// IShizukuService.aidl
interface IShizukuService {
    String executeCommand(String command);
    void destroy();
}
实现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)
    }
}
绑定到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

VPN状态监控

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)
    }
}
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) {
            // 重新检查是否还有VPN网络存在
            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

VPN客户端控制

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}")
}
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"
    )
)
通过shell启动/停止VPN:
kotlin
suspend fun startVpn(config: VpnClientConfig) {
    when (config.method) {
        VpnControlMethod.TOGGLE, VpnControlMethod.SEPARATE -> {
            val action = config.startAction ?: config.toggleAction!!
            if (config.receiverClass != null) {
                // 向小部件接收器发送广播
                service?.executeCommand(
                    "am broadcast -a $action -n ${config.packageName}/${config.receiverClass}"
                )
            } else {
                // 启动导出的Activity(NekoBox SEPARATE模式)
                service?.executeCommand(
                    "am start -n ${config.packageName}/$action"
                )
            }
        }
        VpnControlMethod.MANUAL -> {
            // 打开应用让用户手动连接
            val intent = context.packageManager
                .getLaunchIntentForPackage(config.packageName)
            context.startActivity(intent)
        }
    }
}

suspend fun stopVpn(config: VpnClientConfig) {
    // 步骤1:API停止(仅SEPARATE客户端)
    if (config.method == VpnControlMethod.SEPARATE && config.stopAction != null) {
        service?.executeCommand("am start -n ${config.packageName}/${config.stopAction}")
        delay(1000)
    }
    // 步骤2:强制停止作为备用方案
    service?.executeCommand("am force-stop ${config.packageName}")
}

Detecting Active VPN Client

检测活跃VPN客户端

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() }
}
kotlin
suspend fun detectVpnOwnerPackage(): String? {
    val output = service?.executeCommand("dumpsys connectivity") ?: return null

    // 查找VPN网络所有者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]

    // 将UID解析为包名
    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

添加自定义VPN客户端

Use APK analysis to discover broadcast actions:
  1. Open APK in 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()
}
通过APK分析发现广播动作:
  1. jadx中打开APK
  2. 资源 → 在
    strings.xml
    中找到
    app_widget_name
  3. 清单 → 找到带有
    android.appwidget.provider
    元数据的
    <receiver>
  4. 接收器代码 → 找到
    setAction("...")
    调用
  5. 验证切换模式:
    isRunning ? stop : start
所有v2ray/xray分支共享相同模式:
kotlin
// v2ray分支的发现模板:
val packageName = "com.example.vpnclient"
val toggleAction = "$packageName.action.widget.click"
val receiverClass = "$packageName.receiver.WidgetProvider"

// 通过ADB手动验证:
// adb shell am broadcast -a $toggleAction -n $packageName/$receiverClass
在设置界面注册自定义客户端:
kotlin
// SettingsViewModel.kt
fun setCustomVpnClient(packageName: String) {
    val config = VpnClientConfig(
        packageName = packageName,
        method = VpnControlMethod.MANUAL // 如果已知动作可升级为TOGGLE
    )
    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)
    }
}
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

Compose界面:应用组管理

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")
            }
        }
    }
}
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)

            // 策略选择器
            Row {
                GroupPolicy.entries.forEach { policy ->
                    FilterChip(
                        selected = group.policy == policy,
                        onClick = { onPolicyChange(policy) },
                        label = { Text(policy.name) },
                        modifier = Modifier.padding(end = 4.dp)
                    )
                }
            }

            // 带有冻结状态指示器的应用列表
            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, "移除")
                    }
                }
            }
            TextButton(onClick = onAddApp) {
                Icon(Icons.Default.Add, "添加应用")
                Text("添加应用")
            }
        }
    }
}

Freeze Orchestration on VPN State Change

VPN状态变化时的冻结协调

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)
            }
        }
    }
}
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 -> {
                    // 永不自动冻结;在启动时处理
                }
            }
        }
    }

    private suspend fun freezeGroup(group: AppGroup) {
        group.packages.forEach { pkg ->
            freezeManager.freezePackage(pkg)
        }
    }

    private suspend fun unfreezeGroup(group: AppGroup) {
        // 安全机制:VPN仍在切换时永不解冻
        if (!vpnMonitor.isVpnActive.value || group.policy == GroupPolicy.LOCAL) {
            group.packages.forEach { pkg ->
                freezeManager.unfreezePackage(pkg)
            }
        }
    }
}

Troubleshooting

故障排除

Shizuku Permission Denied

Shizuku权限被拒绝

kotlin
// Check and request Shizuku permission
if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) {
    if (Shizuku.shouldShowRequestPermissionRationale()) {
        // Show rationale dialog
    } else {
        Shizuku.requestPermission(REQUEST_CODE_SHIZUKU)
    }
}
kotlin
// 检查并请求Shizuku权限
if (Shizuku.checkSelfPermission() != PackageManager.PERMISSION_GRANTED) {
    if (Shizuku.shouldShowRequestPermissionRationale()) {
        // 显示权限说明对话框
    } else {
        Shizuku.requestPermission(REQUEST_CODE_SHIZUKU)
    }
}

App Not Freezing

应用未被冻结

bash
undefined
bash
// 手动验证pm disable-user是否有效
adb shell pm disable-user --user 0 com.example.app
// 检查当前状态
adb shell pm list packages -d  # 列出已禁用的应用
// 如果卡住,手动重新启用
adb shell pm enable --user 0 com.example.app

Verify pm disable-user works manually

VPN检测失败

adb shell pm disable-user --user 0 com.example.app
bash
// 调试dumpsys输出
adb shell dumpsys connectivity | grep -A5 "type: VPN"
// 检查UID解析
adb shell pm list packages --uid 1234

Check current state

自定义VPN客户端切换无效

adb shell pm list packages -d # lists disabled packages
bash
// 手动测试广播
adb shell am broadcast -a com.example.vpn.action.widget.click \
  -n com.example.vpn/.receiver.WidgetProvider
// 预期结果:result=0(RESULT_OK)或查看logcat找错误
adb logcat | grep -i "widgetprovider\|broadcast"

Re-enable manually if stuck

VPN断开后应用仍保持冻结状态

adb shell pm enable --user 0 com.example.app
undefined
  • Anubis在VPN仍活跃时绝不会解冻应用
  • 检查VPN是否完全断开:
    adb shell dumpsys connectivity | grep VPN
  • 通过长按应用图标 → "解冻" 手动恢复

VPN Detection Failing

Gradle依赖

bash
undefined
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")

    // 协程 + ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

Debug dumpsys output

adb shell dumpsys connectivity | grep -A5 "type: VPN"

Check UID resolution

adb shell pm list packages --uid 1234
undefined

Custom VPN Client Toggle Not Working

bash
undefined

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"
undefined

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")
}