☰
Android 11 默认授予第三方 USB 权限的 Framework 方案
2026/9/29 4:50:04 网站建设 项目流程

做自助终端和工业平板那几年,最怕客户半夜打电话说扫码枪不识别。远程连上去一看,Android 11 的 USB 授权弹窗还在屏幕上挂着,无人值守设备没人点“允许”,应用就一直在等。这个场景和 Android 11 默认授予第三方应用 USB 权限的需求几乎一模一样:设备是受控的,应用是固定的,USB 外设也是提前选好的,但系统每次插入都要弹一次授权框,体验直接崩掉。后来我在几个定制 ROM 上把这件事从 framework 层解决掉,才发现核心并不复杂,难的是搞清楚 Android 11 的 USB 权限链路、资源覆盖是否生效、以及默认授权之后的安全边界。

这篇内容围绕 Android 11 默认授予第三方应用 USB 权限展开。适合做 Android 系统定制、ROM 开发、工业终端、自助机、智能硬件的同学参考。应用开发者也能从里面看到,为什么自己明明声明了 USB intent-filter,换到 Android 11 后还是会被权限拦住。下面我按实际排查和修改顺序,把源码链路、overlay 方案、白名单补丁、应用侧配合和验证方法一次讲透。

1. 从一次无人值守设备故障说起:USB 授权弹窗到底卡在哪

1.1 现场现象与影响

设备是一台 Android 11 工业平板,外接 USB 扫码枪。应用启动后能收到 USB 插入广播,也能枚举到设备,但调用UsbManager.openDevice()一直抛SecurityException。日志里能看到User has not given permission to device。如果手动点一下系统弹窗,扫码枪立刻可用。问题是设备装在柜子里,没有触摸屏,也没人值守。每次断电重启、拔插扫码枪,都要重新授权。客户不能接受,因为这意味着现场必须有人操作。

这类问题在自助收银、快递柜、门禁、医疗终端、车载设备里非常常见。Android 从 6.0 开始对 USB host 权限管得越来越严,到 Android 11 时,UsbUserPermissionManager和UsbProfileGroupSettingsManager的分工已经比较清晰。第三方应用不是系统应用,拿不到MANAGE_USB权限,所以每次访问 USB 设备都要走用户授权流程。系统应用或者平台签名应用可以绕过一部分限制,但大多数第三方应用没有这个条件。于是需求就变成了:由系统定制方在 framework 层默认授予第三方应用 USB 权限,让应用不需要用户点确认。

这里要先分清两件事。第一,USB 设备插入时,系统会发送android.hardware.usb.action.USB_DEVICE_ATTACHED,如果应用在 Manifest 里声明了匹配的 intent-filter 和device_filter.xml,系统可能启动对应 Activity。第二,应用真正打开设备前,还要调用requestPermission(),或者系统在 attach 流程里帮它授权。弹窗就发生在第二步。很多开发者以为声明了 intent-filter 就自动有权限,实际上 Android 11 上仍然要经过权限管理器的检查。

1.2 Android USB 权限模型的三层检查

Android 11 的 USB 权限不是一层判断,而是三层。第一层在UsbService,它负责 Binder 接口,接收应用调用。第二层在UsbUserSettingsManager和UsbUserPermissionManager,前者管理设备、附件、包名和用户的关系,后者专门管理权限的授予、检查和持久化。第三层在UsbProfileGroupSettingsManager,它处理设备插入时的 Activity 解析、默认应用选择、多用户 profile group 等逻辑。

应用调用UsbManager.requestPermission(device, pendingIntent)时,Binder 进入UsbService.requestDevicePermission()。UsbService会把当前用户 ID、包名、UID、PID 一起传给UsbUserPermissionManager.requestPermission()。权限管理器先检查是不是系统权限,然后检查mDisablePermissionDialogs这个开关。如果开关为 true,直接调用grantDevicePermission(),持久化到/data/system/users/<userId>/usb/usb_permissions.xml,然后通过 PendingIntent 通知应用授权成功。如果开关为 false,就启动UsbPermissionActivity或者UsbAccessoryPermissionActivity,等用户点允许或拒绝。

