跳到正文

启动方式

所有启动方式都会启动同一个应用自有 Privileged Server,最终都与应用建立同样的 Binder 连接。区别只在 Privileged Server 最初通过哪种方式启动。

优先使用 Privilege UI

大多数应用应直接使用 priv-ui。嵌入 PrivilegeScaffold 后,即可获得 Root、 无线调试、TCP/IP、手动和外部 Provider,以及对应的状态展示、权限请求、配对、 确认和错误提示。接入与配置见 Privilege UI

下面的 API 只面向需要替换自带授权页面的应用。

使用 priv-core 构建自定义界面

应用直接使用 priv-core 时,权限请求、配对码输入、确认界面、定期检查状态和错误 展示都需要自行实现。priv-core 只提供启动和连接 API。

监听连接状态

Privilege.serverState 是进程级的 StateFlow<PrivilegeServerInfo?>。 非 null 表示 Privileged Server 已连接,null 表示已断开。每个新的收集者 都会立即收到当前值。

PrivilegeServerInfo 是 data class,并提供便于诊断的 toString。其中的 selinuxContext 在每个服务端进程中读取并缓存一次,owner reconnect 会复用该诊断值; 平台无法提供时为 null。连接快照由 Core 构造,构造函数和 copy 都保持 internal。

应用级持续监听

连接变化需要在应用进程存活期间持续触发工作时,在 Application 创建的协程 作用域中收集:

kotlin
val appScope = MainScope()

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        appScope.launch {
            Privilege.serverState.collect { serverInfo ->
                if (serverInfo != null) {
                    // 已连接,初始化应用级特权功能。
                } else {
                    // 已断开,暂停依赖服务端的功能。
                }
            }
        }
    }
}

页面级状态展示

Compose 页面只需展示当前状态时,使用生命周期感知的收集方式:

kotlin
val serverInfo by Privilege.serverState.collectAsStateWithLifecycle()

协调 owner 进程重启

应用主动重启 owner 进程时,先安排自己的重启任务,再在结束进程前立即通知已经连接的 服务端:

kotlin
scheduleApplicationRestart()
Privilege.prepareOwnerRestart(
    passiveReconnectTimeoutMillis = 10_000,
)
Process.killProcess(Process.myPid())

调用会在服务端确认计划后返回。如果 owner 在五秒内死亡,服务端会在指定时间内保持 被动等待,让应用自己的重启策略成为唯一的进程启动来源。指定时间应为计划重启预留足够 余量;到期仍未重连时,服务端会在 PrivilegeConfig.followDeathDelayMillis 原总截止时间 剩余的范围内恢复既定的主动重连。owner 未在五秒内死亡时计划自动失效;命中的计划重启 不会计入 crash-loop 熔断统计。

检查服务端被拒绝的权限

服务端连接后,自定义界面可以读取其 UID 关联包已经声明、但服务端进程实际未获授予的 权限:

kotlin
val deniedPermissions = withContext(Dispatchers.IO) {
    Privilege.getDeniedServerPermissions()
}

返回结果会去重并按权限名排序。Root 服务端返回空列表;非 Root 服务端无法解析关联包 元数据时会报告失败,不会将其误判为不受限制。

该清单只覆盖 Android permission grant,不检查 AppOps、SELinux 策略或各系统服务内部 执行的授权,因此空列表不保证所有操作都能成功。如果应用只需确认一个已知权限,请直接 调用 Privilege.checkServerPermission(permission)

Root

kotlin
val serverInfo = Privilege.startRoot()

Root 启动会检查可用 su 路径,执行共享服务端命令,然后等待服务端建立 Binder 连接。

ADB

ADB 有两种模式:无线调试和 TCP/IP 静态端口。

无线调试

无线调试要求 Android 11 或更高版本。Priv Kit 会为应用保存一组 ADB 密钥,设备 授权这组密钥后,应用才能通过无线调试启动 Privileged Server。

配对应用

让用户打开“开发者选项 > 无线调试 > 使用配对码配对设备”。配对页面保持打开时, 将页面显示的六位配对码传给 PrivilegeAdbManager.pair()

kotlin
val adbManager = Privilege.createAdbManager()

val pairingResult = adbManager.pair(
    pairingCode = pairingCode,
)

