anubis-android-app-manager
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAnubis 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 () 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.
pm disable-user由ara.so开发的技能——属于Daily 2026 Skills合集。
Anubis是一款Android应用管理器,它利用Shizuku通过命令,根据VPN连接状态完全禁用应用组。与沙箱解决方案(Island、Shelter)不同,被禁用的应用无法运行代码、访问网络接口,甚至无法检测到VPN的存在。
pm disable-userCore 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 |
| 组策略 | 行为 |
|---|---|
| 本地组 | 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
设置步骤
- Install and start Shizuku (via ADB or Wireless Debugging):
bash
adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/start.sh - Install Anubis APK
- Grant Shizuku permission when prompted
- Grant VPN permission when prompted (needed for dummy VPN disconnect)
- Go to Apps tab → assign apps to groups
- Select VPN client in Settings
- Toggle stealth mode on Home screen
- 安装并启动Shizuku(通过ADB或无线调试):
bash
adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/start.sh - 安装Anubis APK
- 出现提示时授予Shizuku权限
- 出现提示时授予VPN权限(用于虚拟VPN断开操作)
- 进入应用标签页 → 将应用分配到对应组
- 在设置中选择VPN客户端
- 在首页开启隐身模式
Building
构建步骤
bash
git clone https://github.com/sogonov/anubis
cd anubisbash
git clone https://github.com/sogonov/anubis
cd anubisDebug 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
- NetworkCallback for VPN state monitoring
ConnectivityManager - for pinned shortcuts
ShortcutManager
- Kotlin + Jetpack Compose(Material 3)
- Shizuku API 13.1.5(AIDL UserService模式)
- 带TypeConverters的Room数据库用于存储应用组
- NetworkCallback用于VPN状态监控
ConnectivityManager - 用于固定快捷方式
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 selectionapp/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:
- Open APK in jadx
- Resources → find in
app_widget_namestrings.xml - Manifest → find with
<receiver>metadataandroid.appwidget.provider - Receiver code → find calls
setAction("...") - 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/$receiverClassRegister 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分析发现广播动作:
- 在jadx中打开APK
- 资源 → 在中找到
strings.xmlapp_widget_name - 清单 → 找到带有元数据的
android.appwidget.provider<receiver> - 接收器代码 → 找到调用
setAction("...") - 验证切换模式:
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
undefinedbash
// 手动验证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.appVerify 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 1234Check 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
undefinedkotlin
// 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
-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")
}—