hasPermission()是另一个关键入口。应用调用openDevice()时,UsbService.openDevice()会调用UsbUserPermissionManager.hasPermission()。如果返回 false,直接抛异常。也就是说,只改requestPermission()还不够,如果某些应用不主动请求权限,直接打开设备,仍然会被hasPermission()拦住。这一点是很多补丁第一次不生效的原因。

1.3 为什么第三方应用比系统应用更容易被拦

系统应用或者平台签名应用可以被授予android.permission.MANAGE_USB。UsbUserPermissionManager.hasPermission()里有一行:

if (mContext.checkPermission(android.Manifest.permission.MANAGE_USB, pid, uid) == PackageManager.PERMISSION_GRANTED) { return true; }

只要拿到这个权限,USB 权限检查直接通过。但MANAGE_USB的 protectionLevel 是 signature 级别,第三方应用即使把权限写进 Manifest,安装时也不会授予。除非把应用预置到/system/priv-app并在privapp-permissions白名单里放行,否则没戏。很多客户又不愿意让应用团队改签名、走系统预置,因为应用要独立升级,预置后升级会麻烦。所以更通用的做法是在系统服务里做默认授权,或者打开系统隐藏的“禁用 USB 权限对话框”开关。

顺带说一句,网络上有人提到“Android 12 默认授予所有应用权限”,这个说法容易让人误会。Android 12 并没有把 USB 权限对所有普通应用完全放开,更多是部分定制 ROM 和设备配置改了默认值。Android 11 上要稳定实现,还是得自己动 framework 资源或服务代码。

2. Android 11 源码里的权限授予链路:从 requestPermission 到 usb_permissions.xml

2.1 UsbService、UsbUserSettingsManager、UsbUserPermissionManager 的分工

Android 11 的 USB 服务代码在frameworks/base/services/usb/java/com/android/server/usb/。核心文件包括:

  • UsbService.java:Binder 服务入口,管理 USB host、device、accessory、port、alsa 等。
  • UsbUserSettingsManager.java:按用户管理 USB 设备、附件、包名、连接记录。
  • UsbUserPermissionManager.java:按用户管理 USB 权限,读写usb_permissions.xml。
  • UsbProfileGroupSettingsManager.java:处理 profile group 内的设备插入、Activity 解析和默认应用。
  • UsbHostManager.java:监听底层 USB 设备节点,向系统上报设备插入和拔出。
  • UsbDeviceManager.java:管理 USB gadget 模式,也就是设备作为从设备时的功能切换。

权限相关的主角是UsbUserPermissionManager。它构造时会确定权限文件路径:

File systemDir = new File(Environment.getUserSystemDirectory(userId), "usb"); mPermissionsFile = new AtomicFile(new File(systemDir, "usb_permissions.xml"), "usb-permissions");

所以 Android 11 上权限文件实际在:

/data/system/users/0/usb/usb_permissions.xml

多用户设备会在/data/system/users/10/usb/usb_permissions.xml、/data/system/users/11/usb/usb_permissions.xml下分别保存。默认授权如果只对当前用户生效,切换用户后可能又失效。这一点在测试时要覆盖。

2.2 requestPermission 的完整分支:弹窗、直接授权、持久化

UsbUserPermissionManager.requestPermission()的逻辑可以简化成下面这样:

public void requestPermission(UsbDevice device, String packageName, PendingIntent pi, int uid) { Intent intent = new Intent(); if (mDisablePermissionDialogs) { grantDevicePermission(device, uid); intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true); try { pi.send(mContext, 0, intent); } catch (PendingIntent.CanceledException e) { // 记录异常 } return; } // 否则启动 UsbPermissionActivity,等待用户确认 // 用户确认后调用 grantDevicePermission() 或 deny }

关键就是mDisablePermissionDialogs。它在UsbUserPermissionManager初始化时从资源读取:

mDisablePermissionDialogs = mContext.getResources().getBoolean( com.android.internal.R.bool.config_disableUsbPermissionDialogs);

config_disableUsbPermissionDialogs定义在frameworks/base/core/res/res/values/config.xml,默认值通常是 false。也就是说,系统本身留了一个开关,只是默认关闭。把设备 overlay 里的这个 bool 改成 true,requestPermission()就不会弹窗,而是直接授予权限并写进usb_permissions.xml。这是最干净的第一方案。

但要注意,这个开关只影响主动调用requestPermission()的应用。如果应用不请求权限,直接openDevice(),hasPermission()仍然可能返回 false。因为hasPermission()默认不会因为mDisablePermissionDialogs为 true 就直接放行所有包,它主要查已经持久化的权限记录和MANAGE_USB。所以应用侧最好还是保留一次requestPermission()调用。系统默认授权之后,这次调用不会弹窗,而是立刻返回成功。

2.3 config_disableUsbPermissionDialogs 这个隐藏开关

config_disableUsbPermissionDialogs在 AOSP 里原本是给特殊设备用的,比如无头设备、kiosk、工业终端。它的语义就是“禁用 USB 权限对话框”。开启之后,UsbUserPermissionManager和UsbProfileGroupSettingsManager都会改变行为。UsbProfileGroupSettingsManager在设备插入时,如果发现多个 Activity 都能处理该 USB 设备,正常会弹选择框。开关开启后,它会尽量直接选择默认组件,不弹选择界面。

用资源 overlay 改这个值有几个好处。第一,改动量极小,不用碰 Java 逻辑。第二,可维护,Android 11 到 Android 12 如果资源名还在,升级时容易迁移。第三,可以按产品线区分,A 产品打开默认授予,B 产品保持弹窗。第四,方便回退,把 overlay 去掉重新编译 framework-res 就行。

但它也有边界。它不会让一个从未请求权限的应用直接获得权限;它只是让请求过程变成静默授予。它也不会按包名白名单限制,打开之后所有调用requestPermission()的应用都会被授予。所以生产设备上最好配合应用侧控制,或者进一步做白名单补丁。

3. 推荐改法:用资源 overlay 打开默认授予,而不是硬改 Java

3.1 overlay 的具体目录与 config.xml 写法

不要直接改frameworks/base/core/res/res/values/config.xml。直接改 AOSP 公共代码,后面合并、升级、多产品维护都会痛苦。正确做法是在设备目录下建一个 overlay:

device/<vendor>/<product>/overlay/frameworks/base/core/res/res/values/config.xml

文件内容:

<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 打开 USB 权限静默授予开关 --> <bool name="config_disableUsbPermissionDialogs">true</bool> </resources>

然后在产品的device.mk里把这个 overlay 加进去:

PRODUCT_PACKAGE_OVERLAYS += device/<vendor>/<product>/overlay

有些平台使用DEVICE_PACKAGE_OVERLAYS,有些使用PRODUCT_PACKAGE_OVERLAYS。两者区别简单说:DEVICE_PACKAGE_OVERLAYS更偏向设备级,PRODUCT_PACKAGE_OVERLAYS更偏向产品级。如果同一套代码出多个产品,建议用产品级,按产品目录区分。写完以后编译 framework-res:

source build/envsetup.sh lunch <product>-userdebug m framework-res

编译产物在:

out/target/product/<product>/system/framework/framework-res.apk

如果只验证资源覆盖,可以只刷framework-res.apk,但实际发布还是刷完整system.img更稳。因为 framework-res 和 services.jar 之间有资源 ID 依赖,单独替换有时会遇到资源表不匹配。

3.2 把 overlay 编进系统镜像:PRODUCT_PACKAGE_OVERLAYS 的配置

如果编译后发现 overlay 没生效,优先检查三件事。第一,PRODUCT_PACKAGE_OVERLAYS的路径是不是相对源码根目录,不要写成绝对路径。第二,设备目录是否在 lunch 选中的产品继承链里。有些产品通过inherit-product继承多个 mk,overlay 可能被后面的配置覆盖。第三,overlay 里的资源名是否和 AOSP 一致。Android 11 是config_disableUsbPermissionDialogs,但某些厂商 ROM 可能改名或删掉。用下面的命令全局搜:

grep -rn "config_disableUsbPermissionDialogs" frameworks/base

正常会看到:

frameworks/base/core/res/res/values/config.xml frameworks/base/core/res/res/values/symbols.xml frameworks/base/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java frameworks/base/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java

如果只在 symbols.xml 里看到,说明资源被裁剪了,需要自己补回 bool 定义。如果 Java 里读取的变量名不同,就要按实际代码调整。

还有一种情况是产品使用了 RRO overlay,而不是静态 overlay。RRO 适合改应用资源,但改com.android.internal.R.bool这种 framework 内部资源,静态 overlay 更直接。RRO 对系统框架资源的覆盖受 target package 和资源可见性限制,搞不好编译通过但运行时不生效。所以能用PRODUCT_PACKAGE_OVERLAYS就不要绕 RRO。

3.3 编译、刷机与验证 overlay 是否生效

刷机后先确认系统版本和 framework-res 是否更新:

adb shell getprop ro.build.version.release adb shell ls -l /system/framework/framework-res.apk

然后插拔 USB 设备,观察是否还弹授权框。更严谨的验证是看dumpsys usb:

adb shell dumpsys usb | grep -i -E "DisablePermissionDialogs|permission|usb_permissions"

如果 dump 里能看到mDisablePermissionDialogs: true,基本就稳了。看不到也没关系,AOSP 的 dump 输出不一定包含这个字段。可以直接在应用里调用requestPermission(),如果 PendingIntent 立刻返回EXTRA_PERMISSION_GRANTED=true,说明静默授予成功。

再检查权限文件:

adb root adb shell cat /data/system/users/0/usb/usb_permissions.xml

授权成功后应该能看到类似:

<permissions> <permission package="com.example.usbterminal" device="..." /> </permissions>

如果文件里没有记录,说明授权没有持久化。可能的原因是应用调用的是openDevice()而不是requestPermission(),或者 PendingIntent 没有正确发送。也可能是hasPermission()检查发生在requestPermission()之前,应用直接抛异常了。

注意:mDisablePermissionDialogs只负责“不弹窗直接授予”,不负责“对所有未请求的应用无条件放行”。应用侧仍然要发起一次权限请求,这是 Android USB 权限模型的基本要求。

4. 当 overlay 不生效时:改 UsbUserPermissionManager 实现按包名白名单默认授权

4.1 修改 requestPermission 直接 grant 的最小补丁

有些厂商 ROM 把config_disableUsbPermissionDialogs删了,或者UsbUserPermissionManager被改得面目全非。这时候只能在服务里补。最小补丁是在requestPermission()开头直接授权:

public void requestPermission(UsbDevice device, String packageName, PendingIntent pi, int uid) { Intent intent = new Intent(); // 定制:默认授予第三方应用 USB 权限 grantDevicePermission(device, uid); intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true); try { pi.send(mContext, 0, intent); } catch (PendingIntent.CanceledException e) { Slog.e(TAG, "send permission intent failed", e); } return; }

这个补丁最直接,但风险也最大。所有调用requestPermission()的应用都会拿到权限,没有包名、没有 VID/PID 过滤。只适合完全封闭的系统,比如设备只允许安装一个应用,或者系统根本不开放应用安装。生产环境如果还能装第三方 APK,这个改法等于把 USB 设备全部暴露。

4.2 只对指定包名/UID 放行:白名单补丁

更合理的做法是白名单。系统属性适合做简单开关和白名单,长度有限,但存几个包名够用。在UsbUserPermissionManager里加两个方法:

private boolean isDefaultGrantPackage(String packageName) { if (packageName == null) { return false; } String list = SystemProperties.get("persist.sys.usb.default_grant_packages", ""); if (list.isEmpty()) { return false; } for (String pkg : list.split(",")) { if (pkg.trim().equals(packageName)) { return true; } } return false; } private boolean isAllowedDevice(UsbDevice device) { // 示例:只放行指定 VID/PID,防止所有 USB 设备都被默认授权 int vid = device.getVendorId(); int pid = device.getProductId(); return (vid == 0x1A86 && pid == 0x7523) // CH340 || (vid == 0x10C4 && pid == 0xEA60); // CP210x }

然后在requestPermission()里改成:

if (mDisablePermissionDialogs || (isDefaultGrantPackage(packageName) && isAllowedDevice(device))) { grantDevicePermission(device, uid); intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true); try { pi.send(mContext, 0, intent); } catch (PendingIntent.CanceledException e) { Slog.e(TAG, "send permission intent failed", e); } return; }

刷机后通过属性设置白名单:

adb shell setprop persist.sys.usb.default_grant_packages com.example.usbterminal

如果应用不主动请求权限,还要在hasPermission()里加同样判断:

public boolean hasPermission(UsbDevice device, String packageName, int pid, int uid) { if (mContext.checkPermission(android.Manifest.permission.MANAGE_USB, pid, uid) == PackageManager.PERMISSION_GRANTED) { return true; } if (isDefaultGrantPackage(packageName) && isAllowedDevice(device)) { return true; } // 原有逻辑 return checkPermissionFromFile(device, packageName, pid, uid); }

这样应用即使跳过requestPermission(),直接openDevice(),也能通过。但注意,hasPermission()放行不会写usb_permissions.xml,每次插拔都靠白名单判断。如果白名单属性被清掉,权限立刻失效。这反而是好事,方便紧急关闭。

4.3 改 hasPermission 的风险:为什么不建议一刀切

直接把hasPermission()改成return true;是最省事的,但也是最危险的。Android 的 USB 权限不仅是给串口、扫码枪用的,还保护键盘、鼠标、存储、调试器、音频设备。如果任何第三方应用都能无授权打开任意 USB 设备,恶意应用可以监听 USB 键盘输入、读取 U 盘数据、访问调试接口。对于自助终端,可能还有现金模块、身份证读卡器、打印机,风险更高。

所以我的原则是:能用资源 overlay 就用 overlay;overlay 不够就白名单;白名单也尽量绑定 VID/PID 和包名;实在不行才全量放行,并且必须保证系统不能安装未知来源应用。很多定制盒子系统会限制第三方应用安装,这类设备做全量默认授权风险相对可控。但只要设备开放了应用市场或者 ADB 安装,就不建议全量放开。

另外,grantDevicePermission()会持久化权限。如果先全量放行了一段时间,权限文件里已经写了很多包名。后来改成白名单,之前写入的权限不会自动删除。需要清掉/data/system/users/<userId>/usb/usb_permissions.xml或者调用revokeDevicePermission()。测试时最好每次重置该文件。

5. 应用侧不踩坑:device_filter、intent-filter 与 PendingIntent 的正确配合

5.1 AndroidManifest 与 device_filter.xml 的完整示例

系统层默认授权之后,应用侧仍然要声明 USB host 特性和设备过滤。完整 Manifest 示例:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.usbterminal"> <uses-feature android:name="android.hardware.usb.host" android:required="true" /> <application android:allowBackup="false" android:label="USB Terminal"> <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTask"> <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity> <service android:name=".UsbForegroundService" /> </application> </manifest>

device_filter.xml放在res/xml/下:

<?xml version="1.0" encoding="utf-8"?> <resources> <!-- CH340: VID 0x1A86 = 6790, PID 0x7523 = 29987 --> <usb-device vendor-id="6790" product-id="29987" /> <!-- CP210x: VID 0x10C4 = 4292, PID 0xEA60 = 60000 --> <usb-device vendor-id="4292" product-id="60000" /> <!-- 也可以按 class 过滤,比如 CDC 设备 --> <usb-device class="2" subclass="0" protocol="0" /> </resources>

注意vendor-id和product-id是十进制。很多教程直接写0x1A86,编译不过。转换关系要算清楚:0x1A86 = 1*4096 + 10*256 + 8*16 + 6 = 6790。0x7523 = 7*4096 + 5*256 + 2*16 + 3 = 29987。写错一个数字,attach 就不会启动对应 Activity。

android:exported="true"在 Android 12 以后是强制要求,Android 11 上加上也没问题。launchMode="singleTask"可以避免多次插入时启动多个 Activity 实例。如果应用只有一个 Activity,也可以在onNewIntent()里处理新的 USB 设备。

5.2 前台服务里主动 requestPermission 的写法

对于无人值守设备,我不建议依赖 Activity 启动来授权。更稳的方式是应用开机后启动前台服务,主动枚举 USB 设备并请求权限。系统层已经默认授权,requestPermission()会立刻回调,不会弹窗。

private static final String ACTION_USB_PERMISSION = "com.example.usbterminal.USB_PERMISSION"; private final BroadcastReceiver permissionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ACTION_USB_PERMISSION.equals(intent.getAction())) { boolean granted = intent.getBooleanExtra( UsbManager.EXTRA_PERMISSION_GRANTED, false); UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (granted && device != null) { openUsbDevice(device); } } } }; private void requestUsbPermission(UsbDevice device) { UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); Intent intent = new Intent(ACTION_USB_PERMISSION); intent.setPackage(getPackageName()); PendingIntent pi = PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); manager.requestPermission(device, pi); }