pair() 默认自动发现无线调试的配对端口。应用已经取得端口时,可以直接传入:

kotlin
val pairingPort = adbManager.discoverPairingPort()

adbManager.pair(
    pairingCode = pairingCode,
    port = pairingPort,
)

使用 checkPairing() 检查应用保存的 ADB 密钥是否已经获得授权:

kotlin
val pairing = adbManager.checkPairing()
if (!pairing.paired) {
    // 启动前展示配对流程。
}

需要反复检查状态时,使用 openPairingCheckSession() 保持同一条连接,并在停止 检查时关闭会话。

通过无线调试启动
kotlin
val serverInfo = Privilege.startAdb()

配对与启动是两个独立操作,pair() 成功后不会自动启动服务端。

如果应用仍然声明并已经持有 WRITE_SECURE_SETTINGS,默认值 PrivilegeAdbWirelessDebuggingControl.IF_AVAILABLE 可以临时打开无线调试、发现 连接端口,并在启动尝试结束后关闭无线调试。NEVER 会保持设置不变,REQUIRE 将 托管无线调试作为启动前提。

Privileged Server 连接成功后,如果 WRITE_SECURE_SETTINGS 仍被声明,且服务端 是 Root 或拥有 android.permission.GRANT_RUNTIME_PERMISSIONS,运行时会尝试 向应用授予 WRITE_SECURE_SETTINGS。缺少这项能力时,用户需要在发现端口前手动 打开无线调试。

在需要申请本地网络访问权限的 Android 版本上,直接使用 priv-core 的应用需要 在配对或启动前请求 ACCESS_LOCAL_NETWORK

TCP/IP 启动

TCP/IP 启动连接固定的本机 ADB 端口,不发现无线调试的动态连接端口。默认静态 端口由 PRIVILEGE_ADB_DEFAULT_TCP_PORT 提供。

打开或恢复静态端口

尚未配置端口时,通过一条已经授权的 ADB 连接切换到 TCP/IP 模式:

kotlin
val tcpPort = PRIVILEGE_ADB_DEFAULT_TCP_PORT
val adbManager = Privilege.createAdbManager()

adbManager.switchToTcp(tcpPort = tcpPort)

switchToTcp() 需要一条已经授权的无线调试或现有 TCP 连接,用于执行 adb tcpip。打开或重启静态端口会影响其他依赖 ADB 的进程,自定义界面在调用前 展示自己的确认交互。 已经知道当前 ADB 连接使用的端口时,通过 options = PrivilegeAdbConnectionOptions(port = sourcePort) 直接传入。

之前已经配置静态端口时,启动前先检查并恢复该端口:

kotlin
val authorization = adbManager.prepareTcpForStart(tcpPort = tcpPort)

prepareTcpForStart() 会检查端口。如果已保存的端口匹配,但 adbd 已停止监听, 且应用能够控制 ADB,Priv Kit 可以恢复核心 ADB 服务。这个操作不会打开 无线调试。

如果结果是 PrivilegeAdbAuthorizationStatus.UNAUTHORIZED,请求授权并等待用户 在系统弹窗中确认:

kotlin
val request = adbManager.requestTcpAuthorization(tcpPort = tcpPort)
check(request.authorized) {
    request.failureMessage ?: "ADB 授权未完成"
}
通过静态端口启动

直接传入端口。portnull 时,启动流程不会发现无线调试端口:

kotlin
val serverInfo = Privilege.startAdb(
    options = PrivilegeAdbConnectionOptions(
        port = tcpPort,
    ),
)

只需检查一次授权状态时使用 checkTcpAuthorization(),需要反复检查时使用 openTcpAuthorizationCheckSession()stopTcp(tcpPort) 用于停止静态端口, restartTcp(tcpPort) 使用同一端口重启。两者都可以传入 PrivilegeAdbConnectionOptions;它们优先连接静态端口,并且只会在控制命令尚未 发送时回退到其它连接。

手动

kotlin
val nativeStarterCommand = withContext(Dispatchers.IO) {
    Privilege.nativeStarterCommand
}
YourApp.showCommandToUser("adb shell $nativeStarterCommand")

