前言
本文基于: RK3128 + Android7.1 进行开发
Android的USB开发, 经常碰到权限的问题, 比如, 本文所提及的一类弹窗: 允许应用”XXX应用”访问该USB设备吗?
在RK3128 android 7.1的平台上, 碰到的问题是, 勾选了: 默认情况下使用该USB设备 后, 每次拔插USB 设备, 弹窗还是会出现
问题分析
-
AndroidManifest.xml
设备监听, 可选, 用于监听设备的拔插状态, 当指定VID/PID的设备接入是, 会自动调起MyUsbActivity
情况1: 未设置为默认时, 系统会通过权限窗口, 提示赋予权限, 确定后启动Activity
情况2: 已经设置默认时, 系统直接调起Activity, 跳过权限申请窗口<activity android:name=".MyUsbActivity" android:process=":usbTest"> <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/usb_device_filter"/> </activity>
usb_device_filter.xml
, 需要监听的设备列表<?xml version="1.0" encoding="utf-8"?> <resources xmlns:android="http://schemas.android.com/apk/res/android"> <usb-device vendor-id="9595" product-id="12305" class="3" subclass="0" protocol="0"/> <usb-device vendor-id="1137" product-id="28675" /> </resources>
-
App 申请USB设备权限的代码:
final String USB_ACTION = "action_to_request_usb_device_permission"; UsbManager usbMgr; UsbDevice dev; Context ctx; //1. 记得先注册广播接收, 在获得设备访问权限后会有广播回来 private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(USB_ACTION.equals(intent.getAction())){ //可以开始连接USB设备 } } }; IntentFilter filter = new IntentFilter(USB_ACTION); ctx.registerReceiver(receiver, filter); //2. 检查是否有权限 usbMgr.hasPermission(dev); //3. 若无权限, 获取到UsbDevice后,发送请求 PendingIntent pi = PendingIntent.getBroadcast(ctx, 0, new Intent(USB_ACTION), 0); usbMgr.requestPermission(dev, pi);
-
有用的命令: dumpsys usb
-
输出结果, 重点关注两点:
-
USB 设备的节点: /dev/bus/usb/001/010
-
Device permissions: 获得权限的设备列表
设备节点 有权限的应用的UID /dev/bus/usb/001/010 10036
以下LOG信息, 是在连接了USB设备, 并且APP已经获取了权限
USB Manager State: USB Device State: mCurrentFunctions: adb mCurrentFunctionsApplied: true mConnected: true mConfigured: true mUsbDataUnlocked: true mCurrentAccessory: null mHostConnected: false mSourcePower: false mSinkPower: false mUsbCharging: false Kernel state: CONFIGURED Kernel function list: ffs USB Host State: /dev/bus/usb/001/010: UsbDevice[mName=/dev/bus/usb/001/010,mVendorId=1137,mProductId=28675,mClass=0,mSubclass=0,mProtocol=0,mManufacturerName=USB,mProductName=Smart Card Reader,mVersion=1.16,mSerialNumber=201307262013728,mConfigurations=[ UsbConfiguration[mId=1,mName=null,mAttributes=128,mMaxPower=75,mInterfaces=[ UsbInterface[mId=0,mAlternateSetting=0,mName=HID Iterface,mClass=3,mSubclass=0,mProtocol=0,mEndpoints=[ UsbEndpoint[mAddress=131,mAttributes=3,mMaxPacketSize=64,mInterval=1]] UsbInterface[mId=1,mAlternateSetting=0,mName=null,mClass=3,mSubclass=0,mProtocol=1,mEndpoints=[ UsbEndpoint[mAddress=129,mAttributes=3,mMaxPacketSize=8,mInterval=1]]]] USB Port State: <no ports> USB Audio Devices: USB MIDI Devices: Settings for user 0: Device permissions: /dev/bus/usb/001/010: 10036 Accessory permissions: Device preferences: DeviceFilter[mVendorId=1137,mProductId=28675,mClass=0,mSubclass=0,mProtocol=0,mManufacturerName=USB,mProductName=Smart Card Reader,mSerialNumber=201307262013728]: com.your.packagename Accessory preferences:
-
-
-
frameworks/base/services/usb/java/com/android/server/usb/UsbService.java
//判断是否有权限 @Override public boolean hasDevicePermission(UsbDevice device) { final int userId = UserHandle.getCallingUserId(); return getSettingsForUser(userId).hasPermission(device); } private UsbSettingsManager getSettingsForUser(int userId) { synchronized (mLock) { UsbSettingsManager settings = mSettingsByUser.get(userId); if (settings == null) { settings = new UsbSettingsManager(mContext, new UserHandle(userId)); mSettingsByUser.put(userId, settings); } return settings; } }
-
frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java
public boolean hasPermission(UsbDevice device) { synchronized (mLock) { int uid = Binder.getCallingUid(); if (uid == Process.SYSTEM_UID || mDisablePermissionDialogs) { return true; } SparseBooleanArray uidList = mDevicePermissionMap.get(device.getDeviceName()); if (uidList == null) { return false; } return uidList.get(uid); } }
mDisablePermissionDialogs
的值来自: frameworks/base/core/res/res/values/config.xml<bool name="config_disableUsbPermissionDialogs">false</bool>
顾名思义, 如果吧这个值改为true, USB权限的弹窗将不再出现, 应用也不再需要申请权限 [解决方案 1]
mDevicePermissionMap
中保存了设备的权限映射集合, 在申请成功后,grantDevicePermission
会将授权的设备和应用写入集合中, 而在USB设备断开后, 函数deviceDetached
会清掉对应的授权信息, 导致应用再次获取权限失败, 这就是问题的根源 -
开始申请USB设备权限
-
frameworks/base/services/usb/java/com/android/server/usb/UsbService.java
@Override public void requestDevicePermission(UsbDevice device, String packageName, PendingIntent pi) { final int userId = UserHandle.getCallingUserId(); getSettingsForUser(userId).requestPermission(device, packageName, pi); }
-
frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java
private void requestPermissionDialog(Intent intent, String packageName, PendingIntent pi) { final int uid = Binder.getCallingUid(); // compare uid with packageName to foil apps pretending to be someone else try { ApplicationInfo aInfo = mPackageManager.getApplicationInfo(packageName, 0); if (aInfo.uid != uid) { throw new IllegalArgumentException("package " + packageName + " does not match caller's uid " + uid); } } catch (PackageManager.NameNotFoundException e) { throw new IllegalArgumentException("package " + packageName + " not found"); } long identity = Binder.clearCallingIdentity(); intent.setClassName("com.android.systemui", "com.android.systemui.usb.UsbPermissionActivity"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_INTENT, pi); intent.putExtra("package", packageName); intent.putExtra(Intent.EXTRA_UID, uid); try { mUserContext.startActivityAsUser(intent, mUser); } catch (ActivityNotFoundException e) { Slog.e(TAG, "unable to start UsbPermissionActivity"); } finally { Binder.restoreCallingIdentity(identity); } }
-
转到SystemUI
frameworks/base/packages/SystemUI/AndroidManifest.xml<activity android:name=".usb.UsbPermissionActivity" android:exported="true" android:permission="android.permission.MANAGE_USB" android:theme="@style/Theme.SystemUI.Dialog.Alert" android:finishOnCloseSystemDialogs="true" android:excludeFromRecents="true"> </activity>
-
frameworks/base/packages/SystemUI/src/com/android/systemui/usb/UsbPermissionActivity.java
@Override public void onDestroy() { IBinder b = ServiceManager.getService(USB_SERVICE); IUsbManager service = IUsbManager.Stub.asInterface(b); // send response via pending intent Intent intent = new Intent(); try { if (mDevice != null) { intent.putExtra(UsbManager.EXTRA_DEVICE, mDevice); if (mPermissionGranted) { service.grantDevicePermission(mDevice, mUid); if (mAlwaysUse.isChecked()) { final int userId = UserHandle.getUserId(mUid); service.setDevicePackage(mDevice, mPackageName, userId); } } } if (mAccessory != null) { intent.putExtra(UsbManager.EXTRA_ACCESSORY, mAccessory); if (mPermissionGranted) { service.grantAccessoryPermission(mAccessory, mUid); if (mAlwaysUse.isChecked()) { final int userId = UserHandle.getUserId(mUid); service.setAccessoryPackage(mAccessory, mPackageName, userId); } } } intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, mPermissionGranted); mPendingIntent.send(this, 0, intent); } catch (PendingIntent.CanceledException e) { Log.w(TAG, "PendingIntent was cancelled"); } catch (RemoteException e) { Log.e(TAG, "IUsbService connection failed", e); } if (mDisconnectedReceiver != null) { unregisterReceiver(mDisconnectedReceiver); } super.onDestroy(); }
-
-
两个主要的函数:
frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java-
grantAccessoryPermission: 更新
mDevicePermissionMap
public void grantDevicePermission(UsbDevice device, int uid) { synchronized (mLock) { String deviceName = device.getDeviceName(); SparseBooleanArray uidList = mDevicePermissionMap.get(deviceName); if (uidList == null) { uidList = new SparseBooleanArray(1); mDevicePermissionMap.put(deviceName, uidList); } uidList.put(uid, true); } }
-
setDevicePackage:
writeSettingsLocked
将会把默认包名和设备绑定信息写入文件.public void setDevicePackage(UsbDevice device, String packageName) { DeviceFilter filter = new DeviceFilter(device); boolean changed = false; synchronized (mLock) { if (packageName == null) { changed = (mDevicePreferenceMap.remove(filter) != null); } else { changed = !packageName.equals(mDevicePreferenceMap.get(filter)); if (changed) { mDevicePreferenceMap.put(filter, packageName); } } if (changed) { writeSettingsLocked(); } } } private void writeSettingsLocked() { if (DEBUG) Slog.v(TAG, "writeSettingsLocked()"); FileOutputStream fos = null; try { fos = mSettingsFile.startWrite(); FastXmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(fos, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "settings"); for (DeviceFilter filter : mDevicePreferenceMap.keySet()) { serializer.startTag(null, "preference"); serializer.attribute(null, "package", mDevicePreferenceMap.get(filter)); filter.write(serializer); serializer.endTag(null, "preference"); } for (AccessoryFilter filter : mAccessoryPreferenceMap.keySet()) { serializer.startTag(null, "preference"); serializer.attribute(null, "package", mAccessoryPreferenceMap.get(filter)); filter.write(serializer); serializer.endTag(null, "preference"); } serializer.endTag(null, "settings"); serializer.endDocument(); mSettingsFile.finishWrite(fos); } catch (IOException e) { Slog.e(TAG, "Failed to write settings", e); if (fos != null) { mSettingsFile.failWrite(fos); } } }
-
-
其中
mSettingsFile
= new AtomicFile(new File( Environment.getUserSystemDirectory(user.getIdentifier()), “usb_device_manager.xml”));
对应的系统重的目录: /data/system/users/0/usb_device_manager.xml<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <settings> <preference package="com.your.packagename"> <usb-device vendor-id="1137" product-id="28675" class="0" subclass="0" protocol="0" manufacturer-name="USB" product-name="Smart Card Reader" serial-number="201307262013728" /> </preference> </settings>
小结: 常规的流程, 到这里已经走完. 要解决勾默认弹窗后, 还会再次出现弹窗的问题, 需要做的是:
- APP 中增加USB设备清单, 即前面提及的文件:
usb_device_filter.xml
- 在App 的
AndroidManifest.xml
中添加Activity, 以便系统服务在检测到指定的USB设备接入后, 正常调起并正常赋予设备权限.
在完成了这两点后, 需要在第一次调起权限窗口时, 勾选默认并确认授权, 后续设备接入都不会再弹窗.
frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java
设备接入
//设备接入
public void deviceAttached(UsbDevice device) {
final Intent intent = createDeviceAttachedIntent(device);
// Send broadcast to running activity with registered intent
mUserContext.sendBroadcast(intent);
if (MtpNotificationManager.shouldShowNotification(mPackageManager, device)) {
// Show notification if the device is MTP storage.
mMtpNotificationManager.showNotification(device);
} else {
resolveActivity(intent, device);
}
}
查找默认的Activity
private void resolveActivity(Intent intent, UsbDevice device) {
ArrayList<ResolveInfo> matches;
String defaultPackage;
synchronized (mLock) {
matches = getDeviceMatchesLocked(device, intent);
// Launch our default activity directly, if we have one.
// Otherwise we will start the UsbResolverActivity to allow the user to choose.
defaultPackage = mDevicePreferenceMap.get(new DeviceFilter(device));
}
// Start activity with registered intent
resolveActivity(intent, matches, defaultPackage, device, null);
}
private void resolveActivity(Intent intent, ArrayList<ResolveInfo> matches,
String defaultPackage, UsbDevice device, UsbAccessory accessory) {
int count = matches.size();
// don't show the resolver activity if there are no choices available
if (count == 0) {
if (accessory != null) {
String uri = accessory.getUri();
if (uri != null && uri.length() > 0) {
// display URI to user
// start UsbResolverActivity so user can choose an activity
Intent dialogIntent = new Intent();
dialogIntent.setClassName("com.android.systemui",
"com.android.systemui.usb.UsbAccessoryUriActivity");
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
dialogIntent.putExtra("uri", uri);
try {
mUserContext.startActivityAsUser(dialogIntent, mUser);
} catch (ActivityNotFoundException e) {
Slog.e(TAG, "unable to start UsbAccessoryUriActivity");
}
}
}
// do nothing
return;
}
ResolveInfo defaultRI = null;
if (count == 1 && defaultPackage == null) {
// Check to see if our single choice is on the system partition.
// If so, treat it as our default without calling UsbResolverActivity
ResolveInfo rInfo = matches.get(0);
if (rInfo.activityInfo != null &&
rInfo.activityInfo.applicationInfo != null &&
(rInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
defaultRI = rInfo;
}
if (mDisablePermissionDialogs) {
// bypass dialog and launch the only matching activity
rInfo = matches.get(0);
if (rInfo.activityInfo != null) {
defaultPackage = rInfo.activityInfo.packageName;
}
}
}
if (defaultRI == null && defaultPackage != null) {
// look for default activity
for (int i = 0; i < count; i++) {
ResolveInfo rInfo = matches.get(i);
if (rInfo.activityInfo != null &&
defaultPackage.equals(rInfo.activityInfo.packageName)) {
defaultRI = rInfo;
break;
}
}
}
if (defaultRI != null) {
// grant permission for default activity
if (device != null) {
grantDevicePermission(device, defaultRI.activityInfo.applicationInfo.uid);
} else if (accessory != null) {
grantAccessoryPermission(accessory, defaultRI.activityInfo.applicationInfo.uid);
}
// start default activity directly
try {
intent.setComponent(
new ComponentName(defaultRI.activityInfo.packageName,
defaultRI.activityInfo.name));
mUserContext.startActivityAsUser(intent, mUser);
} catch (ActivityNotFoundException e) {
Slog.e(TAG, "startActivity failed", e);
}
} else {
Intent resolverIntent = new Intent();
resolverIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (count == 1) {
// start UsbConfirmActivity if there is only one choice
resolverIntent.setClassName("com.android.systemui",
"com.android.systemui.usb.UsbConfirmActivity");
resolverIntent.putExtra("rinfo", matches.get(0));
if (device != null) {
resolverIntent.putExtra(UsbManager.EXTRA_DEVICE, device);
} else {
resolverIntent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
}
} else {
// start UsbResolverActivity so user can choose an activity
resolverIntent.setClassName("com.android.systemui",
"com.android.systemui.usb.UsbResolverActivity");
resolverIntent.putParcelableArrayListExtra("rlist", matches);
resolverIntent.putExtra(Intent.EXTRA_INTENT, intent);
}
try {
mUserContext.startActivityAsUser(resolverIntent, mUser);
} catch (ActivityNotFoundException e) {
Slog.e(TAG, "unable to start activity " + resolverIntent);
}
}
}
赋予权限
public void grantDevicePermission(UsbDevice device, int uid) {
synchronized (mLock) {
String deviceName = device.getDeviceName();
SparseBooleanArray uidList = mDevicePermissionMap.get(deviceName);
if (uidList == null) {
uidList = new SparseBooleanArray(1);
mDevicePermissionMap.put(deviceName, uidList);
}
uidList.put(uid, true);
}
}
[解决方案 3] 增加系统代码, 通过读取已经保存的设备和包名的绑定信息来自动赋予应用USB设备权限
git diff frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java diff --git a/frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java b/frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java index de9ede397c..5e72d683a3 100644 --- a/frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java +++ b/frameworks/base/services/usb/java/com/android/server/usb/UsbSettingsManager.java @@ -743,9 +743,30 @@ class UsbSettingsManager { mMtpNotificationManager.showNotification(device); } else { resolveActivity(intent, device); + //AnsonCode + grantPackagePermission(device, null); } } + //AnsonCode + void d(String s){android.util.Log.d("UsbSettingsManager", "ALog." + s);} + void grantPackagePermission(UsbDevice device, UsbAccessory accessory){ + //d("grantPackagePermission"); + String defaultPackage = mDevicePreferenceMap.get(new DeviceFilter(device)); + if(defaultPackage != null){ + try { + ApplicationInfo aInfo = mPackageManager.getApplicationInfo(defaultPackage, 0); + if(aInfo != null){ + if(device != null)grantDevicePermission(device, aInfo.uid); + if(accessory != null)grantAccessoryPermission(accessory, aInfo.uid); + //d("grantPackagePermission " + defaultPackage + " uid=" + aInfo.uid); + } + } catch (PackageManager.NameNotFoundException e) { + d("NameNotFoundException:" + defaultPackage); + } + } + } + private void resolveActivity(Intent intent, UsbDevice device) { ArrayList<ResolveInfo> matches; String defaultPackage; @@ -787,6 +808,8 @@ class UsbSettingsManager { } resolveActivity(intent, matches, defaultPackage, null, accessory); + //AnsonCode + if(accessory != null) grantPackagePermission(null, accessory); }
总结
解决USB授权的弹窗方案有3种:
- 应用通过一系列的配置, 增加指定设备的过滤监听, 实现应用自启并获得权限
- 修改系统配置, 关闭USB权限弹窗
- 修改系统Framework中的USB服务, 增加通过默认绑定包名自动赋权.