注册广播:

IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(permissionReceiver, filter);

Android 11 上这样写没问题。Android 13 以后注册非系统广播可能需要RECEIVER_NOT_EXPORTED,但本文针对 Android 11,先不展开。PendingIntent.FLAG_IMMUTABLE在 Android 11 已经可用,加上可以兼容 Android 12 以后的要求。

有一个细节:UsbManager.EXTRA_DEVICE在权限回调里不一定有,有些版本只给EXTRA_PERMISSION_GRANTED。如果拿不到 device,可以在请求前把设备保存到成员变量。我们在工业项目里通常维护一个Map<String, UsbDevice>,用device.getDeviceName()做 key,避免回调丢失设备对象。

5.3 Android 11 后台限制与 attach 广播的注意事项

Android 10 开始,后台启动 Activity 受到严格限制。USB 设备插入时,系统想启动应用的 Activity,如果应用在后台,可能被拦住。对于无人值守设备,最稳的是让应用常驻前台服务,并在BOOT_COMPLETED后启动。前台服务里监听 USB 插入,主动请求权限。这样不依赖 Activity 启动,也不受后台启动限制影响。

另一个坑是USB_DEVICE_ATTACHED的 intent-filter 只对 Activity 生效,不是广播接收器。很多开发者写一个 BroadcastReceiver 监听这个 action,结果发现收不到。Android 的 USB attach 广播是发给注册了 intent-filter 的 Activity,或者由系统内部处理。普通应用静态注册 Receiver 监听USB_DEVICE_ATTACHED通常无效。正确做法是让 Activity 声明 intent-filter,或者在前台服务里用UsbManager.getDeviceList()轮询加USB_DEVICE_ATTACHED动态注册?实际上动态注册USB_DEVICE_ATTACHED也不是所有版本都支持。更稳的是前台服务定时枚举,或者通过UsbManager.getDeviceList()在onResume和onNewIntent里处理。

