我的手机是国行小米 15,平时用 KeePassDX 管理密码。升级系统后,点击“打开现有数据库”,出现的是小米的“安全访问”界面,文件选择流程也随之发生了变化。

这次排查的目标,是让应用的文件选择流程回到 Android 自带的 DocumentsUI。最终从 KeePassDX 的“打开现有数据库”入口验证了原生文件选择器已经恢复。

真正生效的方法,是通过 ADB 启动一个很小的 Java 程序,直接调用 Android 的包管理 Binder 接口,为主用户移除小米文件管理,同时保留应用数据。没有获取 root,没有进入 recovery,也没有解锁 Bootloader。

这里的“关闭安全访问”有明确范围:让小米文件管理提供的文件选择页面不再接管本次文件打开流程。它不是关闭 HyperOS 所有名为“安全访问”的功能,也不包括照片选择器。

实测环境和影响范围

实测日期:2026 年 10 月 10 日。

项目 版本或状态
手机 国行小米 15
系统 OS4.0.0.17.XOBCNXM
Android Android 17,API 37
安全补丁 2026-09-01
当前用户 0
小米文件管理 com.android.fileexplorer,9.2.5.3
DocumentsUI com.google.android.documentsui,17
KeePassDX 4.5.5
操作电脑 Windows,PowerShell,JDK 21,Android Platform Tools

这个方法会让小米文件管理在主用户下变为未安装,它的桌面入口和其他功能也会随之不可用。系统分区里的预装包仍然保留,不是把 APK 从系统分区删除。

我原本希望只停用文件选择组件、保留文件管理其他功能,但这版系统拒绝了单组件停用。最后采用整包的按用户移除,是一个明确的取舍。

本文是这台手机、这个版本上的操作记录。其他版本可能修改接口、权限检查或回退逻辑,不能仅凭同为 HyperOS 4 就认定可直接照搬。操作前应备份重要文件,并先确认手机仍然有可用的 DocumentsUI。

应用如何调用系统文件选择器

KeePassDX 使用 Android 的 Storage Access Framework,简称 SAF。正常情况下,应用发起打开文件请求,系统显示文件选择器,由 DocumentsProvider 提供文件。选中后,应用获得一个 content URI 及相应访问授权,再通过提供方读取或写入内容。

在这次实机上,“安全访问”前台组件是:

com.android.fileexplorer/.picker.PickMainNavigatorActivity

它接到的请求是:

act=hyper.intent.action.OPEN_DOCUMENT
pkg=com.android.fileexplorer
cmp=com.android.fileexplorer/.picker.PickMainNavigatorActivity

Android 标准打开文件请求则是:

android.intent.action.OPEN_DOCUMENT

实机记录说明,最终打开的是小米文件管理的专用页面。记录本身不能完整还原请求在哪一层转换,但足以确定实际负责显示页面的包和 Activity。小米官方文件选择控件文档也以标准 OPEN_DOCUMENT 接口作为应用接入方式。小米文件选择控件文档

KeePassDX 的普通单击和长按也不相同:普通单击请求 OPEN_DOCUMENT,长按请求 GET_CONTENT。在我的系统上,GET_CONTENT 的默认处理者是 HyperOS 定制照片选择器,因此长按仍然没有解决问题。KeePassDX 文件管理与同步说明

普通 ADB 方法为什么没有成功

最开始尝试的是更小范围的组件停用:

& $adb -s $serial shell pm disable-user --user 0 com.android.fileexplorer/.picker.PickMainNavigatorActivity
& $adb -s $serial shell pm disable --user 0 com.android.fileexplorer/.picker.PickMainNavigatorActivity

系统分别拒绝把组件状态改成 3 和 2,核心错误是:

java.lang.SecurityException:
Shell cannot change component state for ComponentInfo{...}

整包停用也被拒绝:

java.lang.SecurityException: Cannot disable system packages.

接着尝试常见的按用户卸载命令,包含保留数据的 -k:

& $adb -s $serial shell pm uninstall -k --user 0 com.android.fileexplorer

得到:

Failure [only root can delete system app for a particular user]

这类 Android 17 报错在工具项目中已有记录。它解释了为什么旧教程中的命令在新系统上会失败,但不能据此推导“所有底层接口都已经禁止同一操作”。Universal Android Debloater FAQ

我还检查过从手机提取的 miui-services.jar,发现文件选择跳转涉及这个 DeviceConfig 配置:

namespace: securitycenter
key: hyper_refer_file_picker

