跳到正文

启动方式 ​

使用应用时遇到启动或配对问题?请阅读启动使用教程。

各方式都启动应用自己的 Privileged Server 并返回 Binder,区别在于如何获得特权进程。

优先使用 Privilege UI ​

嵌入 PrivilegeScaffold 即可使用启动、权限请求、配对、确认和错误提示,见 Privilege UI。

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

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

使用 priv-core 时,应用自行实现权限请求、配对输入、确认、状态检查和错误展示。

监听连接状态 ​

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

PrivilegeServerInfo 描述连接。selinuxContext 按服务端进程缓存,仅用于诊断,无法读取时为 null。

应用级持续监听 ​

连接变化需要在应用进程存活期间持续触发工作时,在 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 死亡,且不计入 crash-loop。服务端先按指定时间被动等待,再在 followDeathDelayMillis 剩余时间内恢复配置的主动重连。等待时间应足够应用完成重启。

检查服务端被拒绝的权限 ​

内置界面的查看受限权限展示可选择、复制的列表,返回页面时刷新。

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

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

结果去重、排序并过滤系统未定义的权限。Root 返回空列表;非 Root 的关联包查询失败会报错。

实际访问还受 AppOps、SELinux 和服务内部规则影响。检查单项 Android 权限使用 Privilege.checkServerPermission(permission)。

Redmi K40 实测示例 ​

以下结果来自同一台设备,以 Shell 身份(UID 2000)运行服务端:

项目实测环境
设备市场名Redmi K40(型号 M2012K11AC)
系统名称与版本Xiaomi HyperOS 1.0(小米澎湃 OS),OS1.0.10.0.TKHCNXM
Android 版本Android 13(API 33)

连接服务端后,在协程中调用并按行输出:

kotlin
val deniedPermissions = withContext(Dispatchers.IO) {
    Privilege.getDeniedServerPermissions()
}
println(deniedPermissions.joinToString("\n"))

以下两组结果分别对应这个开关的开启和关闭:

开发者选项 - USB调试(安全设置) / 允许通过USB调试修改权限或模拟点击。

切换开关后,再次调用即可读取当前结果,无需重启特权进程。

关闭安全限制(开启“USB调试(安全设置)”)时:返回空列表,0 项权限被拒绝。

开启安全限制(关闭“USB调试(安全设置)”)时:有 14 项权限被拒绝。

text
android.permission.CALL_PHONE
android.permission.CLEAR_APP_USER_DATA
android.permission.GRANT_RUNTIME_PERMISSIONS
android.permission.INJECT_EVENTS
android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS
android.permission.MANAGE_DEVICE_ADMINS
android.permission.READ_CONTACTS
android.permission.REVOKE_RUNTIME_PERMISSIONS
android.permission.SEND_SMS
android.permission.SET_PREFERRED_APPLICATIONS
android.permission.UPDATE_APP_OPS_STATS
android.permission.WRITE_CONTACTS
android.permission.WRITE_SECURE_SETTINGS
android.permission.WRITE_SETTINGS

该列表仅代表此设备和系统版本被拒绝的权限,应用应实时查询。

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() 成功后,调用 startAdb() 启动服务端。

如果应用仍然声明并已经持有 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 授权未完成"
}
通过静态端口启动 ​

指定 port 后直接连接该端口:

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

Privilege.nativeStarterCommand 返回设备端命令,在电脑终端使用时添加 adb shell。首次读取需检查 APK,应放在工作线程。命令支持 root(0)、system(1000)和 shell(2000),非 0 用户会携带 owner user ID。见原生库打包。

再次执行会结束当前包和 Android 用户的旧服务端,确认退出后再启动新进程。停止失败则不创建新进程,其他用户的服务端不受影响。

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 命令。

授权器需以 root、system 或 shell 执行命令,应用负责授权、绑定和访问控制。下面使用已完成授权的 Shizuku 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: Starter、 UserService 实现 和 AIDL 接口。

崩溃日志 ​

在应用初始化阶段配置首选目录,早于启动服务端或首次读取 Privilege.nativeStarterCommand:

kotlin
PrivilegeConfig.crashLogDirectory = context.getExternalFilesDir("privilege-crashes")

目录由宿主创建。配置为 null 时仅使用 /data/local/tmp。服务端启动时接收路径,并在 owner 握手时刷新;独立 UserService 在启动时继承路径,早于业务构造函数执行。 已经运行的独立进程保留原目录。首选目录写入失败时(例如 shell 无权访问工作资料的 外置目录),记录器重试 /data/local/tmp。实际写入仍受文件系统和 SELinux 权限限制。

Java/Kotlin 致命异常会生成一个 UTF-8 JSON 文件。配置的目录应由当前应用和 Android 用户独享, 其中使用短文件名 priv-crash_uid<uid>_<yyyyMMddHHmmss>.json。 公共兜底目录 /data/local/tmp 中的文件名保留应用和用户标识: priv-crash_<applicationId>_u<userId>_uid<uid>_<yyyyMMddHHmmss>.json。 文件名使用设备本地时区的崩溃时间,精确到秒,假定同一应用、用户和 UID 每秒最多一次崩溃。进程类型、PID 和毫秒时间戳保留在 JSON 中。 记录器先写入 .json.tmp 临时文件,同步并关闭后原子重命名。宿主只扫描 .json, 中途退出可能留下临时文件。扫描和清理由宿主负责:应用启动时直接读取自己的外置目录, 特权连接恢复后通过 Privilege.file() 扫描兜底目录。Root 创建的文件可能需要 Root 读取。

日志对应公开的 @Serializable 数据类 PrivilegeCrashLog。Core 暴露 kotlinx-serialization-core 和生成的序列化器,内部使用 Android JSONObject 写入。 宿主需要用 Kotlin JSON 解码时,自行添加 kotlinx-serialization-json:

kotlin
val json = Json { ignoreUnknownKeys = true }
val report = json.decodeFromString(PrivilegeCrashLog.serializer(), file.readText())
if (report.schemaVersion == 1) {
    println(report.stackTrace)
}

字段包括宿主包名和 Android userId、特权进程 UID/PID 与类型、可空的服务类名、 记录器初始化和崩溃时刻(Unix 毫秒时间戳)、线程名、异常类型、可空的异常消息,以及 包含 cause 和 suppressed 异常的完整堆栈。不包含崩溃前的 Logcat。落盘为尽力执行, 两处均失败也不会阻止进程退出。启动配置解析前的失败、Native 崩溃和 SIGKILL 不在覆盖范围内。