还有device_filter.xml的匹配范围。它只影响系统是否把 attach 事件派给该 Activity,不限制后续openDevice()。也就是说,如果系统默认授予了 USB 权限,应用 A 的 filter 只匹配 VID 0x1A86,但应用 A 仍然可以打开其他 USB 设备,只要它有权限。要限制设备范围,必须在应用侧自己判断 VID/PID,或者在系统白名单补丁里按 VID/PID 放行。

6. 验证、回归与安全边界:默认授权之后必须做的几件事

6.1 用 dumpsys、日志和文件确认权限已落盘

改完系统后不能只看“弹窗没了”就结束。要确认权限是否真的落盘,否则应用重启、设备拔插后可能又失效。常用命令:

adb root adb shell dumpsys usb adb shell ls -l /data/system/users/0/usb/ adb shell cat /data/system/users/0/usb/usb_permissions.xml

日志方面:

adb logcat -s UsbUserPermissionManager UsbProfileGroupSettingsManager UsbService UsbHostManager

插拔设备时留意有没有grantDevicePermission、requestPermission、UsbDeviceAttached相关日志。如果看到SecurityException或者User has not given permission,说明权限没有授予成功。可能是应用没有调用requestPermission(),也可能是hasPermission()没有放行。

如果权限文件里已经有记录,但应用仍然打不开设备,检查是不是 UID 不匹配。Android 的 USB 权限按 UID 和包名记录,应用卸载重装后 UID 可能变化。如果设备是预置应用,升级方式不同,UID 可能保持不变。但如果是覆盖安装,权限文件里的旧记录可能失效。测试时最好卸载重装,再走一遍流程。