SecuritySettingsObserver.refreshSecurityCenterConfig() 读取这个布尔值,缺省值为 true;enableHyperFilePicker() 最终返回对应状态。这是一个比移除应用更局部的候选开关。

但在我的设备上,device_config put 和 override 都被拒绝:

Permission denial for flag 'securitycenter/hyper_refer_file_picker';
allowlist permission granted, but must add flag to the allowlist

最终该配置仍为未设置,并没有被改成 false。Android 对 shell 的 DeviceConfig 写权限确实存在白名单限制;知道配置名,不代表当前权限可以写它。AOSP 关于 DeviceConfig shell 权限变化的提交

最终方法的原理

最终使用的调用是:

IPackageManager.deletePackageAsUser(
    "com.android.fileexplorer",
    -1,
    null,
    0,
    5
);

这个方法是 Android 内部接口。AOSP 的 IPackageManager.aidl 中可以看到参数顺序,但它不是面向普通应用的稳定公共 API。AOSP IPackageManager 定义

参数 本次取值 用途
packageName com.android.fileexplorer 只针对小米文件管理
versionCode -1 不限定具体版本
observer null 不接收删除完成回调,因此必须另外核对状态
userId 0 只针对主用户
flags 5 保留数据,并按系统应用的用户移除方式处理

flags 的 5 来自两个标志按位或:

DELETE_KEEP_DATA  = 1
DELETE_SYSTEM_APP = 4
1 | 4            = 5

DELETE_KEEP_DATA 表示保留应用数据。DELETE_SYSTEM_APP 对有更新包的系统应用尤其重要:它用于按用户标记卸载,避免走通常的卸载更新、退回系统版本流程。这里没有使用面向全部用户的 DELETE_ALL_USERS。AOSP PackageManager 标志说明

辅助程序通过 app_process 在手机的 Android Runtime 中执行,仍然是 ADB shell 身份。它利用反射取得 AppGlobals.getPackageManager() 返回的 Binder 代理,再调用上述接口。

可以把两条路径理解为:

普通命令
ADB shell → pm uninstall 的命令处理路径 → 本机返回 root 限制

本次有效路径
ADB shell → app_process → Java 反射
          → IPackageManager Binder 调用
          → 主用户安装状态变为 installed=false

这没有让 shell 变成 root,也没有取消系统服务的权限检查。 实测结论只是:这版系统中,普通命令路径被拒绝,而直接接口调用完成了目标操作。不能把它扩展为任意系统应用都能移除,或任意权限都能绕过;也没有依据给它指定某个 CVE 编号。

准备工具并检查设备

下面统一使用 Windows PowerShell。新建一个工作目录,把 Java 源码和编译产物都放在其中。准备:

  • Android Platform Tools,提供 adb。官方下载

  • JDK 21,提供 java 和 javac。

  • R8 JAR,使用其中的 D8 把 Java class 转成 DEX。本次实际使用 9.6.0-alpha02;这里固定这个已用过的版本,不表示必须追逐预览版编译器。Google Maven 下载

打开手机的 USB 调试,在手机上授权电脑。先修改下面两个值:

$adb = 'C:\Android\platform-tools\adb.exe'
& $adb devices -l

# 替换为 devices 输出中的设备序列号
$serial = 'YOUR_DEVICE_SERIAL'

确认状态是 device,再检查版本与当前用户:

& $adb -s $serial shell getprop ro.build.version.incremental
& $adb -s $serial shell getprop ro.build.version.release
& $adb -s $serial shell getprop ro.build.version.sdk
& $adb -s $serial shell am get-current-user
& $adb -s $serial shell pm list packages --user 0 com.android.fileexplorer
& $adb -s $serial shell pm list packages --user 0 documentsui

本文程序固定操作 user 0。如果当前用户不是 0,不要直接执行修改步骤。

还要确认标准文件选择请求有处理者:

& $adb -s $serial shell 'cmd package query-activities --brief -a android.intent.action.OPEN_DOCUMENT -c android.intent.category.OPENABLE -t "*/*"'

我的输出包含:

com.google.android.documentsui/com.android.documentsui.picker.PickActivity

可以先显式打开它,确认不会立即崩溃:

& $adb -s $serial shell 'am start -W -a android.intent.action.OPEN_DOCUMENT -c android.intent.category.OPENABLE -t "*/*" -n com.google.android.documentsui/com.android.documentsui.picker.PickActivity'