priv-core 返回设备端命令。在 Android 10 及以上版本,它可以通过系统 linker 直接执行 APK 中的 starter,也可以执行旧式打包解压出的 SO。应用向开发机器展示 命令时,在前面添加 adb shell。Starter 接受 root(UID 0)、system (UID 1000)和 shell(UID 2000)身份。不同 Android 版本的要求见 native 库打包。首次读取会检查已安装的 APK,因此需要在非主线程解析命令。User 0 使用默认 owner 作用域,不写入 userId 环境变量;非 0 Android user 显式携带 userId,因此即使应用主进程没有运行, 从外部执行命令也能保持正确的用户作用域。

再次执行 starter 时,它会先向精确匹配 owner 作用域的服务进程发送 SIGKILL, 确认旧进程退出后再创建新进程。User 0 的可读进程名以 <package>:priv-server 结尾,非 0 user 添加 -u<ownerUserId>。内部的 package/user token 在只能读取 /proc/<pid>/comm 时仍能限定发现范围。旧进程结束 失败时保持原服务端,不创建替代进程。另一 Android user 下的同包名应用使用独立作用域, 不参与匹配。

Android 10 及以上版本使用现代打包时,实际命令可能如下:

shell
adb shell /system/bin/linker64 '/data/app/.../base.apk!/lib/arm64-v8a/libprivkitstarter.so'

使用旧式打包时,命令如下:

shell
adb shell /data/app/~~-YKUdRFBwGAwYBVzJRt7pA==/priv.kit.sample.debug-A-2guZlsvRZ-9e6xF-K0kQ==/lib/arm64/libprivkitstarter.so

外部

外部启动是手动启动的扩展。手动需要用户执行 native starter 命令,外部则借助外部 授权器,在它提供的特权进程中执行同一条命令。两种方式最终进入同一条 Privileged Server 启动流程,并与应用建立同样的 Binder 连接。

外部授权器可以是应用提供的任何服务或工具,只要它能以 root、system 或 shell 身份执行该命令。应用负责接入第三方授权器、绑定服务和控制访问权限,priv-core 负责把命令传给授权器。下面以 Shizuku UserService 为例。这里假设 Shizuku 已经可用并完成授权, 只说明启动 Priv Kit 需要用到的 UserService 代码。

定义 UserService

通过应用定义的 AIDL 接口,把命令、标准输出、错误输出和完成回调传入 Shizuku UserService:

java
interface IPrivilegeShizukuStartService {
    void start(
        String commandLine,
        in ParcelFileDescriptor stdout,
        in ParcelFileDescriptor stderr,
        in ResultReceiver resultReceiver
    ) = 1;
    void destroy() = 16777114;
}

使用 PrivilegeExternalStartupHost 实现 UserService:

kotlin
class PrivilegeShizukuStartService @Keep constructor() :
    IPrivilegeShizukuStartService.Stub() {
    private val host = PrivilegeExternalStartupHost()

    override fun start(
        commandLine: String,
        stdout: ParcelFileDescriptor,
        stderr: ParcelFileDescriptor,
        resultReceiver: ResultReceiver,
    ) {
        host.start(commandLine, stdout, stderr, resultReceiver)
    }

    override fun destroy() {
        host.close()
        exitProcess(0)
    }
}

绑定 UserService 并启动

为 Shizuku UserService 设置固定的 tag。实现或 AIDL 接口不再兼容时修改 version:

kotlin
val args = Shizuku.UserServiceArgs(
    ComponentName(
        context.packageName,
        PrivilegeShizukuStartService::class.java.name,
    ),
)
    .daemon(false)
    .tag("priv-kit-external-start")
    .processNameSuffix("priv-kit-shizuku-start")
    .version(1)

Shizuku.bindUserService(args, serviceConnection)

ServiceConnection 返回 AIDL 接口后,把它的 start() 方法传给 Priv Kit:

kotlin
val nativeStarterCommand = withContext(Dispatchers.IO) {
    Privilege.nativeStarterCommand
}

PrivilegeExternalStartup.runThroughBridge(
    commandLine = nativeStarterCommand,
    bridge = { commandLine, stdout, stderr, resultReceiver ->
        shizukuService.start(commandLine, stdout, stderr, resultReceiver)
    },
)

请求结束后通过 Shizuku.unbindUserService(...) 关闭连接。完整实现见 sample: StarterUserService 实现AIDL 接口