6.2 多用户、多设备、VID/PID 冲突的回归测试

多用户设备要逐个用户测试。USB 权限文件是 per-user 的,/data/system/users/0/、/data/system/users/10/、/data/system/users/11/各有一份。资源 overlay 对所有用户生效,但已经持久化的权限不会跨用户共享。如果设备会在用户之间切换,每个用户下都要触发一次授权。对于 kiosk 设备,通常只有一个用户,问题不大。

多设备场景要注意 VID/PID 相同的情况。两个同型号扫码枪,UsbDevice的deviceName可能不同,但 VID/PID 相同。如果白名单只按 VID/PID 放行,两个设备都会拿到权限。如果只想让指定端口上的设备可用,需要结合device.getDeviceName()或者序列号。但getSerialNumber()在未授权前可能拿不到,因为读取序列号本身需要权限。这是一个鸡生蛋问题。实战中通常按 VID/PID 加物理端口路径做区分,或者干脆接受同型号设备全部放行。

回归测试清单可以列成表:

测试项操作预期
冷启动设备断电重启后插入 USB应用自动获得权限,无需点弹窗
热插拔拔掉再插入同一设备权限仍在,openDevice成功
多用户切换到另一个用户需要该用户下重新授权,或系统默认授予生效
卸载重装卸载应用后重新安装旧权限失效,重新请求后自动授予
白名单外应用安装未在白名单的 APK仍然弹窗或拒绝,不能拿到权限
非目标 VID/PID插入 U 盘或键盘不自动授予,避免误放行