有些系统使用 com.android.documentsui 包名,应以查询结果为准。如果没有可用的 DocumentsUI,就停在这里,不要先把现有选择器移走。

操作前保存包状态:

& $adb -s $serial shell dumpsys package com.android.fileexplorer |
    Set-Content -Encoding utf8 .\fileexplorer-before.txt

这个文本只是状态记录,不是应用数据备份。重要文件仍需单独备份。

编译并执行辅助程序

把下面内容保存为 PickerRepair.java。这是本次实际使用的程序,目标包、用户和保留数据标志都固定在代码里,没有做通用卸载器。

import java.lang.reflect.*;

/** Single-purpose ADB helper; never clears data or targets another package. */
public final class PickerRepair {
    public static void main(String[] args) throws Exception {
        Class<?> globals = Class.forName("android.app.AppGlobals");
        Object pm = globals.getMethod("getPackageManager").invoke(null);
        Class<?> api = Class.forName("android.content.pm.IPackageManager");
        if (args.length == 0 || !"remove-keep-data".equals(args[0])) {
            for (Method m : api.getMethods()) {
                if (m.getName().equals("deletePackageAsUser")) System.out.println(m);
            }
            return;
        }
        Class<?> constants = Class.forName("android.content.pm.PackageManager");
        int keep = constants.getField("DELETE_KEEP_DATA").getInt(null);
        int system = constants.getField("DELETE_SYSTEM_APP").getInt(null);
        if (keep != 1 || system != 4) throw new IllegalStateException("Unexpected flags");
        Class<?> observer = Class.forName("android.content.pm.IPackageDeleteObserver");
        Method delete = api.getMethod("deletePackageAsUser",
                String.class, int.class, observer, int.class, int.class);
        System.out.println("Request: package=com.android.fileexplorer user=0 keep-data=true flags=" + (keep | system));
        try {
            delete.invoke(pm, "com.android.fileexplorer", -1, null, 0, keep | system);
            System.out.println("Request submitted; verify installed state separately.");
        } catch (InvocationTargetException e) {
            e.getCause().printStackTrace();
            System.exit(1);
        }
    }
}

程序不传参数时,只输出接口签名。只有传入 remove-keep-data,才会提交移除请求。main 没有返回值;反射接口不存在时会抛异常,服务调用报错时输出底层异常并以非零状态退出。由于没有 observer,程序正常结束也不能等同于操作完成。

在同一个工作目录中编译:

# 确认 java 和 javac 都来自准备好的 JDK
java -version
javac -version

# 把下载的 JAR 保存为当前目录的 r8.jar
javac --release 8 .\PickerRepair.java
if ($LASTEXITCODE -ne 0) { throw 'Java 编译失败' }

New-Item -ItemType Directory -Force .\dex | Out-Null
java -cp .\r8.jar com.android.tools.r8.D8 --min-api 26 --output .\dex .\PickerRepair.class
if ($LASTEXITCODE -ne 0) { throw 'DEX 编译失败' }

& $adb -s $serial push .\dex\classes.dex /data/local/tmp/codex-picker-repair.dex
if ($LASTEXITCODE -ne 0) { throw '推送失败' }

这里的 –release 8 是 Java 字节码目标,–min-api 26 是 DEX 的最低 API 编译参数,都不是在修改手机系统版本。

先只检查接口:

& $adb -s $serial shell 'CLASSPATH=/data/local/tmp/codex-picker-repair.dex app_process /system/bin PickerRepair'

本次输出的签名为:

public abstract void android.content.pm.IPackageManager.deletePackageAsUser(
    java.lang.String,
    int,
    android.content.pm.IPackageDeleteObserver,
    int,
    int
) throws android.os.RemoteException

确认接口存在、前面的设备检查正确后,再执行修改:

& $adb -s $serial shell 'CLASSPATH=/data/local/tmp/codex-picker-repair.dex app_process /system/bin PickerRepair remove-keep-data'

本次输出:

Request: package=com.android.fileexplorer user=0 keep-data=true flags=5
Request submitted; verify installed state separately.

如果出现权限错误或找不到方法,就停止并保留错误信息。不要为了强行继续而去掉保留数据标志、换成全用户操作,或猜测 Binder transaction 编号。

验证安装状态和实际回退

等待片刻后,查询主用户安装列表:

& $adb -s $serial shell pm list packages --user 0 com.android.fileexplorer

成功时没有这个包的输出。但单凭空输出还不够,还要确认 ADB 命令正常执行,并读取包状态:

& $adb -s $serial shell dumpsys package com.android.fileexplorer |
    Set-Content -Encoding utf8 .\fileexplorer-after.txt

我看到当前生效包的 User 0 状态变成:

installed=false
stopped=true

前后记录中的 ceDataInode 和 deDataInode 保持相同。这与保留应用数据的标志相符,但 inode 一致不等于逐文件校验。dumpsys 还可能显示系统分区原始包的历史状态,不能拿另一段 installed=true 就断言操作失败,应结合当前安装列表一起判断。

再查询小米专用入口:

& $adb -s $serial shell 'cmd package query-activities --brief -a hyper.intent.action.OPEN_DOCUMENT -c android.intent.category.OPENABLE -t "*/*"'

我的输出是:

No activities found

然后发起标准文件选择:

& $adb -s $serial shell 'am start -W -a android.intent.action.OPEN_DOCUMENT -c android.intent.category.OPENABLE -t "*/*"'

结果进入:

com.google.android.documentsui/com.android.documentsui.picker.PickActivity

最后从 KeePassDX 自己的“打开现有数据库”按钮进入,确认显示的是原生文件选择器。这个实际应用内的验证,比单独运行 am start 更有意义。

如果标准选择器无法打开,立即执行下一节的恢复命令,不必重启手机碰运气。

恢复方法和清理

恢复小米文件管理:

& $adb -s $serial shell cmd package install-existing --user 0 com.android.fileexplorer
& $adb -s $serial shell pm list packages --user 0 com.android.fileexplorer

这条命令让仍保留在设备上的包重新对主用户可用,不需要从第三方下载 APK。本次保留了预装包及数据,但没有为了写文章而再次恢复、重新移除;因此它是准备好的回退路径,不是本次额外完成的一轮恢复实测。

恢复文件管理后,它也可能再次接管文件选择。若系统更新后出现同样情况,应先重新检查包和 Activity 状态,不要机械重复旧命令。

确认处理结束后,删除手机上的临时 DEX:

& $adb -s $serial shell rm /data/local/tmp/codex-picker-repair.dex

辅助程序只运行一次,没有安装为 Android 应用,也没有常驻后台服务。电脑上的源码和状态记录可以留作复查。

同样处理小米密码管理器

恢复文件选择器后,我又用相同思路处理了小米密码管理器。先确认目标包和当前默认服务,再决定是否移除,避免误动 Android 自带的凭据管理组件。

确认包名和当前默认服务

这台手机上的小米密码管理器是:

包名:com.miui.passwords
版本:1.1.23
系统路径:/product/app/MiuiPasswords

它提供的两个服务分别是:

自动填充:
com.miui.passwords/.newautofill.MiuiAutofillService

通行密钥:
com.miui.passwords/.credential.provider.XiaomiCredentialProviderService

这里的目标是 com.miui.passwords。不要把 com.android.credentialmanager 当成同一个应用,后者是 Android 的凭据管理组件。

沿用前文的 $adb 和 $serial,先检查并保存状态:

& $adb -s $serial shell am get-current-user
& $adb -s $serial shell pm list packages --user 0 com.miui.passwords
& $adb -s $serial shell dumpsys package com.miui.passwords |
    Set-Content -Encoding utf8 .\xiaomi-passwords-before.txt

& $adb -s $serial shell settings get secure autofill_service
& $adb -s $serial shell settings get secure credential_service
& $adb -s $serial shell settings get secure credential_service_primary

本次主用户为 0,默认自动填充和首选通行密钥服务都已经是 KeePassDX,因此没有更改这些设置。如果当前默认服务仍指向小米密码管理器,应先在系统设置中选好替代服务,再处理目标包。

普通停用被拒绝后保留数据移除

先尝试普通停用:

& $adb -s $serial shell pm disable-user --user 0 com.miui.passwords

这版系统返回:

java.lang.SecurityException: Cannot disable system packages.

因此,最终没有把应用的 enabled 状态改成 disabled,而是调用前文的底层包管理接口,让它在主用户下变为未安装:

deletePackageAsUser("com.miui.passwords", -1, null, 0, 5);

flags 仍然是 DELETE_KEEP_DATA | DELETE_SYSTEM_APP,也就是 1 | 4 = 5。保留应用数据,只针对 user 0,不清除密码数据,不删除系统分区中的 APK,也不处理其他用户。

这意味着小米密码管理器及其服务在该用户下不再可用;原有密码不会自动迁移到 KeePassDX。需要使用其中保存的内容时,应先恢复应用。保留数据标志也不能代替独立备份。

不必重新编写整份程序。在前文 PickerRepair.java 的基础上,只替换类名和目标包名,生成一个专门处理密码管理器的源码:

$sourceText = Get-Content -LiteralPath .\PickerRepair.java -Raw
$sourceText.Replace('PickerRepair', 'XiaomiPasswordsDisable').
    Replace('com.android.fileexplorer', 'com.miui.passwords') |
    Set-Content -LiteralPath .\XiaomiPasswordsDisable.java -Encoding ascii

javac --release 8 .\XiaomiPasswordsDisable.java
if ($LASTEXITCODE -ne 0) { throw 'Java 编译失败' }

New-Item -ItemType Directory -Force .\passwords-dex | Out-Null
java -cp .\r8.jar com.android.tools.r8.D8 --min-api 26 --output .\passwords-dex .\XiaomiPasswordsDisable.class
if ($LASTEXITCODE -ne 0) { throw 'DEX 编译失败' }

& $adb -s $serial push .\passwords-dex\classes.dex /data/local/tmp/codex-passwords-disable.dex
if ($LASTEXITCODE -ne 0) { throw '推送失败' }

先不传修改参数,核对接口:

& $adb -s $serial shell 'CLASSPATH=/data/local/tmp/codex-passwords-disable.dex app_process /system/bin XiaomiPasswordsDisable'

确认前面的检查无误后,执行:

& $adb -s $serial shell 'CLASSPATH=/data/local/tmp/codex-passwords-disable.dex app_process /system/bin XiaomiPasswordsDisable remove-keep-data'

目标已经固定为 com.miui.passwords,user 0 和保留数据标志与前文一致。请求提交后仍需验证,不能仅凭程序没有报错就认定成功。

验证密码管理器和默认填充状态

执行以下查询:

& $adb -s $serial shell pm list packages --user 0 com.miui.passwords
& $adb -s $serial shell dumpsys package com.miui.passwords |
    Set-Content -Encoding utf8 .\xiaomi-passwords-after.txt

& $adb -s $serial shell cmd package query-services --brief -a android.service.autofill.AutofillService -p com.miui.passwords
& $adb -s $serial shell cmd package query-services --brief -a android.service.credentials.CredentialProviderService -p com.miui.passwords

& $adb -s $serial shell settings get secure autofill_service
& $adb -s $serial shell settings get secure credential_service_primary

本次实际结果:

  • 主用户安装列表中不再出现 com.miui.passwords。

  • 包状态为 installed=false、stopped=true。

  • 自动填充和通行密钥服务查询均返回 No services found。

  • 默认自动填充和首选通行密钥仍指向 KeePassDX。

  • 前后记录中的 ceDataInode、deDataInode 保持一致。

这些结果确认了包安装状态和服务注册状态变化。数据目录标识相同与保留数据的操作相符,但不等于逐条核验了密码内容;实际登录页面的填充体验仍应单独验收。

恢复小米密码管理器

需要恢复时,执行:

& $adb -s $serial shell cmd package install-existing --user 0 com.miui.passwords
& $adb -s $serial shell pm list packages --user 0 com.miui.passwords

恢复使用设备上保留的系统安装包。上述流程没有修改默认自动填充服务,因此恢复应用也不等于把它重新设为默认;如果需要,另外到系统设置中选择。

本次没有为了验证恢复命令而重新启用密码管理器;它作为保留安装包前提下的回退路径记录在这里。

最后清理手机上的临时程序:

& $adb -s $serial shell rm /data/local/tmp/codex-passwords-disable.dex

这个辅助程序同样没有安装为应用,也没有留下常驻服务。

这次排查留下的经验

对我来说,最有用的是先查清“当前页面到底属于谁”,再决定动哪个组件。看到“安全访问”几个字就去关闭整个系统优化,范围太大,也缺少针对性。

第二点是把命令成功、包状态变化和实际功能恢复分开验证。直接接口调用返回后仍需要检查 installed 状态,再从应用内重新发起文件选择,确认真正进入 DocumentsUI。

恢复原生文件选择器的关键改动,是通过底层包管理接口,为 user 0 移除 com.android.fileexplorer,并保留数据。随后对 com.miui.passwords 采用了相同的按用户移除方式。两项操作分别针对文件选择入口和小米密码管理器,都保留了应用数据和系统安装包,并准备了恢复命令。