这个表在项目验收时特别有用。客户往往只测一个扫码枪,结果现场还有其他 USB 设备,一插上就被默认授权,安全审计过不了。

6.3 安全退出策略:白名单、设备过滤与可关闭开关

默认授予 USB 权限本质上是降低安全边界。它适合受控设备,不适合通用手机、平板。做定制系统时,最好留一个可关闭的开关。最简单的是系统属性:

persist.sys.usb.default_grant 1

在代码里判断:

private boolean isDefaultGrantEnabled() { return SystemProperties.getInt("persist.sys.usb.default_grant", 0) == 1; }

结合包名和 VID/PID 白名单,做到三层控制:第一层,总开关关闭时恢复弹窗;第二层,只有白名单包名能默认授予;第三层,只有白名单 VID/PID 能默认授予。这样即使系统被装了其他应用,也不能随意访问 USB 设备。售后维护时,可以把总开关关掉,现场立刻恢复系统默认行为。

另外,设备过滤device_filter.xml也要认真写。不要图省事写一个空的 filter 或者匹配所有 class。匹配范围越大,系统 attach 时越容易启动错误的应用。工业设备里经常同时插扫码枪、打印机、键盘,如果 filter 写得太宽,应用会被反复启动,甚至抢到键盘的 attach 事件。按实际 VID/PID 精确匹配,是最省心的做法。

最后再分享一个小经验:改完 framework 后,先别急着做完整 OTA。用adb root && adb remount把framework-res.apk或services.jar推上去,删掉对应的 oat 缓存,重启验证。services.jar替换后要清理/system/framework/oat/arm64/services.*,否则系统可能继续用旧的 odex/vdex,补丁看起来没生效。等行为验证通过,再出正式 system.img。这个流程能省下大量反复刷机的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询